Conversation
Adds a deposit route where the user pays a BOLT11 invoice with Cash App, Strike or any Lightning wallet, and Orchestra swaps and delivers USDC to their Safe on Base. Entered from "Deposit with cash", it runs as four steps in the Add-funds modal: amount, invoice, status, error. Implements https://docs.flashnet.xyz/orchestra/onramp: - POST /v1/orchestration/onramp for the order and its invoice, with a per-call X-Idempotency-Key. - GET /v1/orchestration/estimate for live pricing while the user types, and /limits for the fiat band the form validates against — both fetched rather than hardcoded, because the band is operator-tuned. - GET /v2/orchestration/routes for the destination's decimals. Every amount on the wire is in smallest units, and two assets sharing a ticker on different chains do not share an exponent. - SSE at /v1/sse/operations/:id for user-facing progress, falling back to the documented 3-second poll. EventSource is a browser API and React Native has none, so native builds poll; the poll also backs up the stream on web, since reconnects do not replay missed transitions and frames carry only the status. The flow authenticates with the *client* key (fnp_), whose scopes — orders:onramp, orders:read, orders:sse — are exactly these calls, plus the order-bound read token /onramp returns. The app is a static export with no server of its own, so there is no proxy to hide a server key behind and none is used: ORCHESTRA_SERVER_KEY is declared in .env.example without an EXPO_PUBLIC_ prefix, so Expo cannot inline it, and is left for the backend that owns webhooks and history. Order state that is not delivered is handled rather than flattened into a spinner: `unfulfilled` says a late deposit can still settle, `refunding` says the money is coming back instead of showing a delivery ladder, and an expired read token says we can no longer track the deposit — not that it failed. Env vars added: EXPO_PUBLIC_ORCHESTRA_CLIENT_KEY (public), ORCHESTRA_SERVER_KEY (secret, backend only), EXPO_PUBLIC_ORCHESTRA_API_BASE_URL, and the destination chain/asset. The entry row is hidden until the client key is set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Probing the live API showed GET /v1/orchestration/estimate takes `amount` in sats and has no fiat parameter — `amountFiatUsd`, `fiatUsd`, `usdAmount` and `amountUsd` all answer 400 invalid_request, and there is no v2. The amount screen types in dollars, so it was sending a query the endpoint never accepts: every keystroke would have 400'd. There is no spot endpoint to convert with either, and inventing a rate from a price feed the app would have to add means quoting against a number Orchestra did not agree to. POST /onramp is the first call that takes `amountFiatUsd`, and it answers with the real sats, fee and delivery at Orchestra's own spot — so the invoice screen becomes the review step. It already showed the amounts; it now shows the fee too, and nothing is charged until the invoice is paid, so back is a real way out. Also from the live response: the fee is `totalFeeAmount`, not `feeAmount` — the latter omits the rounding component the user still pays — and `feeAssetDetails.decimals` states the fee asset's own exponent, which replaces the ticker-sniffing heuristic that would have printed a sats fee as "0". Removes getOrchestraEstimate, useOrchestraEstimate and the debounce and pending-state machine they needed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The app no longer holds an Orchestra key or talks to Orchestra directly. Every call goes to /accounts/v1/orchestra on the accounts service, which holds the server key. This is not only about where a secret lives. With a client key on the device, `recipientAddress` was a field in a body the user controls, and reading an order needed only its id plus a token the same response handed back. Both are now the server's to decide: the recipient is the Safe on the authenticated user's record, and a status read is checked against that address, so an order id is not enough to read someone else's deposit. - lib/api/orchestra.ts calls our backend, JWT on native and cookies on web, wrapped in withRefreshToken like every other authenticated call. - /limits and /v2/routes collapse into one `config` call: the amount screen needs the band, the decimals and the symbol together, and three round trips to render one form is three chances to half-render it. - The SSE stream is our proxy. EventSource cannot set headers but can send cookies, which is how web sessions authenticate here anyway; native has no EventSource and keeps the 3-second poll. - The entry row is gated on config succeeding rather than on a bundled flag — the backend 503s when it has no key, so availability is its answer, not a constant that can drift out of step with it. Drops EXPO_PUBLIC_ORCHESTRA_* entirely, along with fiatLimitsFromResponse and its tests: picking the fiat band out of the routes list is the backend's job now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The row was gated on the backend's config call succeeding, so a backend without an Orchestra key — or an unreachable one — made the deposit option disappear entirely. That is indistinguishable from a feature that was never built: there is nothing to tap, no error, and no way to tell the two apart from the app. Show the row unconditionally and let the amount screen state the actual reason. A 503 from a keyless server now reads "The server has no Orchestra key" on the error screen instead of silence. Also aligns the client's error codes with the ones the backend actually sends. ORCHESTRA_NOT_CONFIGURED, ORCHESTRA_NO_WALLET_ADDRESS and ORCHESTRA_ORDER_NOT_YOURS had no entry in the copy table, so every one of them fell through to the generic "something went wrong" — the three failures most likely during setup were the three that explained themselves least. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
components/BuyCrypto/Orchestra sat inside the directory TransFi occupies, which reads as though Orchestra is a variant of it. It isn't — it is a separate provider on a different rail, sharing no code with TransFi. The only thing the two have in common is that I used TransFi as the reference for this codebase's conventions while writing it. components/Orchestra now, with BuyCrypto left to TransFi and the legacy Mercuryo iframe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
account_pending_approval had an action but no copy, so it fell through to the generic "try again in a moment" — advice that can only waste the reader's time, since Flashnet reviews new partner accounts before activating their keys and no amount of retrying changes that. The key is valid; it simply isn't live. Same for origin_not_allowed and scope_required, which are key configuration rather than anything a user can act on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The invoice screen threw "Cannot convert a BigInt value to a number" as soon as an order was created. formatSats and formatSmallestUnits both passed a BigInt to Intl.NumberFormat.format. The spec allows that, but Hermes and the Intl shim under React Native Web coerce the argument with ToNumber first, which throws. Node's Intl accepts a BigInt, so the existing tests passed while the app was broken — the one runtime the tests don't use was the only one that mattered. Groups the digit string directly instead, taking the locale's group and decimal separators from Intl once via a plain Number. That keeps the exactness BigInt was chosen for, and also fixes the decimal point, which was hardcoded to "." regardless of locale. formatSats needs no BigInt at all — it is already a digit string. Adds a test that patches Intl to reject BigInt the way the app's runtime does, so this cannot pass in CI while failing on device again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two changes to how the cash deposit flow is laid out. USD is now a chooser rather than a shortcut. Selecting it opens "Deposit US Dollars" with two rails — "Wire transfer, ACH" (the virtual account's own details) and "Cash App" (the Lightning onramp). The standalone Cash App row is gone from the currency list: it was never a currency, and sitting beside USD, EUR and BRL implied it was one. Outside the US, USD still opens the virtual account directly. A chooser with one option is a tap that asks a question with one answer. And the Lightning rail is US-only. That is not our restriction to relax: Cash App's own Lightning send and receive is US-only, and Orchestra additionally excludes New York City, so the row would otherwise produce an invoice the user has no way to pay. The gate is applied twice — on the row, and again on the amount screen, which is reachable by other routes (a restored modal step, a user whose IP moved between sessions). An unresolved country counts as unavailable rather than as permission: a US-only rail is not something to offer on the strength of not knowing where someone is. Extracts useVirtualAccountEntry, since the bank rail is now reached from two screens and duplicating its Wirex/Rain routing would have been the second copy to drift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The chooser sits between picking USD on the cash screen and picking a rail, so without a view event the two read as one step and the drop-off between them is invisible — including how many people reach it and take neither rail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Lightning is the rail, not the thing the user recognises. Every visible
string now names Cash App: the step titles ("Deposit with Cash App",
"Pay with Cash App"), the row subtitle, the QR caption, the status line,
and the invoice label, which reads "Payment request" rather than naming a
protocol nobody asked about.
Dropped "Strike, or any Lightning wallet" rather than restating it — the
invoice is still a BOLT11 and other wallets still pay it, but leading
with the rail made a one-tap Cash App handoff read like something
technical.
The Cash App row's chips lose "Lightning" too: the row is already titled
Cash App, so the chip says how fast, not how.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The backend now reports `isAvailable` on /orchestra/config — its verdict on the launch flag and the username allowlist. The row is hidden when it says no, and the amount screen sends anyone who reaches it anyway to the error screen rather than letting them mint an invoice the server will refuse. Two independent gates, and both must pass. Region is resolved on the device because only the client can see the user's IP; audience is the server's, because hiding a row is not enforcing anything. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The client was ANDing its own region check with the server's verdict, which is the wrong shape now that the rule is "US **or** allowlisted" — an allowlisted tester abroad was hidden from the feature by the half of the rule that was supposed to be overridden. The client now resolves the country only to pass it along, and gates on config.isAvailable alone. One rule, evaluated once, on the side that also enforces it. Drops ORCHESTRA_REGION_UNSUPPORTED: the region verdict is no longer the client's to reach on its own, so the code had no way to fire. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
| const setModal = useOrchestraNavigation(); | ||
| const order = useOrchestraStore(state => state.order); | ||
| const amountUsd = useOrchestraStore(state => state.amountUsd); | ||
| const { data: config } = useOrchestraConfig(); |
There was a problem hiding this comment.
Bug: The useOrchestraConfig hook is called without a countryCode in OrchestraInvoice.tsx, causing a React Query cache miss and a redundant API call after navigating from the Amount screen.
Severity: HIGH
Suggested Fix
The countryCode is available in both the Invoice and Status screens via the useCashAppDepositAvailability hook. Pass the countryCode to the useOrchestraConfig hook in OrchestraInvoice.tsx and OrchestraOrderStatus.tsx to ensure the cache key matches the one set by OrchestraAmount.tsx. This will prevent the cache miss and the redundant API call.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: components/Orchestra/OrchestraInvoice.tsx#L60
Potential issue: A cache key mismatch in React Query causes a temporary data
availability issue. The `OrchestraAmount.tsx` component calls
`useOrchestraConfig(countryCode)`, caching the result under a key like
`['orchestraConfig', 'US']`. However, `OrchestraInvoice.tsx` and
`OrchestraOrderStatus.tsx` call `useOrchestraConfig()` without the `countryCode`,
resulting in a different cache key `['orchestraConfig', undefined]` and a cache miss.
This triggers a new, redundant API call. While this call is in-flight, `config` is
`undefined`, causing `formatSmallestUnits` to return `undefined`. Consequently, the user
sees "You receive: Not available" on the invoice review screen until the new network
request completes.
Also affects:
components/Orchestra/OrchestraOrderStatus.tsx:61
Did we get this right? 👍 / 👎 to inform future reviews.
Four conflicts, three of them parallel additions and one a rename. constants/modals.ts — qa added OPEN_ONRAMPER_WIDGET at number 28, which the Orchestra steps had also claimed; they move to 29-32. qa also renamed OPEN_DEPOSIT_CHAIN to OPEN_DEPOSIT_TOKEN, so its doc comment is taken from qa rather than merged. hooks/useDepositOption.tsx — six hunks, all "both sides added a branch to the same chain", kept both. The getContent hunk needed care: each side's final `if` was left open inside the conflict and shared the brace after it. isDepositChain no longer exists, so those references are dropped in favour of qa's isDepositToken. components/DepositOption/DepositCashOptions.tsx — only the imports clashed; the body merged. Neither side's contested imports survive it: qa dropped the live TransFi payment-method fetch for getCardFundLocalPaymentMethods, and the Rain/virtual-account plumbing had already moved into useVirtualAccountEntry. The 2 failures in components/Rewards/NewRewards/__tests__/skipTheLine predate this merge — they fail identically on a clean origin/qa worktree with none of this branch's code present. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. |
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.