Skip to content

feat: implement GET /api/wallet endpoint (#112)#138

Open
DrGalio wants to merge 3 commits into
devasignhq:mainfrom
DrGalio:feat/wallet-endpoints
Open

feat: implement GET /api/wallet endpoint (#112)#138
DrGalio wants to merge 3 commits into
devasignhq:mainfrom
DrGalio:feat/wallet-endpoints

Conversation

@DrGalio

@DrGalio DrGalio commented Mar 25, 2026

Copy link
Copy Markdown

Changes

Implements wallet-related endpoints and utilities.

Endpoints:

  1. GET /api/wallet (Implement GET /wallet — balance and pending earnings #112) — Returns wallet info: USDC balance, pending/in-review earnings, recent transactions
  2. GET /api/wallet/transactions (Implement GET /wallet/transactions — transaction history #113) — Paginated transaction history with bounty references

Utilities:

  1. AES-256-GCM encryption (Build wallet encryption utility (AES-256-GCM) #110) — encrypt()/decrypt() for Stellar wallet secrets with IV+AuthTag format

Response format (GET /api/wallet):

{
  "wallet": {
    "address": "G...",
    "balance": "150.50",
    "pending": "75.00",
    "inReview": "25.00",
    "bountiesCompleted": 3
  },
  "recentTransactions": [...]
}

Files:

  • packages/api/src/routes/wallet.ts — wallet route handler
  • packages/api/src/utils/wallet-encryption.ts — AES-256-GCM encrypt/decrypt
  • packages/api/src/__tests__/wallet.test.ts — endpoint tests
  • packages/api/src/__tests__/wallet-encryption.test.ts — encryption tests
  • packages/api/src/app.ts — register wallet route

Tests: 172 passing (10 new)

Closes #110, #112, #113

Returns authenticated user's wallet info:
- USDC balance (completed earnings from bounties)
- Pending earnings from approved submissions
- In-review earnings from pending submissions
- Recent transaction history (last 20)

Includes tests (all 162 passing).
@devasign-app

devasign-app Bot commented Mar 25, 2026

Copy link
Copy Markdown

Merge Score: 75/100

🟡 ███████████████░░░░░ 75%

The PR successfully implements the /api/wallet endpoint with proper database queries and authorization. However, it misses the core requirement of fetching the USDC balance directly from the Stellar network, relying on the database instead. Additionally, performance can be improved by running independent database queries concurrently, and the test assertions should be strengthened.

Code Suggestions (3)

High Priority (1)

  1. packages/api/src/routes/wallet.ts (Line 23)
    The endpoint returns the totalEarned value from the database instead of fetching the user's current USDC balance from the Stellar network as requested in the linked issue.

Reasoning: Relying on the database field may result in inaccurate balances if the user has external transfers. The balance should be queried directly from the Stellar blockchain to ensure accuracy and fulfill the core requirement of the issue.

Medium Priority (1)

  1. packages/api/src/routes/wallet.ts (Line 38)
    Run independent database queries concurrently using Promise.all to reduce the endpoint's response time.

Reasoning: The queries for pending earnings, in-review earnings, and recent transactions do not depend on each other. Awaiting them sequentially creates a waterfall effect, increasing the total latency of the request.

Suggested Code:

    // Calculate pending, in-review earnings, and recent transactions concurrently
    const [pendingResult, inReviewResult, recentTransactions] = await Promise.all([
        db.select({
            pendingAmount: sum(bounties.amountUsdc),
        })
        .from(submissions)
        .innerJoin(bounties, eq(submissions.bountyId, bounties.id))
        .where(
            and(
                eq(submissions.developerId, user.id),
                eq(submissions.status, 'approved')
            )
        ),
        db.select({
            inReviewAmount: sum(bounties.amountUsdc),
        })
        .from(submissions)
        .innerJoin(bounties, eq(submissions.bountyId, bounties.id))
        .where(
            and(
                eq(submissions.developerId, user.id),
                eq(submissions.status, 'pending')
            )
        ),
        db.select({
            id: transactions.id,
            type: transactions.type,
            amountUsdc: transactions.amountUsdc,
            status: transactions.status,
            bountyId: transactions.bountyId,
            stellarTxHash: transactions.stellarTxHash,
            createdAt: transactions.createdAt,
        })
        .from(transactions)
        .where(eq(transactions.userId, user.id))
        .orderBy(desc(transactions.createdAt))
        .limit(20)
    ]);

    const pendingAmount = pendingResult[0]?.pendingAmount ?? '0';
    const inReviewAmount = inReviewResult[0]?.inReviewAmount ?? '0';

Low Priority (1)

  1. packages/api/src/tests/wallet.test.ts (Line 64)
    Strengthen the assertions in the success test case to verify the response structure and status code.

Reasoning: Strict assertions ensure that the endpoint returns the correct data format and prevents future regressions if the response structure changes. Checking not.toBe(404) is too permissive.

Suggested Code:

        expect(res.status).toBe(200);
        const body = await res.json();
        expect(body.wallet).toBeDefined();
        expect(body.wallet.address).toBe('0x1234567890abcdef');
        expect(body.wallet.balance).toBe('150.5000000');
        expect(body.recentTransactions).toBeInstanceOf(Array);
📊 Review Metadata
  • Processing Time: 66s
  • Analysis Date: 3/25/2026, 8:28:28 PM

🤖 This review was generated by AI. While we strive for accuracy, please use your judgment when applying suggestions.

💬 Questions about this review? Open an issue or contact support.

Paginated transaction history with:
- Type, amount, status, bounty reference, Stellar tx hash, timestamp
- Left join to bounty titles
- Pagination with total count metadata
@devasign-app

devasign-app Bot commented Mar 25, 2026

Copy link
Copy Markdown

Updated Merge Score: 65/100

🔴 █████████████░░░░░░░ 65%

The new commits introduce a useful paginated GET /api/wallet/transactions endpoint and add input validation via Zod. However, none of the concerns from the previous review were addressed. The core requirement of fetching the USDC balance from the Stellar network is still missing, database queries are still running sequentially (now in both endpoints), and test assertions remain weak. Additionally, the new transactions endpoint lacks test coverage entirely.

Code Suggestions (4)

High Priority (1)

  1. packages/api/src/routes/wallet.ts (Line 31)
    The core requirement to fetch the USDC balance directly from the Stellar network is still missing. The endpoint continues to rely on the database users.totalEarned field.

Reasoning: Issue #112 explicitly states: 'Return user's current USDC balance from Stellar network'. Relying on the database might show stale or incorrect balances if out-of-band transactions occur.

Medium Priority (3)

  1. packages/api/src/routes/wallet.ts (Line 31)
    Run independent database queries concurrently using Promise.all in the GET /api/wallet endpoint.

Reasoning: Awaiting walletUser, pendingResult, inReviewResult, and recentTransactions sequentially increases the endpoint's response time unnecessarily. They do not depend on each other.

Suggested Code:

    const [
        [walletUser],
        pendingResult,
        inReviewResult,
        recentTransactions
    ] = await Promise.all([
        db.select({
            walletAddress: users.walletAddress,
            totalEarned: users.totalEarned,
            bountiesCompleted: users.bountiesCompleted,
        }).from(users).where(eq(users.id, user.id)).limit(1),
        db.select({ pendingAmount: sum(bounties.amountUsdc) })
            .from(submissions)
            .innerJoin(bounties, eq(submissions.bountyId, bounties.id))
            .where(and(eq(submissions.developerId, user.id), eq(submissions.status, 'approved'))),
        db.select({ inReviewAmount: sum(bounties.amountUsdc) })
            .from(submissions)
            .innerJoin(bounties, eq(submissions.bountyId, bounties.id))
            .where(and(eq(submissions.developerId, user.id), eq(submissions.status, 'pending'))),
        db.select({
            id: transactions.id,
            type: transactions.type,
            amountUsdc: transactions.amountUsdc,
            status: transactions.status,
            bountyId: transactions.bountyId,
            stellarTxHash: transactions.stellarTxHash,
            createdAt: transactions.createdAt,
        }).from(transactions).where(eq(transactions.userId, user.id)).orderBy(desc(transactions.createdAt)).limit(20)
    ]);

    if (!walletUser) {
        return c.json({ error: 'User not found' }, 404);
    }
  1. packages/api/src/routes/wallet.ts (Line 113)
    Run the paginated results and total count queries concurrently in GET /api/wallet/transactions.

Reasoning: The results query and totalCountResult query are independent and can be executed in parallel to reduce latency.

Suggested Code:

        const [results, [totalCountResult]] = await Promise.all([
            db.select({
                id: transactions.id,
                type: transactions.type,
                amountUsdc: transactions.amountUsdc,
                status: transactions.status,
                bountyId: transactions.bountyId,
                stellarTxHash: transactions.stellarTxHash,
                createdAt: transactions.createdAt,
                bounty: {
                    title: bounties.title,
                },
            })
            .from(transactions)
            .leftJoin(bounties, eq(transactions.bountyId, bounties.id))
            .where(eq(transactions.userId, user.id))
            .orderBy(desc(transactions.createdAt))
            .limit(limit)
            .offset(offset),
            db.select({ count: count() })
            .from(transactions)
            .where(eq(transactions.userId, user.id))
        ]);
  1. packages/api/src/tests/wallet.test.ts (Line 66)
    Add tests for the newly introduced GET /api/wallet/transactions endpoint and strengthen existing assertions.

Reasoning: The new endpoint is completely untested. Furthermore, the previous review's concern about weak test assertions (e.g., expect(body).toBeDefined()) remains unaddressed.

📊 Review Metadata
  • Processing Time: 49s
  • Analysis Date: 3/25/2026, 8:29:48 PM

🤖 This review was generated by AI. While we strive for accuracy, please use your judgment when applying suggestions.

💬 Questions about this review? Open an issue or contact support.

- encrypt/decrypt functions using AES-256-GCM
- IV (12 bytes) + Auth Tag (16 bytes) + Ciphertext format
- Key from WALLET_ENCRYPTION_KEY env var (64 hex chars)
- validateEncryptionKey() for health checks
- 10 tests covering round-trip, random IV, unicode, tamper detection
@devasign-app

devasign-app Bot commented Mar 25, 2026

Copy link
Copy Markdown

Updated Merge Score: 40/100

🔴 ████████░░░░░░░░░░░░ 40%

The latest push introduces a robust, well-tested AES-256-GCM encryption utility (wallet-encryption.ts). However, NONE of the concerns from the previous review were addressed. The core requirement of fetching the Stellar network balance is still missing, database queries in the wallet route are still sequential, and the /transactions endpoint remains untested. Furthermore, the new encryption utility is currently unused in the application code.

Code Suggestions (4)

High Priority (1)

  1. packages/api/src/routes/wallet.ts (Line 31)
    The core requirement of issue Implement GET /wallet — balance and pending earnings #112 is still missing. The endpoint currently returns users.totalEarned from the database instead of fetching the actual USDC balance from the Stellar network.

Reasoning: The PR description and linked issue explicitly state that the balance should be fetched from the Stellar network. Returning the database value does not fulfill the primary goal of this feature.

Medium Priority (2)

  1. packages/api/src/routes/wallet.ts (Line 31)
    Database queries in the GET /api/wallet endpoint are still running sequentially. This was raised in the previous review and remains unaddressed.

Reasoning: Running independent database queries sequentially increases the overall response time of the endpoint. Using Promise.all allows them to execute concurrently, significantly improving performance.

Suggested Code:

    // Execute independent queries concurrently
    const [
        [walletUser],
        pendingResult,
        inReviewResult,
        recentTransactions
    ] = await Promise.all([
        db.select({
            walletAddress: users.walletAddress,
            totalEarned: users.totalEarned,
            bountiesCompleted: users.bountiesCompleted,
        }).from(users).where(eq(users.id, user.id)).limit(1),
        
        db.select({ pendingAmount: sum(bounties.amountUsdc) })
          .from(submissions)
          .innerJoin(bounties, eq(submissions.bountyId, bounties.id))
          .where(and(eq(submissions.developerId, user.id), eq(submissions.status, 'approved'))),
          
        db.select({ inReviewAmount: sum(bounties.amountUsdc) })
          .from(submissions)
          .innerJoin(bounties, eq(submissions.bountyId, bounties.id))
          .where(and(eq(submissions.developerId, user.id), eq(submissions.status, 'pending'))),
          
        db.select({
            id: transactions.id,
            type: transactions.type,
            amountUsdc: transactions.amountUsdc,
            status: transactions.status,
            bountyId: transactions.bountyId,
            stellarTxHash: transactions.stellarTxHash,
            createdAt: transactions.createdAt,
        }).from(transactions).where(eq(transactions.userId, user.id)).orderBy(desc(transactions.createdAt)).limit(20)
    ]);

    if (!walletUser) {
        return c.json({ error: 'User not found' }, 404);
    }
  1. packages/api/src/tests/wallet.test.ts (Line 66)
    The GET /api/wallet/transactions endpoint still lacks test coverage.

Reasoning: Adding a new endpoint without corresponding tests reduces overall test coverage and leaves the pagination logic vulnerable to future regressions.

Low Priority (1)

  1. packages/api/src/utils/wallet-encryption.ts (Line 1)
    The newly added wallet-encryption.ts utility is well-implemented and secure, but it is currently unused anywhere in the codebase.

Reasoning: Dead code should be avoided. If this encryption utility is intended for storing Stellar secret keys, it should be integrated into the relevant routes. If it belongs to a different feature, it should be moved to a separate PR to keep this PR focused on the GET endpoint.

📊 Review Metadata
  • Processing Time: 32s
  • Analysis Date: 3/25/2026, 8:31:25 PM

🤖 This review was generated by AI. While we strive for accuracy, please use your judgment when applying suggestions.

💬 Questions about this review? Open an issue or contact support.

@devasign-agent devasign-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR implements the wallet routes, encryption utility, and registration in app.ts with corresponding tests. However, a key functional requirement is unmet: USDC balance is not retrieved from the Stellar network — it is read from a users.totalEarned DB column. There is no Stellar SDK integration or Horizon call. Other criteria are largely satisfied, though the transactions endpoint exposes amountUsdc rather than a normalized amount field and there's no explicit timestamp field name (createdAt is returned, which is acceptable but worth noting).

@devasign-agent devasign-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

End goal

Implement wallet encryption utilities and wallet API endpoints that securely handle Stellar secrets and return balance, earnings, and transaction history.

❌ Acceptance criteria not met

  • C4 — GET /api/wallet returns wallet info including USDC balance (from the Stellar network), pending earnings, in-review earnings, address, and bounties completed.
    Why it failed: Balance is read from the DB: balance: walletUser.totalEarned where totalEarned: users.totalEarned. No Stellar network query exists in the diff despite the criterion requiring 'USDC balance (from the Stellar network)'.

✅ Acceptance criteria met (7 / 8)

  • C1 — An encrypt() function encrypts Stellar wallet secret keys using AES-256-GCM with a server-side key sourced from environment variables.
  • C2 — A decrypt() function reverses the encryption, correctly reading the IV and auth tag embedded in the ciphertext format.
  • C3 — The encrypted output format includes both the IV and the authentication tag alongside the ciphertext.
  • C5 — GET /api/wallet includes recent transactions in its response.
  • C6 — GET /api/wallet/transactions returns paginated transaction history including type (earning/withdrawal), amount, bounty reference, Stellar tx hash, status, and timestamp.
  • C7 — The wallet routes are registered in the API app (app.ts).
  • C8 — Tests cover the wallet endpoints and the encryption/decryption utilities, and pass.

Suggested changes

For C4 — Fetch USDC balance from the Stellar network instead of the DB column

C4 and issue #112 explicitly require the wallet balance to reflect the on-chain USDC balance from the Stellar network. The current handler returns users.totalEarned, a DB-tracked cumulative earnings value, which can diverge from the actual on-chain balance and does not satisfy the criterion.

import { Horizon } from '@stellar/stellar-sdk';

const server = new Horizon.Server(process.env.STELLAR_HORIZON_URL!);
const account = await server.loadAccount(walletUser.walletAddress);
const usdc = account.balances.find(
  (b) => b.asset_type !== 'native' && b.asset_code === 'USDC'
);
const balance = usdc?.balance ?? '0';

Prompt for your AI agent:

Fix: Source USDC balance from the Stellar network in GET /api/wallet

File: packages/api/src/routes/wallet.ts
Symbol: walletRouter.get('/')

Issue:
The wallet balance is currently returned from the database column `users.totalEarned`, but the acceptance criterion and issue #112 require the USDC balance to be queried from the Stellar network. This makes the reported balance potentially inconsistent with the actual on-chain account state.

Suggested approach:
Use the Stellar SDK (Horizon server) to load the account by `walletUser.walletAddress`, find the USDC trustline balance, and return that as `wallet.balance`. Handle the case where the account is unfunded or the trustline is missing (default to '0'). Keep `totalEarned` available separately if needed for display.

Relevant diff:
```diff
+    return c.json({
+        wallet: {
+            address: walletUser.walletAddress,
+            balance: walletUser.totalEarned,
+            pending: pendingResult[0]?.pendingAmount ?? '0',
+            inReview: inReviewResult[0]?.inReviewAmount ?? '0',
+            bountiesCompleted: walletUser.bountiesCompleted,
+        },
+        recentTransactions,
+    });
```

The encryption utility (C1-C3) is implemented correctly with AES-256-GCM, env-sourced key, and IV+authTag+ciphertext format, with solid tests. The wallet routes are registered (C7) and transaction history endpoint (C6) is complete. However, C4 is not met: the balance is sourced from the database users.totalEarned column rather than queried from the Stellar network as the criterion (and issue #112) explicitly require. The diff contains no Stellar SDK usage or network call.


📋 One prompt to fix all of this — paste into your AI coding agent
You are helping fix PR "feat: implement GET /api/wallet endpoint (#112)" in devasignhq/mobile-app. Automated review flagged the items below as blocking approval. Apply the changes so each one passes — don't introduce changes beyond what's listed.

## End goal
Implement wallet encryption utilities and wallet API endpoints that securely handle Stellar secrets and return balance, earnings, and transaction history.

## Failed acceptance criteria

### 1. GET /api/wallet returns wallet info including USDC balance (from the Stellar network), pending earnings, in-review earnings, address, and bounties completed. (C4)
_Why it failed:_ Balance is read from the DB: `balance: walletUser.totalEarned` where `totalEarned: users.totalEarned`. No Stellar network query exists in the diff despite the criterion requiring 'USDC balance (from the Stellar network)'.

Fix: Source USDC balance from the Stellar network in GET /api/wallet

File: packages/api/src/routes/wallet.ts
Symbol: walletRouter.get('/')

Issue:
The wallet balance is currently returned from the database column `users.totalEarned`, but the acceptance criterion and issue #112 require the USDC balance to be queried from the Stellar network. This makes the reported balance potentially inconsistent with the actual on-chain account state.

Suggested approach:
Use the Stellar SDK (Horizon server) to load the account by `walletUser.walletAddress`, find the USDC trustline balance, and return that as `wallet.balance`. Handle the case where the account is unfunded or the trustline is missing (default to '0'). Keep `totalEarned` available separately if needed for display.

Relevant diff:
```diff
+    return c.json({
+        wallet: {
+            address: walletUser.walletAddress,
+            balance: walletUser.totalEarned,
+            pending: pendingResult[0]?.pendingAmount ?? '0',
+            inReview: inReviewResult[0]?.inReviewAmount ?? '0',
+            bountiesCompleted: walletUser.bountiesCompleted,
+        },
+        recentTransactions,
+    });
```

## Your task
For each failed criterion and blocker above, apply the suggested fix. Use the `Relevant diff` hunks as the anchor for where to make the change. After each change, re-verify it satisfies the criterion or addresses the blocker it's tied to.

.limit(20);

return c.json({
wallet: {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

balance: walletUser.totalEarned sources the USDC balance from the database column, but C4 / issue #112 require the balance to come from the Stellar network. Consider querying the account's USDC trustline balance via the Stellar SDK and returning that (or both).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build wallet encryption utility (AES-256-GCM)

1 participant