feat: TASK-21816 withdraw on a URL-backed stepper + TASK-21454 Field - #2917
Conversation
Named screen ids in the URL via nuqs (never indexes), entry guards with fallbacks for refresh/deep-link into a step whose prerequisites are gone, back owned by the stepper (backMap for non-linear flows, onExit on the first step). TASK-21816 / TASK-21665.
Board 17802:61539 anatomy; error is FieldError text only and replaces the helper — never an input border (DS call). react-hook-form is the expected state owner. Showcase at /dev/ds/primitives/field. TASK-21454.
…/withdraw - WithdrawFlowProvider mounts at the /withdraw layout, not app-global. Fresh entry IS the reset: the hand-written resetWithdrawFlow() compensation in home and send is gone (TASK-21203 / TASK-20806). - Root page: method → amount as ?step= named ids; the amount travels as ?amount= to every downstream route (TASK-21665); showAllWithdrawMethods becomes ?showAll= (kills the twin racing effects, TASK-21198). - features/ pattern: WithdrawRoot → useWithdrawRootFlow → dumb views; bank page → useBridgeOfframpFlow + WithdrawBankReviewView with ?step=review|success; crypto page steps recipient|review|success. - AddWithdrawRouterView deleted (only consumer was the withdraw page; its add branches were unreachable — add-money renders AddWithdrawCountriesList). - DynamicBankAccountForm decoupled from the withdraw context and the redux bankForm slice; fields render through the new Field component. - Amount-error gating: the banner yields to the limits card only when the card renders — crypto always shows a reason (TASK-21666). TASK-21816
…honest search placeholder - Manteca flow moves onto the shared URL stepper (amount | bank-details | review | success | failure) and honors ?amount= from the shared amount step — one amount entry, honored downstream (TASK-21664). Back from bank-details returns to the root amount step when the amount was seeded. - Withdraw network icons: the chain registry's curated raster logos win over chain-details.json SVG URLs, which next/image refuses without dangerouslyAllowSVG — ETH/OP/BNB rendered as initials (TASK-21667). - Token search placeholder stops promising address paste in all four locales — the field never supported it (TASK-21199). TASK-21816
- withdraw-states runs the REAL stepper + flow hook against the nuqs
testing adapter, so the URL contract itself is asserted: guard fallback
on ?step=amount without flow memory, ?amount= pre-fill and forwarding,
send-marker survival, TASK-21666 crypto error visibility.
- crypto-withdraw-confirm keeps its double-spend regression net on the
goTo('success') transition instead of setCurrentView.
- send/home drop the resetWithdrawFlow expectations — the compensation
they pinned is gone with the app-global context.
- WithdrawFlowContext test moves with the module to features/withdraw.
- deflake: the stepper's guard-redirect URL assertion waits out nuqs's
write throttle.
TASK-21816
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Code-analysis diffPainscore total: 7127.63 → 7182.98 (+55.35) 🆕 New findings (151)
…and 131 more. ✅ Resolved (139)
…and 119 more. 📈 Painscore deltas (top movers)
|
…eld bank form Fixture routes may now carry their own query string (deep-linked flow steps — the URL stepper's whole point); the shots runner and the route-exists check split on '?'. TASK-21816 / TASK-21454
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
There was a problem hiding this comment.
Chip review — changes requested
Request changes: backing out of the ordinary crypto recipient screen no longer returns to the amount step because the unmount reset clears that step's guard prerequisite.
Findings
- MAJOR · src/app/(mobile-ui)/withdraw/crypto/page.tsx:66 · Crypto back navigation loses the selected method
Choose Crypto from/withdraw, enter an amount, continue to the recipient screen, then press Back. This target asks to restore?step=amount, but unmounting the crypto page callsresetWithdrawFlow(), which clearsselectedMethod; the root amount guard therefore rejects the target and rewrites the flow to method selection. Preserve or re-seed the crypto method during this intra-/withdrawtransition (while still clearing transient charge/payment state), and cover Crypto → Back → amount with an integrated regression test.
Checked clean
- Confirmed the detached worktree head, exact base SHA, and merge base match the supplied values.
- Read the trusted PR title and description without fetching issue or review comments.
- Reviewed the URL stepper, root method/amount guards, route hand-offs, and send-origin markers.
- Reviewed Bridge bank submission and double-send protection around post-deposit confirmation failures.
- Reviewed crypto route preparation, affordability/minimum gates, execution retry behavior, and success transition.
- Reviewed Manteca URL amount seeding, price-lock navigation, failure reset, and withdrawal submission path.
- Reviewed scoped withdraw-context consumers, native query-route dispatch, bank-account form changes, and Field integration.
- Exact-head CI review, analysis, and policy checks passed; Deploy-Preview was still in progress when checked.
Second opinion: did not run — the model did not answer in time. This review is one reviewer short.
Third opinion by claude-opus: 0 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.
Exact head: 8567bc6eb6c9 · Context: repo · Took 23m
The crypto page's unmount cleanup called resetWithdrawFlow(), clearing selectedMethod on the intra-/withdraw back transition — the root amount guard then bounced ?step=amount to method selection. The cleanup now clears only crypto-transient state (charge, route, recipient, modals); the selection survives, and leaving /withdraw still resets everything by unmounting the provider. Regression test pins the cleanup contract. TASK-21816
There was a problem hiding this comment.
Chip review — changes requested
Two previously raised major defects remain: URL-controlled success screens still lack execution proof, and the bank-offramp handler still trusts an unvalidated URL amount. The crypto back-navigation defect (P1) is fixed at this head.
Findings
-
MAJOR · src/features/withdraw/useBridgeOfframpFlow.ts:82 · Require proof before rendering URL success steps
With a bank account and amount still in the /withdraw-scoped context, changingsteptosuccessrenders PaymentSuccessView even though createOfframp, the wallet transfer, and confirmOfframp never ran, because this stepper has no success guard. Crypto has the same issue after recipient setup because its guard only requires pre-execution charge data, and Manteca only requires usdAmount. Gate each success step on flow-local proof set only after its money operation succeeds, and add URL-tampering regressions for all three flows. -
MAJOR · src/features/withdraw/useBridgeOfframpFlow.ts:370 · Revalidate the URL amount before creating a bank offramp
amountToWithdraw is a raw URL string. This branch explicitly clears the blocking error for0and malformed values, while positive sub-$1 values also pass the balance-only check; proceedWithOfframp then forwards that string to createOfframp without a synchronous finite, positive, rail-minimum, or current-balance check. The API contract also accepts any string and calls the provider with it. Validate and normalize the latest amount inside proceedWithOfframp before analytics or createOfframp, then cover zero, malformed, below-minimum, and over-balance query values.
Checked clean
- P1 rechecked: crypto recipient back navigation now preserves selectedMethod and returns to /withdraw?step=amount; the exact head adds regression coverage for the cleanup contract.
- Reviewed URL stepper guards for root, crypto, Bridge bank, and Manteca, including direct success-step entry and refresh behavior.
- Reviewed the bank-offramp create, wallet-send, confirm, and retry ordering, including the submitted-transaction double-send guard.
- Reviewed the sibling peanut-api-ts /bridge/offramp/create contract: amount is only typed as a string and is forwarded to the provider without numeric or minimum validation.
- Reviewed the Field extraction, DynamicBankAccountForm integration, withdraw-provider scoping, and route builders; found no additional actionable defects.
- Exact-head CI succeeded for the aggregate ci-success gate, unit, typecheck, format, eslint, analyze, deployment, provenance, and bot-policy checks; ds-shots was still in progress at the final check.
- A redundant local targeted Jest run was unavailable because the detached worktree has no node_modules; exact-head unit CI succeeded.
Second opinion by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only, so treat its findings as advice.
Third opinion: did not run — claude-failed(1): Warning: no stdin data received in 3s, proceeding without it. If piping from a slow command, redirect stdin explicitly: < /dev/null to skip, or wait longer.. This review is one reviewer short.
Exact head: d42df7a4caf8 · Context: repo, peanut-api-ts · Took 16m
…withdraw-url-stepper
…t (Chip review) - step-guards.ts: every ?step=success (and manteca's failure) now demands flow-local proof set only after the money operation — bank: confirm succeeded (completedTxHash); crypto: broadcast transaction identifier; manteca: submission outcome. A hand-edited URL or a refresh without proof falls back to a working step. Tampering regressions per flow. - amount-validation.ts: the bank submit handler revalidates the user-editable ?amount= synchronously — finite, positive, ≥ the $1 Bridge floor, within the displayed balance — and the normalized decimal string is what goes on the wire. TASK-21816
|
/chip review |
There was a problem hiding this comment.
Chip review — changes requested
The prior crypto-navigation and terminal-step findings are fixed, but two URL-amount paths can still bypass required money-flow validation.
Findings
-
MAJOR · src/features/withdraw/amount-validation.ts:29 · Wait for the balance before creating a bank offramp
On a fresh review render,spendableBalanceis legitimately undefined. This branch then skips the ceiling check and returnsok; the review's balance effect also clears its error while the balance is undefined, so an edited?amount=above the user's funds can reachcreateOfframpbeforesendMoneyrejects it. That creates an avoidable provider transfer for an amount the wallet cannot fund. Treat an undefined balance as not ready, keep submit disabled until it loads, and only then validate the amount. This is the remaining reachable case from P3. -
MAJOR · src/app/(mobile-ui)/withdraw/manteca/page.tsx:582 · Keep URL-seeded Manteca amounts behind the limits gate
?amount=is editable, but this effect seeds it and immediately skips the amount screen, which is the only place that blocks onbalanceErrorMessageandlimitsValidation.isBlocking. For example, a user with the default $2,000 monthly LATAM allowance can open this route withamount=2500, enter valid bank details, lock a price, and reachhandleWithdraw; neither the bank-details nor review submit path rechecks the limit, minimum, or loaded balance before signing/submitting. Validate the seeded amount before advancing and recheck the async limits and balance immediately before the money operation. -
MAJOR · src/app/(mobile-ui)/withdraw/manteca/page.tsx:572 · [claude-opus] Manteca ?amount= seeding sets the withdrawal amount with no test
src/app/(mobile-ui)/withdraw/manteca/page.tsx:572-583adds a new effect that reads the user-editable?amount=param, seeds BOTH denominations (setUsdAmount(usd.toFixed(2))andsetCurrencyAmount((usd * currencyPrice.sell).toFixed(2))), latchesseededFromUrlRef, and auto-advances past the flow's own amount screen straight tobank-details→ price lock →handleWithdraw. That is the amount that gets price-locked and withdrawn, and there is no test for it anywhere: there is no test file for the Manteca withdraw page (onlysrc/services/__tests__/manteca-withdraw-transport.test.tsandsrc/constants/__tests__/manteca.consts.test.tsexist), andwithdraw-states.test.tsxonly asserts the sending side — that Continue pushes a URLstringContaining('amount=50'). Nothing asserts what the receiving page does with it.
The PR gave the equivalent hand-offs on the other two rails real coverage in this same diff (amount-validation.test.ts for the Bridge submit revalidation, step-guards.test.ts for the terminal-step proof, crypto-withdraw-confirm.test.tsx for the crypto stepper), so Manteca is the odd one out. Three concrete cases are untested:
?amount=50with a loadedcurrencyPrice.sellseedsusdAmount='50.00'andcurrencyAmount = 50 * sell, and lands onbank-details(the USD→local conversion is the number the user is actually paid; it is only correct because it matches theprice: currencyPrice.selldirection used byAmountInputat line 812 — nothing pins that).- Back from
bank-detailswhenseededFromUrlRef.currentis true must callonBack()(return to the root amount step) rather thanstepper.goTo('amount'). - After a terminal failure, "Try again" runs
resetState()(which clearsseededFromUrlRef) +stepper.reset()and must re-seed from the URL rather than dead-end on an empty amount screen.
Fix: add src/app/(mobile-ui)/withdraw/manteca/__tests__/ covering those three, mocking currencyPrice and rendering under NuqsTestingAdapter with { amount, country, method } — the same harness withdraw-states.test.tsx already uses.
Checked clean
- Verified the detached worktree HEAD, exact base SHA, merge base, trusted author, base ref, PR title, and description.
- Prior P1 is fixed: crypto unmount clears transient state without clearing the selected method, so intra-withdraw back navigation can re-enter the amount step.
- Prior P2 is fixed: bank, crypto, and Manteca terminal URL steps now require flow-local execution proof.
- Reviewed root method/amount routing, send-origin markers, Bridge create-send-confirm ordering, crypto broadcast/record retry protection, Manteca signing/submission, scoped withdraw context, bank-account form extraction, and Field composition.
- Exact-head CI reports success for unit, typecheck, eslint, format, build/deploy, design-system, provenance, and binding checks.
- Targeted Jest could not run locally because the supplied read-only worktree has no node_modules; no dependencies were installed.
- Security and slop passes found no additional actionable secret exposure, injection, privilege, dead-code, or architecture finding in the changed behavior.
Second opinion: did not run — the model did not answer in time. This review is one reviewer short.
Third opinion by claude-opus: 1 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.
Exact head: d22eeb150cc0 · Context: repo, product, ci · Took 26m
An unloaded balance is no longer headroom: validateBankOfframpAmount refuses with balanceLoading and the bank review submit stays disabled until the spendable balance is real, so an edited ?amount= can't reach createOfframp before the ceiling check exists. The Manteca ?amount= seed moves into useMantecaAmountSeed and only advances past the amount screen once the limits gate and a synchronous balance/minimum validator pass for the seeded amount — the effect-set balanceErrorMessage lags a render, so the gate asks the live balance instead. Price-lock and handleWithdraw re-check balance + async limits right before the money operations as the last line of defense. The seeding, gating, back-to-root, and Try-again re-arm behavior is unit tested (useMantecaAmountSeed.test.tsx).
|
/chip review |
There was a problem hiding this comment.
Chip review — changes requested
The six prior findings are fixed, but the new URL-backed crypto amount is not validated before same-chain request and charge creation.
Findings
-
MAJOR · src/app/(mobile-ui)/withdraw/crypto/page.tsx:93 · Validate crypto URL amounts before creating records
useWithdrawAmount()accepts arbitrary URL text, so/withdraw/crypto?amount=0passes the later nonempty check. For same-chain USDC,handleSetupReviewskips the Rhino minimum check and persists a request and charge with amount0; malformed values such asabclikewise become persistent request state before signing fails. The current API schemas accept these amount fields as unrestricted strings. Validate and normalize a positive plain-decimal amount within the loaded balance beforerequestsApi.create, recheck it before broadcast, and cover direct-route zero and malformed cases. -
MAJOR · src/features/withdraw/useBridgeOfframpFlow.ts:382 · [claude-opus] Bank offramp submit path has no test, and its new useCallback freezes the handler at first render
useBridgeOfframpFlowis the money leg of this PR (createOfframp → sendMoney → confirmOfframp) and nothing exercises it: grepping the repo,useBridgeOfframpFlow/createOfframp/confirmOfframpappear only in the hook, the page, the view andstep-guards.test.ts. The new unit tests cover the pure helpers (validateBankOfframpAmount,bankStepGuards) but nothing covers the submit handler that calls them — which is where the extraction changed behaviour.
Concretely, the old page had const handleCreateAndInitiateOfframp = () => {...} (fresh closure every render). The new hook wraps it in useCallback(..., [advisory, advisoryIntercept, trackUpliftStarted]) with an eslint-disable (useBridgeOfframpFlow.ts:382-387). All three deps are stable for the page's lifetime in the normal case: advisory is gate.kind === 'ready' ? gate.advisory : undefined, i.e. undefined both while the gate is loading/accept-tos and once it is ready-with-no-advisory; advisoryIntercept is useCallback(..., [advisory]) (useAdvisoryPreempt.ts:40-53); trackUpliftStarted is useCallback(..., [channel]) (useEeaUpliftFunnel.ts:48). So the memo never recomputes and every click runs the FIRST render's proceedWithOfframp, closing over the first render's gate, balance, amountToWithdraw and user.
Failure scenario: navigate to /withdraw/[country]/bank while useCapabilities is still resolving. gate.kind === 'loading' is captured. Capabilities land, the gate becomes ready, isBalanceReady (computed live, line 419) enables the Withdraw button — but the click runs the frozen closure, hits if (gate.kind === 'loading') return (line 207) and silently no-ops. Dead button until the page is remounted. Same shape for the ToS path: the gate is captured as accept-tos, so after the user accepts, onComplete → handleCreateAndInitiateOfframp re-enters the stale closure and re-opens the ToS modal instead of running the offramp; and for a balance that resolves after mount, where the stale balance === undefined makes validateBankOfframpAmount return balanceLoading and the handler no-ops (line 236).
Fix: drop the memo (restore the plain function — nothing requires a stable identity here) or include proceedWithOfframp via a ref, and add the missing test for the case named above: render the bank review flow with the gate/balance loading on first render, let them resolve, click Withdraw, and assert createOfframp is called with the normalized amount (and that a tampered ?amount= above balance or below $1 never reaches createOfframp/sendMoney).
- MINOR · src/features/withdraw/amount-validation.ts:8 · [claude-opus] $1 bank-withdrawal minimum contradicts product/networks.md, which says fiat withdrawal minimums are none
This PR codifies a user-facing bank-withdrawal floor:BRIDGE_OFFRAMP_MIN_USD = 1(amount-validation.ts:8) enforced in the submit path, surfaced ast('errors.minimumWithdrawal', { amount: '$1' })→ "Minimum withdrawal is $1." (useBridgeOfframpFlow.ts:241).
/home/chip/mono/product/networks.md documents the opposite for every Bridge withdrawal rail: sepa.withdrawal.minimum: none, ach.withdrawal.minimum: none, spei.withdrawal.minimum: none, faster-payments.withdrawal.minimum: none (lines 64, 82, 101, 121). The app in fact enforces $1 by default, £3 for GB and 50 MXN for MX (getMinimumAmount, src/utils/bridge.utils.ts:192-204), which the amount step converts to USD and blocks on — and networks.md already cites that same function for the MX deposit minimum, so the withdrawal side of the doc is simply stale.
The code is right and the doc is wrong: Bridge does have per-rail minimums (apidocs.bridge.xyz/docs/transaction-costs, cited in amount-validation.ts). Impact is support-facing — the doc feeds support-answers/withdrawals-to-bank.md, so an agent answering "is there a minimum to withdraw to my bank?" will say "no minimum" while a UK user withdrawing £2 is blocked. Fix: update the four withdrawal.minimum entries in product/networks.md to the enforced values ($1 default, GB £3, MX 50 MXN) with the getMinimumAmount cross-reference. No code change needed. Note the enforcement itself pre-dates this PR at the amount step; this PR is what puts the floor and its copy on the submit path.
Checked clean
- Prior P1 is fixed: crypto unmount clears transient state without clearing the selected method, preserving back navigation to the root amount step.
- Prior P2 is fixed: bank, crypto, and Manteca terminal URL steps require flow-local execution proof, with URL-tampering tests.
- Prior P3 and P4 are fixed: Bridge submission synchronously validates and normalizes the URL amount, rejects an unloaded balance, and keeps submit disabled until balance is ready.
- Prior P5 and P6 are fixed: the Manteca URL seed waits for live minimum, balance, and limits gates; hook tests cover conversion, blocked gates, back navigation, and retry re-arming.
- The shared URL stepper guard, fallback, back-map, reset, and URL rewrite behavior was reviewed with its test coverage.
- Withdraw provider scoping and the bank-form/claim-flow split were checked for consumers outside the withdraw layout.
- The sibling API request and charge schemas were checked and do not constrain amount strings, confirming the crypto persistence path.
- All checks at the exact head completed successfully, including unit, typecheck, eslint, format, design-system lint/screenshots, analysis, backend baseline, and preview deployment.
Second opinion: did not run — the model did not answer in time. This review is one reviewer short.
Third opinion by claude-opus: 2 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.
Exact head: 55a7d4db2a59 · Context: repo, sibling-api · Took 25m
…ubmit handler (Chip round 4) The crypto withdraw page now validates and normalizes the user-editable ?amount= before any request/charge row is persisted (same-chain USDC had no floor, so 0 and malformed values sailed past the Rhino-only minimum and persisted records that could never sign). The USD amount the records were created for is pinned to the charge id, and the confirm leg broadcasts the pinned amount — re-validated against the live balance — so a URL edit between review and confirm can no longer move a different amount on-chain than the records say. The route quote pins the same way. The bank submit handler is a plain function again: its useCallback deps were all lifetime-stable, so the memo froze the first render's proceedWithOfframp — a click after capabilities/balance resolved ran the stale gate-loading no-op forever (dead button until remount). New tests: useBridgeOfframpFlow.test.tsx runs the real hook under the nuqs adapter (create→send→confirm with the normalized amount, the frozen-closure regression — verified failing against the old memoized handler — and over-balance/below-minimum/malformed tamper cases); crypto-withdraw-confirm.test.tsx gains the setup-persistence and broadcast-pin cases.
|
/chip review |
|
/chip review |
There was a problem hiding this comment.
Chip review — changes requested
Request changes: Manteca URL seeding still permits an oversized exponential amount that crashes the flow during its live-balance check. All 15 supplied prior findings are fixed or no longer applicable at this head and the current product source.
Findings
-
MAJOR · src/features/withdraw/useMantecaAmountSeed.ts:60 · Reject exponential Manteca URL amounts before seeding
With a loaded FX rate and balance,/withdraw/manteca?country=argentina&method=bank-transfer&amount=1e21passes this finite/positiveparseFloatcheck. JavaScript then normalizes it to1e+21; the advance effect calls the page's live-balance validator, whose directparseUnitscall rejects scientific notation and throws, blanking the flow before a gate can render. Validate the raw query with the existing fail-closed USD parser (or otherwise catch/reject valuesparseUnitscannot consume) before seeding, and add this exponential/oversized URL case to the hook tests. -
MINOR · src/app/(mobile-ui)/withdraw/crypto/page.tsx:115 · [moonshotai/kimi-k3] Crypto success guard satisfied by a stale transaction hash
The success-step guard isexecuted: !!transactionHash(cryptoStepGuards block added in this file).transactionHashlives in the /withdraw-scoped context and is only cleared by the unmount cleanup. Within the same mounted flow, after one completed withdrawal, nothing inhandleSetupRevieworhandleBackFromConfirmclears it, so?step=successin the URL renders the old success screen even when a new attempt is mid-flight — the guard's execution proof is stale, not current. Fix: callsetTransactionHash(null)when a new review is set up (inhandleSetupReviewor when moving back to the recipient step), so the guard only admits execution proof from the current attempt. -
MINOR · src/app/(mobile-ui)/withdraw/crypto/page.tsx:735 · [moonshotai/kimi-k3] Crypto success screen displays the still-editable ?amount= instead of the broadcast amount
PaymentSuccessViewon the success step receivesamount={usdAmount}/usdAmount={usdAmount}, whereusdAmount = amountToWithdrawis the liveuseWithdrawAmount()URL value. The PR deliberately keeps?amount=user-editable and pins the broadcast viasetupAmountRef, but after execution the success view (and any receipt/share rendering from these props) displays whatever the URL says — user edits it to '999999' and the success screen shows that, a false-receipt/spoof vector (same for theWITHDRAW_COMPLETEDposthog payload read at confirm time). Fix: pin the displayed/completed amount — set anexecutedAmountUsdstate alongsidesetTransactionHash(finalTxHash)at confirm, or readsetupAmountRef.current.amountUsd— and pass that toPaymentSuccessViewand the analytics capture. -
MINOR · src/features/withdraw/views/WithdrawMethodView.tsx:137 · [claude-opus] New method-select view ships with no test while its predecessor's test file is deleted
WithdrawMethodView is the screen that mutates the withdraw flow's shared destination state — setSelectedBankAccount + setSelectedMethod — and routes saved Manteca accounts straight into the money flow: onAccountClick pushes mantecaWithdrawUrl({country, destination: account.identifier, isSavedAccount:'true'}), which makes /withdraw/manteca skip bank-details entry and submit to that identifier. The file has no test, and this same diff deletes src/components/AddWithdraw/tests/AddWithdrawRouterView.test.tsx (242 lines), the only file that covered these behaviours — including 'clicking Crypto sets the method in context and does NOT navigate', a regression test whose stated reason (a pre-amount push trips the crypto page's no-amount redirect guard, whose unmount cleanup resets the flow) still applies to the new handleMethodSelected. withdraw-states.test.tsx mocks WithdrawMethodView out entirely, so nothing exercises it. Untested cases to name: (a) a saved Manteca account forwards destination= and isSavedAccount=true; (b) a saved non-Manteca account sets selectedBankAccount and advances to the amount step without navigating; (c) the crypto row sets selectedMethod and performs no router.push. Fix: add a WithdrawMethodView test covering those three, mirroring what the deleted AddWithdrawRouterView test pinned. Severity is minor rather than major because the destination is shown on the review screen before any money moves, and the downstream submit gates are themselves tested.
Inline anchors unavailable for 2 finding(s); the findings remain in this summary.
Checked clean
- Exact detached HEAD, supplied base SHA, merge base, trusted author, PR number, and tech-debt base ref all match.
- Current CI at this SHA: all required typecheck, lint, formatting, unit, design-system, analysis, provenance, and aggregate checks completed successfully; skipped ping is non-gating.
- Prior P1-P8: crypto back navigation, terminal-step execution proofs, bank URL-amount validation/balance readiness, Manteca seed gates/tests, crypto record/broadcast pinning, and the fresh bank submit closure are fixed.
- Prior P9, P11, and P15: the live Lexicon has no rail-minimum definition; current mono main product/network and customer-facing help sources now state the enforced bank and per-network crypto minimums.
- Prior P10 and P13: real-page Manteca tests cover limits/loading/live-balance changes at both money boundaries, and both handlers synchronously recheck the current balance.
- Prior P12 and P14: the UK record uses iso2=GB for the GBP rate, and Bridge submit revalidates the converted destination rail minimum before createOfframp.
- URL step entry guards, scoped withdraw context, send-origin back paths, native bank route preservation, and success proof handling were checked for reachable dead screens or false success.
- Crypto request/charge setup, same-chain and cross-chain execution, live-balance revalidation, amount pinning, and record-only retry behavior were checked for mismatch and repeat-spend paths.
- Field/DynamicBankAccountForm integration and the claim-flow existing-account path were checked for validation, routing, and state regressions.
- Security/slop pass found no secret exposure, injection path, privilege change, unsafe external write, or additional actionable architecture drift in the changed surface.
Second opinion by moonshotai/kimi-k3: 2 finding(s), marked with the model name. It reads the diff only, so treat its findings as advice.
Third opinion by claude-opus: 1 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.
Exact head: d5a87081ab69 · Context: repo, product, ci · Took 20m
…o success amount (Chip round 7) ?amount=1e21 survived the seed's bare parseFloat check, normalized to '1e+21', and crashed the live-balance validator's parseUnits call before any gate could render. Parsing now goes through parseUsdAmount — the fail-closed plain-decimal parser the bank/crypto validators already used — in both seed effects, with exponential cases in the hook tests. The crypto success screen and the WITHDRAW_COMPLETED analytics read a new executedAmountUsd (set from the charge-pinned broadcast amount at completion) instead of the still-editable ?amount= — a post-execution URL edit could forge the displayed receipt. A new review setup clears the previous attempt's transactionHash, so the success-step guard only admits execution proof from the current attempt. New WithdrawMethodView test pins what the deleted AddWithdrawRouterView test covered: a saved Manteca account forwards destination + isSavedAccount into /withdraw/manteca, a saved bank account advances without navigating, and the crypto row sets the method with no router.push (the pre-amount-push redirect-guard regression).
|
/chip review |
There was a problem hiding this comment.
Chip review — changes requested
The prior findings are fixed at this head, including P15's live Manteca balance recheck, but the bank success screen can still display a URL-tampered amount instead of the amount that actually executed.
Findings
- MAJOR · src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:87 · Pin the bank success amount
After a bank withdrawal of $50 completes, editing the URL to?step=success&amount=5000leavescompletedTxHashset, so the success guard still admits the screen while this line renders a false $5,000 confirmation. Store the validatedamountUsdalongside the completion proof and render that pinned value on the success screen; add a regression that changes the URL amount after completion.
Checked clean
- Confirmed the detached worktree head and merge base match the supplied SHAs and read the trusted PR metadata.
- Rechecked P1-P14 as fixed or no longer applicable at this head; P15 is fixed by synchronous live-balance checks at both Manteca money boundaries and a balance-drop regression test.
- Reviewed URL step guards, root/provider routing, Bridge amount and rail-minimum validation, crypto setup/broadcast/success amount pinning, record-only retry behavior, Manteca URL seeding and submit gates, and the extracted bank form/Field wiring.
- CI at the exact head has successful unit, typecheck, format, lint, analysis, provenance, and aggregate report checks; ds-shots was still running when checked.
Second opinion by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only, so treat its findings as advice.
Third opinion by claude-opus: 0 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.
Exact head: 40cf01f181e2 · Context: repo · Took 16m
Editing ?amount= after a completed bank offramp left completedTxHash satisfying the success guard while the screen rendered the edited number — a forged confirmation. The hook now stores executedAmountUsd (the validated amount the offramp moved) alongside the completion proof and the success screen renders it; a regression drives the real hook, completes a withdrawal, edits the URL amount through the nuqs setter, and asserts the pinned amount survives.
|
/chip review |
There was a problem hiding this comment.
Chip review — no blocking findings — this is not an approval
No blocking defect remains at this head. All prior findings are fixed; one minor success-screen consistency issue remains in the points preview.
Findings
-
MINOR · src/features/withdraw/useBridgeOfframpFlow.ts:186 · Pin the bank success points to the executed amount
After a $50 bank withdrawal completes, editing?amount=5000leaves the execution proof and pinned currency amount intact, but this points query still keys off the live URL amount. It can therefore fetch and render the hypothetical $5,000 points estimate on the success screen, and PaymentSuccessView emits POINTS_EARNED with that false value. UseexecutedAmountUsdfor the success-step calculation (or snapshot the completed estimate) and extend the post-completion URL-edit regression to assert the points value too. -
MINOR · src/components/AddWithdraw/DynamicBankAccountForm.tsx:195 · [claude-opus] New existing-account branch selects the withdrawal destination with no test
DynamicBankAccountFormreplaced its unconditional existing-account short-circuit withif (existingAccount && onExistingAccount)(DynamicBankAccountForm.tsx:195), andAddWithdrawCountriesListsupplies the handler (AddWithdrawCountriesList.tsx:459) which callswithdrawFlow?.setSelectedBankAccount(account)and pushes/withdraw/{country}/bank?amount=….selectedBankAccountis exactly the shared stateuseBridgeOfframpFlowreads to builddestination.externalAccountIdforcreateOfframp— it decides which bank account receives the money — and the amount forwarded on that URL is what the submit re-validates. Neither half is covered: the only test file that touches this component mocks it away entirely (jest.mock('@/components/AddWithdraw/DynamicBankAccountForm', () => ({ DynamicBankAccountForm: () => <div /> })), AddWithdrawCountriesList.test.tsx:162), so no test exercises the branch.
The same edit silently changes the claim flow too: with no onExistingAccount prop, a bank-claim user who re-types an already-saved account now falls through to addBankAccount instead of short-circuiting. That path happens to be safe — peanut-api-ts src/routes/user/add-bank-account.ts:177-184 returns the existing row idempotently when it already belongs to the same user, and only 409s for another user's account — but nothing pins that behaviour.
Fix: add two cases to AddWithdrawCountriesList.test.tsx (unmocking the form, or asserting on the handler): (1) withdraw flow, typed account already saved → setSelectedBankAccount is called with that account and the push carries both amount and, from send, method; (2) claim flow with no onExistingAccount → submission proceeds to onSuccess rather than navigating.
Checked clean
- Exact detached head and merge base match the supplied SHAs; worktree stayed clean.
- Prior P1-P15 were rechecked against this head; their navigation, terminal-step proof, amount validation, balance/limits, rail-minimum, Manteca seed, method-view coverage, and bank success-amount defects are fixed.
- Bank, crypto, and Manteca money boundaries were traced through setup, live revalidation, execution proof, retry behavior, and success rendering.
- URL stepper guards, scoped withdraw context, method/account routing, Field integration, and native route builders were reviewed.
- Exact-head CI completed successfully, including unit, typecheck, eslint, format, build analysis, deploy preview, and design-system checks.
- Focused local tests that resolved in the detached environment passed 30/30; remaining focused suites could not resolve dependencies because the supplied read-only worktree has no node_modules, while exact-head CI unit passed.
Second opinion: did not run — the model did not answer in time. This review is one reviewer short.
Third opinion by claude-opus: 1 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.
Exact head: 96669a1825ef · Context: repo, peanut-api-ts · Took 28m
…on branch (Chip round 9) The success-step points estimate keyed off the live ?amount= — after completion, an edited URL could fetch and render a hypothetical points preview and emit it in POINTS_EARNED. The calculation now keys off executedAmountUsd once the offramp ran; the post-completion URL-edit regression asserts the points amount too. The existing-account branch that selects the withdraw destination gets its tests: the countries-list handler (a typed already-saved account becomes selectedBankAccount and routes to review carrying ?amount=), the form-level short-circuit (onExistingAccount wins, onSuccess never runs), and the claim fall-through (no handler → submission proceeds to onSuccess instead of the old navigate-to-dead-end behavior).
|
/chip review |
There was a problem hiding this comment.
Chip review — changes requested
Bank-review recovery drops the URL-backed amount after a refresh, and the shared URL amount parser still accepts non-decimal syntax despite the P13 fix.
Findings
-
MAJOR · src/features/withdraw/useBridgeOfframpFlow.ts:211 · Preserve the URL amount during bank-review recovery
After a user reaches/withdraw/us/bank?amount=50, a refresh remounts the withdraw-scoped provider withoutselectedBankAccount. This branch then replaces the URL with/withdraw/us(or only?method=bank), discardingamount=50even though the URL is now the sole durable amount store, so the user must enter it again. PreserveamountToWithdrawin the recovery query (along with the send marker) and cover the context-loss/refresh path. -
MINOR · src/components/AddWithdraw/AddWithdrawCountriesList.tsx:255 · [claude-opus] New-bank-account hand-off to the review page is untested
In AddWithdrawCountriesList.handleFormSubmit, the successfuladdBankAccountbranch was rewritten by this PR: it now writes the destination through the optional provider (withdrawFlow?.setSelectedBankAccount(...)) and must itself append the typed amount to the review URL (params.set('amount', urlAmount)→/withdraw/<country>/bank?amount=50), because the amount no longer travels in flow context. Nothing pins that behavior. If theamountparam is dropped, useBridgeOfframpFlow's prerequisite effect sees!amountToWithdrawandrouter.replace('/withdraw'), silently restarting the withdrawal right after the user typed their bank details — and if the context write were ever a no-op, the same effect bounces on!bankAccount. The sibling existing-account shortcut on the very same screen did get a test ('withdraw flow: a typed account that already exists selects it and routes to review with the amount', AddWithdrawCountriesList.test.tsx:376, asserting/withdraw/testland/bank?amount=50), so the more common first-time path is the one left uncovered. Add the mirror case: mockaddBankAccountto resolve with a new account, submit the form with?amount=50, and assert bothsetSelectedBankAccountandmockPushwith/withdraw/<country>/bank?amount=50(plus&method=when entered from the send flow).
Already resolved by a human, not raised again
These are still present as far as this review can tell. You closed the thread, so Chip files them instead of repeating them.
- MINOR · src/features/withdraw/useMantecaAmountSeed.ts:60 · Validate the raw amount syntax before normalizing — no task filed, please file one
Checked clean
- Pinned head, base, trusted author, and PR metadata matched the supplied coordinates.
- Rechecked prior findings P1-P17 at this head; their concrete cases are fixed except for the remaining raw-syntax gap linked to P13.
- Reviewed bank, crypto, and Manteca amount gates, terminal guards, success amount and points pinning, scoped state, navigation recovery, and existing-account routing.
- Reviewed the Field migration, native route handling, supported-chain logo changes, and focused regression coverage.
- Exact-head CI passed unit, typecheck, lint, format, analysis, design-system, provenance, ownership, and preview checks; git diff whitespace validation also passed.
- Targeted local Jest could not start because dependencies are not installed in the detached worktree; exact-head unit CI passed.
Second opinion by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only, so treat its findings as advice.
Third opinion by claude-opus: 1 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.
Exact head: 26a1958fd8db · Context: repo · Took 28m
…l raw amounts Chip round 10. The bank-review recovery redirects (refresh remounts the withdraw-scoped provider) dropped ?amount= even though the URL is the sole durable amount store, forcing a second amount entry — the recovery query now carries the amount alongside the send marker, with tests for both branches. parseUsdAmount now checks the RAW string is plain-decimal syntax before normalizing (P13 residue): Number() alone also accepted exponential, hex and padded forms. '5e1' flips from accepted to rejected in the tests. Also pins the untested new-bank-account submit hand-off (setSelectedBankAccount + push to review with ?amount=, with and without the send marker) and dedupes a duplicated PIX-maintenance describe this branch introduced.
|
Re the review's "Already resolved by a human" note (validate the raw amount syntax before normalizing, useMantecaAmountSeed.ts:60 / the P13 residue): fixed in 384f88e instead of task-filed — parseUsdAmount now requires plain-decimal RAW syntax before normalizing, so exponential/hex/padded forms ( |
|
/chip review |
There was a problem hiding this comment.
Chip review — no blocking findings — this is not an approval
All 20 prior findings are addressed at this head. P20 (validate raw amount syntax before normalizing) is fixed: parseUsdAmount now runs /^(\d+.?\d*|.\d+)$/ against the raw input before calling Number(), and a second round-trip check on .toString() catches numbers that serialize to scientific notation (≥ 1e21). The test suite explicitly covers 5e1, 0x10, whitespace-padded, sign-prefixed and comma-separated inputs. The new useFlowStepper hook and its tests are clean. No new defects found.
Findings
- MINOR · src/features/withdraw/amount-validation.ts:94 · [claude-opus] New crypto amount gate enforces no minimum, contradicting the documented Arbitrum $0.50 floor
validateCryptoWithdrawAmount(src/features/withdraw/amount-validation.ts:94) callscheckWithdrawUsdAmount(amount, balance, 0)and its doc comment asserts "No rail floor here — same-chain USDC has no minimum (parity with send-via-link)". The root amount step agrees (useWithdrawRootFlow.ts:131,if (isCryptoWithdraw) return 0) and the crypto page exempts same-chain USDC fromgetMinWithdrawUsdForChainentirely, so a $0.05 USDC-on-Arbitrum withdrawal is accepted end to end. This PR adds a test pinning that behaviour (withdraw-states.test.tsx:460, 'Crypto withdrawal has no amount-step minimum').
Product truth says otherwise in two places: /home/chip/mono/product/networks.md:7 ("Crypto withdrawal minimums are per network, as the app enforces them ($0.50 default; Ethereum $5; Tron $10)"), networks.md:22 (arbitrum minimum_withdrawal: "$0.50") and the Withdrawal Limits table row | Crypto send (Arbitrum) | $0.50 | None | Same-chain; no count cap |. The customer-facing page is stricter still: /home/chip/mono/content/withdraw/arbitrum/en.md:26/62/77 promises "Minimum withdrawal: $1 equivalent" and answers the FAQ with it.
The code is the deliberate side here — the comments explain the reasoning (a same-chain transfer is not Rhino-routed, so no route floor applies, matching send-links, whose product doc records minimum_amount: none). So the docs are what's wrong: networks.md's line-7 header, the arbitrum minimum_withdrawal field and the "Crypto send (Arbitrum) $0.50" row should say no minimum for same-chain Arbitrum and scope the $0.50/$5/$10 floors to other networks, and content/withdraw/arbitrum/en.md should be regenerated off that. If instead the $0.50 floor is the intended promise, the fix is a minUsd of 0.5 passed through validateCryptoWithdrawAmount and useWithdrawRootFlow's minUsdAmount for same-chain destinations. Either way this needs a human call — I am not asking for a code change without it. Note this is the crypto sibling of the bank-minimum gap already fixed as P9/P11, which was resolved by correcting networks.md.
Checked clean
- P20 fixed: parseUsdAmount (amount-validation.ts:26) applies /^(\d+.?\d*|.\d+)$/ to the raw string first, then Number() + isFinite/positive, then a plain-decimal round-trip on .toString(). Test describe 'rejects non-plain-decimal raw syntax even when Number() would accept it (Chip P13)' covers 5e1, 0x10, ' 50', '50 ', '+5', '5,5'. Both useMantecaAmountSeed effects route through this parser.
- useFlowStepper: guards resolve synchronously so no dead-screen flash; guard redirect rewrites URL via a useEffect; back() respects backMap before linear order; default step uses null URL param (clean URL); urlKey override works. 14-case test suite covers all branches including the nuqs throttle on URL correction.
- WithdrawFlowContext scope: provider is mounted at src/app/(mobile-ui)/withdraw/layout.tsx only, not app-global — the stale-method hijack root cause from TASK-21203. resetWithdrawFlow includes compatibility modal. useOptionalWithdrawFlow returns null outside the provider so dual-flow callers (AddWithdrawCountriesList) can guard their writes without throwing.
- P1–P19: verified resolved at this head — crypto back navigation clears transient state only; step guards gate terminal steps on execution proof; all three submit paths (bank, crypto, Manteca) re-validate the URL amount through parseUsdAmount-backed validators; balance-loading gate disables submit and returns balanceLoading no-op; Manteca seed behind limits gate with synchronous live-balance check; crypto pre-create validation and executed-amount pin; bank offramp plain function closure; rail minimum docs updated in mono; Manteca gates test file; UK GBP minimum uses iso2 fallback; exponential seed rejection; method-select test; executed amount pinned on bank success and points; existing-account form test; recovery query carries ?amount=; new-account hand-off test.
Security review: did not run — this change has no security, privacy or money surface, so it was not asked. This review is one reviewer short.
Third opinion by claude-opus: 1 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.
The usual first reviewer was out of plan, so this review was done by openrouter/anthropic/claude-sonnet-4-6.
Exact head: 384f88e81920 · Context: repo · Took 12m (queued 1h12m)
Lands via the
tech-debtintegration train (dev is merge-frozen for the release; tech-debt merges back to dev after it). Base retargeted 2026-09-02; branch restacked onto tech-debt by merge.Summary
Withdraw was the worst flow in the app by code condition: 2,792 LoC across 4 pages, three competing step machines, an app-global
WithdrawFlowContext(mounted for every page in the app), a dead amount step for 2 of 5 destinations, and zero URL state. This PR rebuilds it on a shared URL-backed stepper and scopes the flow state to the flow.The stepper is born here (
useFlowStepper): named screen ids in the URL (?step=review, never indexes), typed viaparseAsStringEnum, per-step entry guards with fallbacks (refresh/deep-link into a step whose flow memory died bounces to a live screen instead of a dead one), back owned by the stepper (backMapfor non-linear flows,onExiton the first step). Not a state-machine lib — a cursor with guards. The setup rebuild (TASK-21460) adopts it next.Withdraw on the stepper, features/ pattern:
features/withdraw/:WithdrawRoot→useWithdrawRootFlow→ dumb views (WithdrawMethodView,WithdrawAmountView); bank review page →useBridgeOfframpFlow+WithdrawBankReviewView.?step=method|amount; the typed USD amount travels as?amount=to every downstream route;?showAll=replaces the context boolean two effects raced over.currentView→?step=recipient|review|success.?step=amount|bank-details|review|success|failure, and it now honors?amount=— one amount entry, honored downstream. The seed (useMantecaAmountSeed) only advances past the amount screen once the balance/limits gates pass synchronously for the seeded amount, and the price-lock + withdraw handlers re-check balance and async limits right before the money operations (?amount=is user-editable).WithdrawFlowProvidermounts at the/withdrawlayout, not app-global. Fresh entry IS the reset; the hand-writtenresetWithdrawFlow()compensation in home and send is deleted with the disease.Field is born here (TASK-21454):
0_Bruddle/Field— label + control + one helper/error line (form board 17802:61539, gap 4). Error isFieldErrortext only and replaces the helper; Field never paints error borders (DS call). react-hook-form is the expected state owner. First consumer:DynamicBankAccountForm(all inputs + selects), which also drops its reduxbankFormpersistence and its withdraw-context coupling (props instead). Showcase:/dev/ds/primitives/field.Task
TASK-21816 (withdraw rebuild on a URL-backed stepper) + TASK-21454 (DS Field component). Absorbs: TASK-21665 (step state outside URL), TASK-21664 (dead amount step for manteca), TASK-21198 (list flicker —
showAllto the URL), TASK-21199 (token-search placeholder promised paste; copy made honest in 4 locales), TASK-21203 / TASK-20806 (stale-method back-nav hijack — killed by scoping the provider), TASK-21666 (crypto amount screen rendered no reason when limits blocked — gating predicate fixed + unit-tested), TASK-21667 (SVG chain icons → registry raster logos).Deleted / moved
AddWithdrawRouterViewdeleted: its only app consumer was the withdraw page; theflow="add"branches were unreachable (add-money rendersAddWithdrawCountriesListdirectly). The withdraw half lives on asWithdrawMethodView.src/context/WithdrawFlowContext.tsx→src/features/withdraw/WithdrawFlowContext.tsx, descoped (amount/step/showAll now URL state).components/Withdraw/views/*→features/withdraw/views/*(Initial/Confirm/PixKeySend).RecipientStatetype →components/Global/GeneralRecipientInput/types.ts(recover-wallet/recover-funds import from there).Risks / breaking changes
proceedWithOfframp,handleConfirmWithdrawal,handleWithdraw) were ported verbatim — diffs there are mechanical (state source changes only).?step=,?amount=,?showAll=) are new params; the existing native contract (?country=,?view=bank,?method=) is preserved, andscripts/native-build.js's copy of the bank page keeps working (all imports@/-based).DynamicBankAccountForm's existing-account shortcut used to hijack claim users into the withdraw review page (which then dead-ended on its no-amount guard). Without anonExistingAccounthandler (claim) submission now proceeds through the claim's own path. Flagged as a behavior change; it replaces a broken path with a working one.Design notes / accepted trade-offs
selectedMethod/selectedBankAccountstay in (flow-scoped) React context, not the URL — they're objects; the stepper guards make a refresh land on method selection instead of a dead screen. URL carries what must survive refresh/sharing: step, amount, showAll.AddWithdrawCountriesListstays dual-flow (add + withdraw) viauseOptionalWithdrawFlow; splitting it belongs to the add-money extraction (TASK-21854 Tier A)./dev/ds/audit/*data files still nameAddWithdrawRouterView— they're a dated audit snapshot, deliberately not rewritten.?amount=in the URL means analytics page-URL properties ($current_url) now carry the typed amount on /withdraw routes. Accepted: amounts already reach PostHog explicitly (WITHDRAW_AMOUNT_ENTERED.amount_usd), the privacy policy's processor/purpose table already covers both, and no new party receives anything. Flagged in case a URL-scrubbing config is preferred (docs/legal impact check: none — full sweep in the review thread).QA
npm run typecheckclean ·npx eslintclean on all touched files (0 errors) ·npm run buildgreen.withdraw-statesrewritten to run the REAL stepper + flow hook against the nuqs testing adapter — the URL contract itself is what's asserted (guard fallbacks,?amount=pre-fill/forwarding, send-marker survival, TASK-21666 predicate).crypto-withdraw-confirmkeeps the double-spend regression net on the newgoTo('success')transition. New:useFlowStepper(14 cases),Field(5),amount-gating(3),amount-validation(6 — URL-amount revalidation incl. balance-not-loaded refusal),useMantecaAmountSeed(9 — seed conversion, gate blocking, back-to-root flag, Try-again re-arm),useBridgeOfframpFlow(5 — the real submit path under the nuqs adapter: create→send→confirm with the normalized amount, the frozen-closure regression, tamper cases), crypto setup/broadcast validation (7 — zero/malformed/over-balance persist no records; the broadcast uses the charge-pinned amount, not a post-review URL edit),manteca-withdraw-gates(4 — renders the real Manteca page: submit-time balance/limits gates bounce with no money call, all-clear fires once with the locked price; writing it caught and fixed a mount-order bug that silently killed the ?amount= hand-off), GB rail-minimum submit cases (the bank submit now enforces the per-rail minimum via the shared bankWithdrawMinUsd, not a flat $1),WithdrawMethodView(3 — saved-Manteca forwarding, saved-bank advance without navigation, crypto sets method with no push). The crypto success screen and completion analytics read the executed (charge-pinned) amount, never the still-editable?amount=; exponential URL amounts (1e21) are refused fail-closed before the Manteca seed.?amount=(3 cases), the shared amount parser rejects non-plain-decimal RAW syntax —5e1flips from accepted to rejected (the P13 residue, fixed here rather than task-filed), and the new-bank-account submit hand-off is pinned (2 cases, with and without the send marker).add-money-states › loaded EVM deposit shows QR code and addressfails identically on pristineorigin/dev(b33fd0a).e2e-withdraw-onchain,e2e-withdraw-bank-ach, nav screenshots) drive routes this PR preserves; classifications unchanged.withdraw/page.tsx,withdraw/crypto/page.tsx,WithdrawFlowContext— all rebuilt/moved here. Whoever lands second rebases; the tap-balance feature is a small delta on the newWithdrawAmountView/useWithdrawRootFlowif this lands first.withdraw/crypto/page.tsxandAddWithdrawRouterView(deleted here; its withdraw half isfeatures/withdraw/views/WithdrawMethodView), and adds components undersrc/components/Withdraw/(emptied here — new home would befeatures/withdraw/).