Skip to content

fix(grid-wallet-prod): activity list reflects the real ledger, not the UI - #745

Open
pengying wants to merge 1 commit into
graphite-base/745from
07-25-fix_grid-wallet-prod_activity_list_reflects_the_real_ledger_not_the_ui
Open

fix(grid-wallet-prod): activity list reflects the real ledger, not the UI#745
pengying wants to merge 1 commit into
graphite-base/745from
07-25-fix_grid-wallet-prod_activity_list_reflects_the_real_ledger_not_the_ui

Conversation

@pengying

@pengying pengying commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Money moved but the activity list didn't. Three causes, all real:

  • Inbound rows only ever came from the arrival webhook, which never reaches localhost without a tunnel. Every completion site now calls refreshLedger (balance and GET /transactions together), so rows come from Grid rather than from the UI.
  • Grid records an inbound pull as a DEBIT (it debits the external source), so keying the row off direction labelled arriving money "Money sent". transactionToRow now derives direction from which side the wallet is on, falling back to direction/type when the wallet id isn't known.
  • Funding arrives in two legs (fiat credit, then conversion), so fetchActivity lists only the legs that touch the wallet — the leg the balance moved on.

The push stand-in produced no row at all, for a different reason: it funded the platform's USD account and on-ramped from it. That lands spendable balance, but the transaction belongs to the platform and never appears in GET /transactions?customerId=… — measured 0 of 20 rows in the customer's list. It's now a REALTIME_FUNDING quote settled with POST /sandbox/send: the same settlement a pushed wire gets, and the customer's own transaction.

Fidelity note: a real wire to those deposit instructions lands USD in the fiat account and the sweep converts it (two legs); the stand-in settles straight into the wallet (one). Both produce a customer transaction and a row.

That removed the platform on-ramp helpers entirely, and with them GET /platform/internal-accounts and POST /sandbox/internal-accounts/{id}/fund from the proxy allowlist. The proxy injects the platform's own client credentials, so a narrower allowlist is a real reduction in what a compromised browser session can reach.

Also in this pass

  • Outbound is ledger-driven too — a pending row goes in on confirm and settles from the transaction's terminal status, so a failed quote no longer leaves a phantom "Withdrawn from balance" row.
  • Webhook cards show a static Payload label instead of a Response tab (there is no response to show), vertically centred in the tab group.
  • Panel filter chips for Requests vs Webhooks. Action cards are controls, not traffic, so they're never filtered out.
  • After USD arrives, quote+execute USD internal → USDB automatically.
  • External accounts dedupe on routing + account number (or IBAN), ignoring spacing/case. Collapses nothing today — the demo randomizes account digits on every add, so each run leaves a new real account at Grid — but it stops true repeats, which is what production sees.
  • accountLabel() shows handles whole and masks only numbers (no more •••• @upi).
  • detectAddressNetworks() + 15 tests — groundwork for pasting from the real clipboard outside sandbox. Deliberately not wired to the UI yet; the ambiguous-EVM case still needs a chain prompt.

Verification

Sandbox-probed before wiring, then end-to-end in a browser:

  • REALTIME_FUNDING + /sandbox/sendCOMPLETED, listed with the wallet as destination, spendable (not book-only) balance.
  • Push flow in the UI: balance $751 → $776, top row Money added · Added to balance · Just now · +$25.00.
  • Pull flow: +$100.00 inbound now reads as added, not sent.
  • EUR real-time funding is rejected by this platform (INVALID_INPUT: Sending currency 'EUR' is not configured), so the stand-in deposits USD and the action card says so rather than implying euros arrived.
  • 101 unit tests pass; tsc --noEmit clean.

Known, not addressed here

The push flow is hard to reach: on "Select bank" the 13 saved accounts fill the screen, and the country picker that leads to deposit instructions sits behind the small + in the top-right. Fixing it means either capping the saved list or giving the instructions path its own row on the source step — both visible design changes, so they're left for a follow-up.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Pjqydq34z8DBK1kBrioXK2

@vercel

vercel Bot commented Jul 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
grid-flow-builder Ignored Ignored Preview Jul 27, 2026 9:57pm
grid-wallet-demo Ignored Ignored Preview Jul 27, 2026 9:57pm

Request Review

Copy link
Copy Markdown
Contributor Author

@greptile-apps

greptile-apps Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes wallet activity ledger-driven and expands supporting demo behavior.

  • Refreshes balances and transaction history together, deriving transaction direction relative to the wallet.
  • Replaces platform-account sandbox funding with customer-owned real-time funding and adds automatic USD-to-USDB sweeping.
  • Adds terminal-state handling for outbound activity rows, external-account deduplication, account labels, API-panel traffic filters, and crypto-address detection.
  • Narrows the Grid proxy allowlist and adds focused unit coverage.

Confidence Score: 2/5

This PR should not merge until outbound outcomes are correlated to their transactions and terminal settlement cannot race with delayed pending-row insertion.

The new outbound lifecycle can apply stale transaction outcomes to the wrong transfer, and fast terminal outcomes can leave permanent phantom pending rows after settlement.

Files Needing Attention: components/grid-wallet-prod/src/hooks/useWalletDemoLogic.ts; components/grid-wallet-prod/src/apps/shared/wallet/useWalletHome.ts

Important Files Changed

Filename Overview
components/grid-wallet-prod/src/hooks/useWalletDemoLogic.ts Adds ledger refreshes, funding/sweep orchestration, and outbound outcome reporting, but terminal outcomes are not correlated to their transactions.
components/grid-wallet-prod/src/apps/shared/wallet/useWalletHome.ts Adds pending outbound rows and settlement logic, with an uncancelled delayed-insertion race and single-row correlation limitation.
components/grid-wallet-prod/src/lib/gridReads.ts Adds wallet-relative activity filtering/direction, fiat-balance reads, and external-account deduplication with supporting tests.
components/grid-wallet-prod/src/lib/gridFunding.ts Replaces platform on-ramp helpers with real-time funding and customer fiat-to-wallet sweep quote builders.
components/grid-wallet-prod/src/components/ApiPanel/ApiPanel.tsx Adds independent request and webhook filters while preserving action cards.
components/grid-wallet-prod/src/lib/cryptoAddresses.ts Adds checksum-aware network detection, including explicit EVM ambiguity.

Sequence Diagram

sequenceDiagram
  participant UI as Wallet UI
  participant Logic as Demo Logic
  participant Grid as Grid API/Webhooks
  UI->>Logic: Confirm outbound transfer
  Logic->>Grid: Execute quote
  UI->>UI: Schedule pending row insertion
  Grid-->>Logic: Terminal transaction outcome
  Logic-->>UI: transferOutcome(ok, nonce)
  UI->>UI: Settle current pendingRow
  Note over UI,Logic: Outcome currently lacks transaction identity
  UI->>UI: Delayed timer may still insert Pending row
Loading
Prompt To Fix All With AI
### Issue 1
components/grid-wallet-prod/src/hooks/useWalletDemoLogic.ts:490-494
**Uncorrelated outcomes settle wrong transfer**

When an earlier or overlapping outbound transaction reaches a terminal status while another transfer is pending, this handler discards the Grid transaction ID and reports only `ok`; `settleTransfer` then consumes whichever row currently occupies the global `pendingRow`, causing the wrong row and toast to be settled while the matching transfer remains pending.

### Issue 2
components/grid-wallet-prod/src/apps/shared/wallet/useWalletHome.ts:526-530
**Settlement races pending-row insertion**

When an execute error or terminal webhook arrives within the 700 ms insertion delay, `settleTransfer` clears the ref and filters activity before this row exists, but this uncancelled timer subsequently inserts it. The failed or completed transfer is then shown permanently as Pending because its synthetic ID cannot be reconciled with a Grid transaction.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(grid-wallet-prod): activity list ref..." | Re-trigger Greptile

Comment on lines +490 to +494
if (event.type?.startsWith('OUTGOING_PAYMENT.') && data) {
if (data.status === 'COMPLETED') reportTransfer(true);
else if (['FAILED', 'REJECTED', 'REFUNDED', 'EXPIRED'].includes(data.status)) {
reportTransfer(false);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Uncorrelated outcomes settle wrong transfer

When an earlier or overlapping outbound transaction reaches a terminal status while another transfer is pending, this handler discards the Grid transaction ID and reports only ok; settleTransfer then consumes whichever row currently occupies the global pendingRow, causing the wrong row and toast to be settled while the matching transfer remains pending.

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/grid-wallet-prod/src/hooks/useWalletDemoLogic.ts
Line: 490-494

Comment:
**Uncorrelated outcomes settle wrong transfer**

When an earlier or overlapping outbound transaction reaches a terminal status while another transfer is pending, this handler discards the Grid transaction ID and reports only `ok`; `settleTransfer` then consumes whichever row currently occupies the global `pendingRow`, causing the wrong row and toast to be settled while the matching transfer remains pending.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines 526 to 530
window.clearTimeout(sheetInsertTimer.current);
sheetInsertTimer.current = window.setTimeout(() => {
setActivity((prev) => [makeTransferRow(mode, cents, dest), ...prev]);
const row = makeTransferRow(mode, cents, dest);
setActivity((prev) => [{ ...row, id: pendingId, detail: `${row.detail} · Pending` }, ...prev]);
}, SHEET_INSERT_DELAY_MS);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Settlement races pending-row insertion

When an execute error or terminal webhook arrives within the 700 ms insertion delay, settleTransfer clears the ref and filters activity before this row exists, but this uncancelled timer subsequently inserts it. The failed or completed transfer is then shown permanently as Pending because its synthetic ID cannot be reconciled with a Grid transaction.

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/grid-wallet-prod/src/apps/shared/wallet/useWalletHome.ts
Line: 526-530

Comment:
**Settlement races pending-row insertion**

When an execute error or terminal webhook arrives within the 700 ms insertion delay, `settleTransfer` clears the ref and filters activity before this row exists, but this uncancelled timer subsequently inserts it. The failed or completed transfer is then shown permanently as Pending because its synthetic ID cannot be reconciled with a Grid transaction.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@pengying
pengying force-pushed the 07-25-feat_grid-wallet-prod_payouts_reach_every_corridor_only_funding_is_rail-limited branch from 026bd43 to 742d8cc Compare July 27, 2026 21:17
@pengying
pengying force-pushed the 07-25-fix_grid-wallet-prod_activity_list_reflects_the_real_ledger_not_the_ui branch from f8ed841 to 6f43c97 Compare July 27, 2026 21:17
@pengying
pengying force-pushed the 07-25-feat_grid-wallet-prod_payouts_reach_every_corridor_only_funding_is_rail-limited branch from 742d8cc to 8eb60b9 Compare July 27, 2026 21:41
@pengying
pengying force-pushed the 07-25-fix_grid-wallet-prod_activity_list_reflects_the_real_ledger_not_the_ui branch from 6f43c97 to 89b2018 Compare July 27, 2026 21:41
…e UI

Money moved but the activity list didn't. Three causes, all real:

- Inbound rows only ever came from the arrival webhook, which never reaches
  localhost without a tunnel. Every completion site now calls refreshLedger
  (balance AND GET /transactions together), so rows come from Grid.
- Grid records an inbound pull as a DEBIT (it debits the external source), so
  keying the row off `direction` labelled arriving money "Money sent".
  transactionToRow now derives direction from which side the wallet is on and
  falls back to direction/type when the wallet id isn't known.
- Funding arrives in two legs (fiat credit, then conversion), so fetchActivity
  lists only the legs that touch the wallet — the leg the balance moved on.

The push stand-in produced no row at all, because it funded the PLATFORM's USD
account and on-ramped from it: that lands spendable balance, but the transaction
belongs to the platform and never appears in GET /transactions?customerId=...
(measured: 0 of 20 rows). It's now a REALTIME_FUNDING quote settled with
POST /sandbox/send — the same settlement a pushed wire gets, and the customer's
own transaction. Verified in the sandbox: COMPLETED, wallet as destination,
spendable balance. That drops the platform on-ramp helpers entirely, and with
them two endpoints from the proxy allowlist (the proxy injects the platform's
own credentials, so a narrower allowlist is a real reduction in reach).

Also in this pass:
- Outbound is ledger-driven too: a pending row goes in on confirm and settles
  from the transaction's terminal status, so a failed quote no longer leaves a
  phantom "Withdrawn from balance" row.
- Webhook cards show a static Payload label instead of a Response tab (there is
  no response to show), vertically centred in the tab group.
- Panel filter chips for Requests vs Webhooks; action cards are never filtered.
- After USD arrives, quote+execute USD internal -> USDB automatically.
- External accounts dedupe on routing+account number (or IBAN). Collapses
  nothing today — the demo randomizes digits per add — but stops true repeats.
- accountLabel() shows handles whole and masks only numbers (no more "•••• @upi").
- detectAddressNetworks() + tests: groundwork for pasting from the real
  clipboard outside sandbox. Not wired to the UI yet.
@pengying
pengying force-pushed the 07-25-feat_grid-wallet-prod_payouts_reach_every_corridor_only_funding_is_rail-limited branch from 8eb60b9 to b895952 Compare July 27, 2026 21:57
@pengying
pengying force-pushed the 07-25-fix_grid-wallet-prod_activity_list_reflects_the_real_ledger_not_the_ui branch from 89b2018 to c246e5c Compare July 27, 2026 21:57

pengying commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Merge activity

@pengying
pengying changed the base branch from 07-25-feat_grid-wallet-prod_payouts_reach_every_corridor_only_funding_is_rail-limited to graphite-base/745 July 27, 2026 22:01
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.

2 participants