Repository navigation
chore: back-merge main into dev (2026-09-07) - #3020
Conversation
…te the route (TASK-22154) peanut-api-ts #1497 answers POST /rhino/sda-transfer with 429 { error, code: XCHAIN_WITHDRAW_LIMIT_REACHED, retryAfterSec } once a user is over the per-user cross-chain withdrawal cap. Today postRhino throws "Failed to provision SDA transfer: 429 {json}" and the confirm view shows that string; its Retry then calls onConfirm with no prepared transactions and dead-ends on "transaction not prepared". - postRhino throws an ApiError (message = backend text, plus code and retryAfterSec), and apiErrorFromResponse now carries retryAfterSec. - friendlyError maps XCHAIN_WITHDRAW_LIMIT_REACHED to localized copy that states the wait in the coarsest unit (minutes / hours / days) and points at Arbitrum (no limit) and support; en, es-419, es-AR, pt-BR. - useCrossChainTransfer surfaces errors through useFriendlyError instead of echoing err.message. - Retry after a route error recomputes the route instead of failing on the transactions the failed route never built. Claude-Session: https://claude.ai/code/session_017idXbJRcFgbugvA8Xxr5YC
…0903-014858 content: publish latest to production (src/content → peanut-content@d700af2)
…-stablecoin withdrawals peanut-api-ts #1497 gates POST /rhino/bridge/quote with the same per-user cross-chain cap as the SDA route when the quote names its charge. Send context + contextId from the bridge path (ETH/WETH/… destinations), which never touched /rhino/sda-transfer and so was uncapped. Claude-Session: https://claude.ai/code/session_017idXbJRcFgbugvA8Xxr5YC
…nder-promises Chip review (bc90092): - handleConfirmWithdrawal branched on routeError but its dependency list omitted routeError and calculateCurrentRoute, so a 429 that arrived after the callback was memoized still fell through to "transaction not prepared". Both are dependencies now; regression test added to crypto-withdraw-confirm.test.tsx. - The cap wait rounded the shown unit down (119 min → "1 hour", 47 h → "1 day"). It now rounds up, and a unit is used only once the wait reaches it. Claude-Session: https://claude.ai/code/session_017idXbJRcFgbugvA8Xxr5YC
Chip on peanut-api-ts#1497 (60991481): rhino-bridge.ts postJson/getJson still threw a plain Error, so a cap 429 on POST /rhino/bridge/quote would have rendered the generic 'contact support' copy. Same apiErrorFromResponse as rhino-sda.ts now. Claude-Session: https://claude.ai/code/session_017idXbJRcFgbugvA8Xxr5YC
…e binding, drop stray legal-versions bump Chip review (42de3c5): - Retry recomputes only when no calculation is in flight, so a double tap cannot provision twice (each provision holds a cap slot) or race the route state. - useCrossChainTransfer test: the bridge quote carries context + contextId for a withdraw and omits both for claim-xchain. - src/constants/legal-versions.generated.ts was regenerated by the dev server's predev hook against a newer content submodule and committed by mistake; restored to main. The privacy bump ships with its backend half. Claude-Session: https://claude.ai/code/session_017idXbJRcFgbugvA8Xxr5YC
Chip (f2e511f): the cap counts request payments too, and a payer blocked on the semantic-request flow saw "withdrawals to other networks … withdraw on Arbitrum instead" — advice they cannot act on, the request fixed the destination. friendlyError takes a crossChainSurface hint; the cross-chain hook passes 'payment' for pay-request, which selects xchainPaymentLimit* copy (no Arbitrum advice) in en, es-419, es-AR, pt-BR. Claude-Session: https://claude.ai/code/session_017idXbJRcFgbugvA8Xxr5YC
Chip (3844036): the isCalculating guard on the route-error Retry branch had no test. A second tap while a recalculation is in flight must not call calculate again or set 'not prepared'. Claude-Session: https://claude.ai/code/session_017idXbJRcFgbugvA8Xxr5YC
…ASK-22185) (#2942) * fix(watchdog): compare the mirror stamp by ancestry, not by string The mirror stamps peanut-content with the mono commit it ran at. When someone pulls without rebasing and pushes the merge, that stamp is the merge commit, which only contains the content change. The watchdog read "expected 7cf077f, published 37adb24" as a stale mirror and alerted Discord every 15 minutes for 8 hours on 2026-09-03 while production was serving the right content. The mirror is current when the stamped commit is the expected commit or a descendant of it. Ask the compare API (contents:read, which the mono token already has) and accept "identical" or "ahead". Anything else, including an unknown SHA, still alerts. Claude-Session: https://claude.ai/code/session_01Rg9upFPdzh4ns3Dwyk6cao * fix(watchdog): also require the stamp to be on main; API failure is a watchdog fault Review findings on the ancestry check: - "ahead" from expected...stamped does not prove the stamp is on mono main. The mirror has workflow_dispatch and checks out the triggering ref, so a manual run from a branch would publish content main never had and the watchdog would stay green. Add a second compare, stamped...main, and alert when it is not identical or ahead. - A failed compare call (rate limit, 404, token scope) produced an empty status that read as a stale mirror and paged with no grace. Route it to the DEGRADED branch instead, and log the raw response. Claude-Session: https://claude.ai/code/session_01Rg9upFPdzh4ns3Dwyk6cao
…0903-131959 content: publish latest to production (src/content → peanut-content@152377f)
Chip on peanut-api-ts#1497 (6e815a66): only the quote sent context/contextId; with the cap on, POST /rhino/bridge/commit refuses commits without them, so a non-stablecoin withdrawal would quote and then fail at commit. The commit now carries the same charge (none for claim-xchain). Hook test asserts both. Claude-Session: https://claude.ai/code/session_017idXbJRcFgbugvA8Xxr5YC
…0903-171034 content: publish latest to production (src/content → peanut-content@8edbfaf)
…t-error-22154 fix(withdraw): show the cross-chain withdrawal cap message and retry the route (TASK-22154)
…0904-120851 content: publish latest to production (src/content → peanut-content@40503da)
…-2026-09-07 # Conflicts: # src/app/(mobile-ui)/withdraw/crypto/__tests__/crypto-withdraw-confirm.test.tsx # src/app/(mobile-ui)/withdraw/crypto/page.tsx # src/features/payments/shared/hooks/__tests__/useCrossChainTransfer.test.ts # src/features/payments/shared/hooks/useCrossChainTransfer.ts # src/services/api-error.ts # src/services/rhino-bridge.ts # src/services/rhino-sda.ts # src/utils/friendly-error.utils.tsx
|
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: 7260.1 → 7263.33 (+3.23) 🆕 New findings (29)
…and 9 more. ✅ Resolved (26)
…and 6 more. 📈 Painscore deltas (top movers)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
🖼 Visual diff — 10 screens moved10 of 68 shots changed · 58 identical · baseline
job summary · before/after/diff images — artifact Fixture screenshots, no backend. Advisory — this check never blocks a merge. Posted from the default branch by ds-shots-comment.yml; the report it renders is untrusted data. |
There was a problem hiding this comment.
Chip review — no blocking findings — this is not an approval
One major merge-resolution regression remains: initial and retry cross-chain quotes use the rounded UI amount instead of the amount frozen into the charge, so max withdrawals can leave funds behind. Exact-head required CI is green.
Findings
-
MAJOR · src/app/(mobile-ui)/withdraw/crypto/page.tsx:233 · Quote the amount frozen into the withdrawal charge
handleSetupReviewfreezespreparedAmountfromliveResolvedAmountand writes that value into the charge, but this entry/retry callback sendsamountToWithdrawto the pay-mode quote. For a max withdrawal with 10.126123 USDC displayed as 10.12, the charge is for 10.126123 while the SDA or bridge transaction is sized to 10.12, leaving 0.006123 behind and persisting quote economics that disagree with the charge. ReusequoteRoutehere, or passeffectiveAmountand depend on it, and cover a cross-chain max withdrawal where the frozen amount differs from the displayed cents. -
MAJOR · src/app/(mobile-ui)/withdraw/crypto/page.tsx:233 · [claude-opus] Back-merge re-quotes the withdraw route off the unfrozen amount; no test covers the amount calculateRoute is called with
dev (parent ecd7c0b / base 5c52afa) quoted the route throughquoteRoute, which passeseffectiveAmount(=preparedAmount, the amount frozen when the charge was built — TASK-21899). main (parent 623291d) still had the oldercalculateCurrentRoute, which passesamountToWithdraw, the displayed-cents amount. The merge kept both:quoteRoutesurvives but is now reachable only from the quote-expiry branch (page.tsx:437), while the confirm-view entry effect (page.tsx:251-253) and the new cap-429 Retry path (page.tsx:423) both callcalculateCurrentRoute, i.e.tokenAmount: amountToWithdraw(page.tsx:233).
Failure case: a 'use full balance' withdrawal where spendable balance is 10.126123 USDC. resolveWithdrawAmount (src/utils/withdraw.utils.ts:391) returns '10.126123' and that is frozen into preparedAmount and into the charge the API validator settles against; amountToWithdraw stays '10.12'. Entering CONFIRM (and every Retry after a cap 429) now quotes the route with '10.12'. On the bridge path the quote is sized in mode: 'pay' from source.tokenAmount, and for a cross-chain withdrawal the kernel spends payAmount derived from that quote (page.tsx:702) — so the spend and delivery are sized off a number that is not the charge's, the sub-cent remainder is stranded again (exactly the bug the frozen-amount work fixed), and the first quote and the expiry re-quote disagree with each other.
The reason this merged green: the whole 'the spend is frozen with the charge' suite only asserts sendMoney's argument (crypto-withdraw-confirm.test.tsx:709-778), which covers the same-chain path; the two new Retry tests assert only that calculate was called once more, never with what amount. There is no test that a max cross-chain withdrawal quotes the route with the frozen amount — that is the untested money case, and per the repo's hard test rule it needs one.
Fix: point the confirm-entry effect and the Retry branch at the frozen amount (either call quoteRoute() in both places, as dev did, or change calculateCurrentRoute's tokenAmount to effectiveAmount and delete the duplicate), and add a case asserting calculate is invoked with source.tokenAmount === preparedAmount for a max withdrawal on both confirm-entry and Retry.
Checked clean
- Confirmed the detached worktree head, trusted author, PR number, base ref, and supplied base SHA; the merge base is ecd7c0b.
- Reviewed the complete merge-base diff and all eight conflict resolutions across withdrawal routing, Rhino services, error classification, tests, translations, and the content watchdog.
- Checked the sibling backend contract: SDA provisioning derives the authoritative withdrawal source amount from charge metadata and persists the client quote economics.
- Exact-head ci-success, unit, typecheck, eslint, format, native-export, and ds-lint checks succeeded; advisory ds-shots was still in progress.
- Security pass found no exposed secret or reachable command injection in the watchdog; compare inputs are constrained to hexadecimal revisions.
Security review: did not run — openrouter-http-402. 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: 9011d72e7447 · Context: repo, sibling · Took 19m
There was a problem hiding this comment.
Chip review — no blocking findings — this is not an approval
The frozen withdrawal amount is now used and tested on both confirm entry and Retry, fixing P1/P2. Two unrelated dev changes are still reverted by the back-merge.
Findings
-
MINOR · package.json:117 · Preserve the PostHog upgrade from dev
The exact base already contains #3016 / 987542a, which intentionally upgraded posthog-js from 1.360.0 to 1.418.11 and posthog-node from 5.29.2 to 5.50.0. This merge restores both older versions (and rewrites the lockfile accordingly), so merging the back-merge would silently undo that completed dependency update. Resolve the merge by retaining dev's package.json and matching pnpm-lock.yaml entries. -
MINOR · scripts/ds-lint-baseline.json:7 · Do not raise the DS baseline to absorb reverted cleanup
The base already contains #3015's cleanup, with nonDsClassesInViews reduced to 6. This head deletes the named 60vh utilities, restores arbitrary 60vh/min-width/text-size classes, and raises the baseline to 15, so ds-lint stays green by accepting nine reintroduced violations. Preserve the base-side utility/class changes and keep the baseline at 6 instead of masking the regression.
Inline anchors unavailable for 2 finding(s); the findings remain in this summary.
Checked clean
- P1 fixed: both withdrawal route calculators use effectiveAmount, which resolves to the amount frozen with the charge.
- P2 fixed: confirm-entry and Retry tests assert calculateRoute receives source.tokenAmount 10.126123 when the displayed live amount is 10.12.
- Reviewed withdrawal route calculation, Rhino SDA/bridge error propagation and charge binding, friendly-error localization, conflict-resolution tests, dependency and design-system merge results, and the secret-bearing watchdog workflow.
- Exact-head required CI completed successfully; ds-shots and Deploy Preview were still in progress when checked.
- Local focused Jest execution could not start because the detached worktree has no installed node_modules; exact-head unit CI passed.
- git diff --check passed.
Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, 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: 9df64880465e · Context: repo · Took 10m
Summary
Back-merges current
mainintodev.devhad 18 commits frommainnot yet merged, most recently main's TASK-22185 watchdog fix (2026-09-04) — no open back-merge PR existed for this yet.8 conflicts, all in the same area: the cross-chain withdraw cap feature (main's
xchain-withdraw-limit/ TASK-22154) landed the same weekdevwas independently extending the cross-chain withdraw flow (rate limiting, stale-quote guards, expired-quote-vs-error retry UX). This is the frontend half of the equivalent back-merge on peanut-api-ts (peanutprotocol/peanut-api-ts#1541).What conflicted and how it was resolved
api-error.ts/friendly-error.utils.tsx— both sides added different wire error codes / friendly-copy branches (LINK_ALREADY_CLAIMED+BELOW_MIN_BRIDGE_AMOUNTvsXCHAIN_WITHDRAW_LIMIT_REACHED). Kept both.services/rhino-sda.ts/rhino-bridge.ts—devused the sharedapiFetchhelper with an ad-hoc thrown error;mainused a lower-levelfetchWithSentry+manual-auth-headers approach with a properApiError(carriesretryAfterSec, needed for the new cap 429's countdown copy).apiFetchalready wrapsfetchWithSentry/auth/demo-mode/dev-fixtures — strictly more complete — so kept it, but swapped the error construction toapiErrorFromResponse(main's), sincewireErrorCode/cooldownMinutesneed theretryAfterSecit captures and the old ad-hoc error didn't carry it. Verifiedrhino-bridge.ts's ownpostJsonalready combined the two this exact way, unconflicted — confirms this is the intended shape.useCrossChainTransfer.ts—devadded a stale-response guard (live()wrapping every setter, since concurrent quote recalculations are routine on re-quote) and aquoteIdnull-assertion before commit;mainaddedcontext/contextIdthreading into the bridge path (required —commitBridgeQuote's signature already takes a 4thchargeparam, confirmed by grep) so the cap gate covers bridge commits too. Combined: kept thelive()guards, addedcontext/contextIdto the bridge path call and tocommitBridgeQuote, and swapped the plain-string catch handler fortoFriendlyError(...)(stilllive()-wrapped) so the cap's localized 429 copy renders instead of the raw error.useCrossChainTransfer.test.ts— rewrote to merge both test suites (dev's fee/quote-math + stale-response-guard tests, main's charge-naming tests) under one mock setup;useFriendlyErrornow needs mocking for every test since the hook calls it unconditionally, not just on error.withdraw/crypto/page.tsx—devandmaineach defined what looked like the same "quote the route" callback with different names (quoteRoutevscalculateCurrentRoute) and different amount variables (effectiveAmount, the frozen sign-time amount, vsamountToWithdraw, the live input amount). Turned out both are needed and both are referenced elsewhere unconflicted —quoteRoutere-quotes right before signing when the on-screen quote expires (needs the frozen amount for consistency),calculateCurrentRoutedrives the view-entry effect and the retry-after-error path (needs the live amount). Kept both functions; droppeddev's own view-entryuseEffectsincemain's unconflicted one (callingcalculateCurrentRoute) already covers that.handleConfirmWithdrawal's "nothing prepared" branch,main's version only retried when arouteErrorwas already set, else showed a dead-end "not prepared" error;dev's test (crypto-withdraw-confirm.test.tsx, unconflicted, pre-existing) requires an unconditional retry with no dead-end message at all, matching dev's own comment: "a persistent failure keeps surfacing through routeError," never a one-off error banner. Went with dev's unconditional-retry philosophy, keeping main's double-tap guard (isCalculating) — confirmed by running the full suite (see below), main's own new retry tests don't require therouteError-gated behavior, only dev's pre-existing test does.crypto-withdraw-confirm.test.tsx— two disjoint newdescribeblocks (dev's balance-gate + frozen-spend tests, main's retry-after-cap-error tests). Concatenated, no overlap.Verified
pnpm typecheckclean.useCrossChainTransfer.test.ts,crypto-withdraw-confirm.test.tsx,friendly-error.utils.test.tsxall green (30 + 58 tests).pnpm prettier --checkon every touched file: no diff.Worth a second look
Nothing outstanding — every conflict resolved to a state confirmed compatible by an existing (possibly newly-combined) test, unlike the api-ts counterpart which had a couple of items needing a product call.