fix(grid-wallet-prod): activity list reflects the real ledger, not the UI - #745
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
Greptile SummaryThis PR makes wallet activity ledger-driven and expands supporting demo behavior.
Confidence Score: 2/5This 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
|
| 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
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
| if (event.type?.startsWith('OUTGOING_PAYMENT.') && data) { | ||
| if (data.status === 'COMPLETED') reportTransfer(true); | ||
| else if (['FAILED', 'REJECTED', 'REFUNDED', 'EXPIRED'].includes(data.status)) { | ||
| reportTransfer(false); | ||
| } |
There was a problem hiding this 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.
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.| 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); |
There was a problem hiding this 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.
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.026bd43 to
742d8cc
Compare
f8ed841 to
6f43c97
Compare
742d8cc to
8eb60b9
Compare
6f43c97 to
89b2018
Compare
…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.
8eb60b9 to
b895952
Compare
89b2018 to
c246e5c
Compare
Merge activity
|

Money moved but the activity list didn't. Three causes, all real:
refreshLedger(balance andGET /transactionstogether), so rows come from Grid rather than from the UI.DEBIT(it debits the external source), so keying the row offdirectionlabelled arriving money "Money sent".transactionToRownow derives direction from which side the wallet is on, falling back todirection/typewhen the wallet id isn't known.fetchActivitylists 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 aREALTIME_FUNDINGquote settled withPOST /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-accountsandPOST /sandbox/internal-accounts/{id}/fundfrom 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
Payloadlabel instead of a Response tab (there is no response to show), vertically centred in the tab group.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/send→COMPLETED, listed with the wallet as destination, spendable (not book-only) balance.Money added · Added to balance · Just now · +$25.00.+$100.00inbound now reads as added, not sent.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.tsc --noEmitclean.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