Skip to content

fix(card): revert to collateral-first spend routing (incident: smart-first reverts on-chain)#2233

Closed
jjramirezn wants to merge 1 commit into
mainfrom
hotfix/revert-card-spend-smart-first
Closed

fix(card): revert to collateral-first spend routing (incident: smart-first reverts on-chain)#2233
jjramirezn wants to merge 1 commit into
mainfrom
hotfix/revert-card-spend-smart-first

Conversation

@jjramirezn

Copy link
Copy Markdown
Contributor

🚨 Incident mitigation — reverts #2230

Symptom (prod): card users whose funds live in collateral get Paymaster error … UserOperation reverted during simulation … ERC20: transfer amount exceeds balance when trying to pay — the payment hard-fails.

Cause: #2230 reordered computeSpendStrategy to smart-first. But card funds are swept smart→collateral to back the card, so the smart account is normally ~empty. The FE's smartBalance (useBalance, 30s-cached / read before a sweep) can be >= amount while the on-chain smart account is empty → smart-only is chosen → the kernel USDC transfer reverts at the paymaster. The old collateral-first ordering never trusted the smart balance for these users, so it was safe.

This PR: restores collateral-first ordering + its test, and leaves a NOTE in the code documenting why smart-first was reverted.

Trade-off

  • Restores the pre-fix(card): prioritize smart-account balance over Rain collateral for spends #2230 behavior → reopens the original Rain withdrawal-signature cooldown on rapid back-to-back payments when the user does hold smart-account USDC (kkonrad's report). That's a tolerable annoyance vs. broken payments.
  • The correct smart-first redo (follow-up) needs: a live/uncached smart-balance read at routing time AND a fallback to collateral when the smart-only path can't be funded (catch the revert / pre-simulate). Not safe to land without that.

QA

  • npx jest src/hooks/wallet — strategy returns collateral-only when collateral covers (single recipient), smart-only only when collateral path ineligible and smart covers, mixed for shortfall.
  • Sandbox: collateral-funded card user (smart ~0) makes a payment → routes collateral-only, no paymaster revert.

🤖 Generated with Claude Code

…rts on-chain)

#2230 reordered computeSpendStrategy to smart-first, but card funds are swept
smart→collateral to back the card, so the smart account is normally ~empty. The
FE's smartBalance (useBalance, 30s-cached / pre-sweep) could read >= amount while
the on-chain smart account is empty → smart-only was chosen → the USDC transfer
reverts at the paymaster with "ERC20: transfer amount exceeds balance" for
collateral-funded users (prod incident). Collateral-first never trusted the smart
balance for these users, so it was safe.

Restore collateral-first. Re-introducing smart-first (to avoid Rain's
withdrawal-signature cooldown when the user genuinely holds smart-account USDC)
needs a live/uncached balance read AND a fallback to collateral when the
smart-only path can't be funded — tracked as a follow-up.
@vercel

vercel Bot commented Jun 16, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Jun 16, 2026 5:17pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0e2005d4-a0e8-4bf7-9f7d-3045fc92353f

📥 Commits

Reviewing files that changed from the base of the PR and between 9f371b3 and b8cd018.

📒 Files selected for processing (2)
  • src/hooks/wallet/__tests__/useSpendBundle.test.ts
  • src/hooks/wallet/useSpendBundle.ts

Walkthrough

computeSpendStrategy in useSpendBundle.ts now checks collateral-only before smart-only: when collateralOnlyAllowed is true and Rain can fully cover the amount, it returns collateral-only; otherwise it falls back to smart-only. The test for the overlapping-coverage case is updated to expect collateral-only.

Changes

computeSpendStrategy Collateral-First Routing

Layer / File(s) Summary
Routing priority swap and test update
src/hooks/wallet/useSpendBundle.ts, src/hooks/wallet/__tests__/useSpendBundle.test.ts
computeSpendStrategy now returns collateral-only before smart-only when collateralOnlyAllowed is true and Rain can cover the full amount; the inline comment is updated to note the revert of the prior smart-first order, and the test case is updated to expect collateral-only in the overlapping-coverage scenario.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~5 minutes

Possibly related PRs

  • peanutprotocol/peanut-ui#2230: Directly modifies the same computeSpendStrategy routing priority and useSpendBundle.test.ts expectations, with the opposite smart-first ordering that this PR reverts.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: reverting to collateral-first spend routing and references the incident it fixes (#2230 smart-first revert causing on-chain failures).
Description check ✅ Passed The description is well-detailed and directly related to the changeset, explaining the incident symptom, root cause (smart-first causing paymaster reverts), the fix (collateral-first restoration), trade-offs, and QA verification.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 5786.03 → 5786.17 (+0.14)
Findings: 0 net (+9 new, -9 resolved)

🆕 New findings (9)

  • medium high-mdd — src/hooks/wallet/useSpendBundle.ts:141 — useSpendBundle: MDD 68.0 (uses across many lines from declarations)
  • medium high-mdd — src/hooks/wallet/useSpendBundle.ts:155 — : MDD 60.5 (uses across many lines from declarations)
  • medium structural-dup — src/hooks/wallet/useSignSpendBundle.ts:271 — 22 duplicate lines / 98 tokens with src/hooks/wallet/useSpendBundle.ts:334
  • medium structural-dup — src/hooks/wallet/useSpendBundle.ts:216 — 22 duplicate lines / 116 tokens with src/hooks/wallet/useSpendBundle.ts:300
  • medium structural-dup — src/hooks/wallet/useSignSpendBundle.ts:197 — 20 duplicate lines / 98 tokens with src/hooks/wallet/useSpendBundle.ts:220
  • low high-dlt — src/hooks/wallet/useSpendBundle.ts:141 — useSpendBundle: DLT 26 (calls 26 distinct functions — high context load)
  • low high-dlt — src/hooks/wallet/useSpendBundle.ts:155 — : DLT 19 (calls 19 distinct functions — high context load)
  • low structural-dup — src/hooks/wallet/useSignSpendBundle.ts:241 — 12 duplicate lines / 50 tokens with src/hooks/wallet/useSpendBundle.ts:289
  • low missing-return-type — src/hooks/wallet/useSpendBundle.ts:141 — useSpendBundle: exported fn missing return type annotation

✅ Resolved (9)

  • src/hooks/wallet/useSpendBundle.ts:138 — useSpendBundle: MDD 68.0 (uses across many lines from declarations)
  • src/hooks/wallet/useSpendBundle.ts:152 — : MDD 60.5 (uses across many lines from declarations)
  • src/hooks/wallet/useSignSpendBundle.ts:271 — 22 duplicate lines / 98 tokens with src/hooks/wallet/useSpendBundle.ts:331
  • src/hooks/wallet/useSpendBundle.ts:213 — 22 duplicate lines / 116 tokens with src/hooks/wallet/useSpendBundle.ts:297
  • src/hooks/wallet/useSignSpendBundle.ts:197 — 20 duplicate lines / 98 tokens with src/hooks/wallet/useSpendBundle.ts:217
  • src/hooks/wallet/useSpendBundle.ts:138 — useSpendBundle: DLT 26 (calls 26 distinct functions — high context load)
  • src/hooks/wallet/useSpendBundle.ts:152 — : DLT 19 (calls 19 distinct functions — high context load)
  • src/hooks/wallet/useSignSpendBundle.ts:241 — 12 duplicate lines / 50 tokens with src/hooks/wallet/useSpendBundle.ts:286
  • src/hooks/wallet/useSpendBundle.ts:138 — useSpendBundle: exported fn missing return type annotation

@github-actions

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 1431 ran, 0 failed, 0 skipped, 21.7s

📊 Coverage (unit)

metric %
statements 52.1%
branches 34.4%
functions 39.1%
lines 52.0%
⏱ 10 slowest test cases
time test
0.3s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in updateUserById
0.3s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every stamp stays within canvas at any count
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in updateUserById body
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid 9-digit US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle too long for US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid Italian IBAN
0.1s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Perk claim in progress shows disabled button + progress
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid German IBAN
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle invalid ETH address (invalid characters)
0.1s src/components/Request/__tests__/request-states.test.tsx › API failure shows error message and toast
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@jjramirezn

Copy link
Copy Markdown
Contributor Author

Superseded by #2234 — keeping smart-first and fixing the root cause (route on a live on-chain balance) instead of reverting. Branch retained as an emergency fallback.

@jjramirezn jjramirezn closed this Jun 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant