Skip to content

fix(request): show spendable balance incl. card collateral#2266

Merged
Hugo0 merged 10 commits into
devfrom
fix/request-spendable-balance
Jun 24, 2026
Merged

fix(request): show spendable balance incl. card collateral#2266
Hugo0 merged 10 commits into
devfrom
fix/request-spendable-balance

Conversation

@abalinda

@abalinda abalinda commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Summary

A user reported the "Balance: $XXX" on /request reading lower than /home (their example: $2213.38 vs $2472.03). Both screens read the same useWallet() hook but picked different fields:

  • /home (and /send) show spendableBalance = smart-account USDC + Rain card collateral (landed + in-transit).
  • /request showed balance = smart-account USDC only, silently dropping card collateral.

The gap is exactly the funds the user holds as card collateral. This is the case useWallet.ts:257-263 explicitly warns against ("payment-input forms … should show spendableBalance … otherwise a user with funds split across smart and collateral sees a smaller balance than they actually have"). The Send view already follows this; the two Request views were the only outliers.

Fix: alias spendableBalance: balance in both Request views, mirroring Initial.link.send.view.tsx.

Risks / breaking changes

  • Display-only. AmountInput's walletBalance prop is rendered as text only (AmountInput/index.tsx:279-284); it is not wired to any affordability / spend gate. No spend-routing logic changes.
  • No cross-repo / API impact. spendableBalance is already returned by useWallet.

QA

Sandbox, user with funds split across smart account and Rain card collateral:

  1. /home balance, /send balance, and /request balance (both the create-link view and a direct /<username> request) now show the same number.
  2. Previously /request read lower by the collateral amount.

Update — 2nd commit (d2cfdfeda): unify the affordability gate (scope expanded)

The display fix above only patched a symptom. The same "which balance field?" bug class lives in the affordability gates of the legacy money-flows, so this commit fixes the root cause across them.

The bug class. The displayed spendable balance includes in-transit card-collateral top-ups (added in #2170 so the balance doesn't crater to $0 during the ~10–45s smart→collateral handoff). Five legacy flows hand-rolled their "insufficient balance" gate by comparing the entered amount against that display number — which can briefly read higher than what's actually routable. useWallet already exposes hasSufficientSpendableBalance() (gates on available-now = smart + landed collateral) for exactly this, but only the newer features/payments/flows used it.

Fix. Route the gates through the shared predicate:

  • Send/link, qr-pay, withdraw/bank, withdraw/mantecahasSufficientSpendableBalance(amount)
  • withdraw page ceiling (maxDecimalAmount) → newly-exposed availableSpendableBalance
  • Displayed balances are unchanged.

Risk. Outside the top-up window available == display, so behaviour is identical — the change only tightens the rare in-transit window (block at input vs. fail at execution). One known, by-design nuance: during a top-up the display can read higher than the gate allows for ~30s, self-healing once collateral lands.

Tests. Revived the qr-pay insufficient-balance test (was .skipped on mock-shape drift) + added a request-view regression locking the spendable-balance display field. 6 source + 3 test files; typecheck ✅, prettier ✅, affected jest suites ✅.

The /request create-link and direct-request views showed smart-account-only `balance`, excluding Rain card collateral, so the "Balance:" affordance read lower than /home and /send for users whose funds are split into card collateral. Switch to spendableBalance (smart + collateral), matching the Send view and useWallet's documented intent (useWallet.ts:257-263).
@vercel

vercel Bot commented Jun 21, 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 24, 2026 2:33pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The PR centralizes balance-gate messages, changes wallet affordability checks to use displayed spendable balance, and updates request, QR pay, withdraw, and send flows to use the new balance and error-message behavior.

Changes

Spendability gating and balance messaging

Layer / File(s) Summary
Balance messages and wallet gating
src/utils/balance.utils.ts, src/hooks/wallet/useWallet.ts
balance.utils exports distinct insufficient and settling copy, and useWallet gates affordability against spendableBalance while removing the previous available-now derivation.
Retry handling and error messaging
src/hooks/wallet/useSignSpendBundle.ts, src/hooks/wallet/useSpendBundle.ts, src/hooks/wallet/useSendMoney.ts, src/utils/friendly-error.utils.tsx
useSignSpendBundle and useSpendBundle invalidate the Rain card overview query before throwing InsufficientSpendableError, useSendMoney maps that error to the settling message, and friendly-error.utils maps matching errors to the same settling copy.
Request views use spendable balance
src/components/Request/direct-request/views/Initial.direct.request.view.tsx, src/components/Request/link/views/Create.request.link.view.tsx, src/components/Request/__tests__/request-states.test.tsx
DirectRequestInitialView and CreateRequestLinkView now render formattedSpendableBalance, and the request-state test suite adds direct-request wiring plus assertions for the spendable balance passed into AmountInput.
QR pay spendability checks
src/app/(mobile-ui)/qr-pay/page.tsx, src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx
QRPayPage validates payments with hasSufficientSpendableBalance, uses the insufficient and settling messages in validation and signing error paths, and the QR pay test suite updates its wallet mocks and restores the insufficient-balance case.
Withdraw spendability checks
src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx, src/app/(mobile-ui)/withdraw/manteca/page.tsx, src/app/(mobile-ui)/withdraw/page.tsx, src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx
The bank, manteca, and main withdraw pages now use formattedSpendableBalance for display, hasSufficientSpendableBalance for validation, and the centralized insufficient/settling messages, with the withdraw tests updated for the new wallet shape and error text.
Send link spendability checks
src/components/Send/link/views/Initial.link.send.view.tsx
LinkSendInitialView now uses formattedSpendableBalance, validates with hasSufficientSpendableBalance, and shows the centralized insufficient-balance message when the send amount is blocked.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • peanutprotocol/peanut-ui#1995: Both PRs rework payment-input affordability to use useWallet’s spendable-balance API and adjust balance gating across request/payment flows.
  • peanutprotocol/peanut-ui#2014: Both PRs touch the same QR pay and Manteca withdrawal signing error-handling paths, with shared InsufficientSpendableError handling.
  • peanutprotocol/peanut-ui#2190: Both PRs modify the wallet affordability and balance-gating stack in balance.utils.ts and hooks/wallet/useWallet.ts.

Suggested reviewers

  • jjramirezn
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: showing the request balance from spendable funds including card collateral.
Description check ✅ Passed The description accurately describes the request balance fix and the related follow-up scope in the PR.
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.


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.

❤️ Share

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

@github-actions

github-actions Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 5813.79 → 5746.2 (-67.59)
Findings: +3 net (+103 new, -100 resolved)

🆕 New findings (103)

  • critical complexity — src/app/(mobile-ui)/qr-pay/page.tsx — CC 296, MI 53.26, SLOC 955
  • critical complexity — src/app/(mobile-ui)/withdraw/manteca/page.tsx — CC 152, MI 52.4, SLOC 520
  • critical complexity — src/app/(mobile-ui)/withdraw/page.tsx — CC 122, MI 53.88, SLOC 336
  • critical complexity — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — CC 98, MI 54.79, SLOC 327
  • critical method-complexity — src/app/(mobile-ui)/qr-pay/page.tsx:97 — QRPayPage CC 74 SLOC 328
  • critical complexity — src/components/Request/direct-request/views/Initial.direct.request.view.tsx — CC 57, MI 55.65, SLOC 167
  • critical complexity — src/utils/friendly-error.utils.tsx — CC 54, MI 44.72, SLOC 147
  • high hotspot — src/app/(mobile-ui)/qr-pay/page.tsx — 86 commits, +988/-955 lines since 6 months ago
  • high hotspot — src/app/(mobile-ui)/withdraw/manteca/page.tsx — 70 commits, +699/-421 lines since 6 months ago
  • high method-complexity — src/app/(mobile-ui)/withdraw/manteca/page.tsx:92 — MantecaBankWithdrawFlow CC 44 SLOC 197
  • high hotspot — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — 43 commits, +422/-227 lines since 6 months ago
  • high complexity — src/hooks/wallet/useWallet.ts — CC 43, MI 57.56, SLOC 186
  • high method-complexity — src/utils/friendly-error.utils.tsx:44 — CC 41 SLOC 112
  • high complexity — src/hooks/wallet/useSignSpendBundle.ts — CC 13, MI 38.79, SLOC 179
  • medium react-long-component — src/app/(mobile-ui)/qr-pay/page.tsx:97 — QRPayPage is 1507 lines — split it
  • medium react-long-component — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:59 — WithdrawBankPage is 495 lines — split it
  • medium react-long-component — src/app/(mobile-ui)/withdraw/page.tsx:28 — WithdrawPage is 443 lines — split it
  • medium high-mdd — src/app/(mobile-ui)/qr-pay/page.tsx:97 — QRPayPage: MDD 339.3 (uses across many lines from declarations)
  • medium high-mdd — src/app/(mobile-ui)/withdraw/manteca/page.tsx:92 — MantecaBankWithdrawFlow: MDD 249.2 (uses across many lines from declarations)
  • medium high-mdd — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:59 — WithdrawBankPage: MDD 142.7 (uses across many lines from declarations)

…and 83 more.

✅ Resolved (100)

  • src/app/(mobile-ui)/qr-pay/page.tsx — CC 295, MI 53.27, SLOC 955
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx — CC 151, MI 52.36, SLOC 520
  • src/app/(mobile-ui)/withdraw/page.tsx — CC 119, MI 53.93, SLOC 336
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — CC 98, MI 54.59, SLOC 331
  • src/app/(mobile-ui)/qr-pay/page.tsx:92 — QRPayPage CC 74 SLOC 329
  • src/components/Request/direct-request/views/Initial.direct.request.view.tsx — CC 57, MI 55.57, SLOC 167
  • src/utils/friendly-error.utils.tsx — CC 51, MI 45.57, SLOC 139
  • src/app/(mobile-ui)/qr-pay/page.tsx — 81 commits, +950/-930 lines since 6 months ago
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx — 65 commits, +663/-391 lines since 6 months ago
  • src/hooks/wallet/useWallet.ts — CC 47, MI 56.61, SLOC 195
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx:87 — MantecaBankWithdrawFlow CC 43 SLOC 198
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — 39 commits, +399/-197 lines since 6 months ago
  • src/utils/friendly-error.utils.tsx:41 — CC 39 SLOC 108
  • src/hooks/wallet/useSignSpendBundle.ts — CC 13, MI 38.9, SLOC 177
  • src/app/(mobile-ui)/qr-pay/page.tsx:92 — QRPayPage is 1499 lines — split it
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:63 — WithdrawBankPage is 498 lines — split it
  • src/app/(mobile-ui)/withdraw/page.tsx:28 — WithdrawPage is 432 lines — split it
  • src/app/(mobile-ui)/qr-pay/page.tsx:92 — QRPayPage: MDD 335.8 (uses across many lines from declarations)
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx:87 — MantecaBankWithdrawFlow: MDD 247.4 (uses across many lines from declarations)
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:63 — WithdrawBankPage: MDD 142.4 (uses across many lines from declarations)

…and 80 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/utils/balance.utils.ts 5.7 7.0 +1.2
src/components/Send/link/views/Initial.link.send.view.tsx 10.6 11.7 +1.1
src/app/(mobile-ui)/withdraw/page.tsx 15.0 15.5 +0.5
src/app/(mobile-ui)/add-money/[country]/bank/page.tsx 20.0 19.5 -0.5

@github-actions

github-actions Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 1554 ran, 0 failed, 0 skipped, 23.1s

📊 Coverage (unit)

metric %
statements 53.5%
branches 36.3%
functions 40.9%
lines 53.3%
⏱ 10 slowest test cases
time test
0.4s 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.2s 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/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Perk claim in progress shows disabled button + progress
0.1s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Perk claimed shows shake class + go home button
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid Italian IBAN
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle minimum length (6 digits) 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 German IBAN
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@abalinda abalinda marked this pull request as ready for review June 21, 2026 16:10
Copilot AI review requested due to automatic review settings June 21, 2026 16:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@abalinda

Copy link
Copy Markdown
Contributor Author

Switches the two /request views from the smart-account-only balance to spendableBalance, so the "Balance:" line now includes Rain card collateral and matches /home + /send. Fixes the report where /request read lower than the homescreen

image image

Root-caused

  • useWallet already exposes both; /send was correct, the two request views (create-link + direct) were the only ones still reading smart-only. Lines up with useWallet's own comment that payment-input forms should show spendable.
  • Confirmed AmountInput's balance is display-only — not wired to any affordability/spend gate, so zero risk to send routing.

CodeRabbit no actionable comments + /code-review clean,
Mergeable. Display-only 2-line change, low risk.

@abalinda abalinda requested a review from jjramirezn June 22, 2026 20:45
…y balance

The displayed spendable balance includes in-transit card-collateral top-ups so
it doesn't crater during the ~10-45s smart->collateral handoff. Several legacy
flows hand-rolled their affordability gate against that DISPLAY number, so
during a top-up they could green-light a spend whose funds aren't routable yet
- failing at execution instead of being blocked at input.

Route Send-link, qr-pay and bank/manteca withdraw gates through the shared
useWallet.hasSufficientSpendableBalance() predicate (smart + LANDED collateral),
matching the features/payments flows. Withdraw's amount ceiling now derives from
a newly-exposed availableSpendableBalance. Displayed balances are unchanged.

Outside the top-up window available == display, so behaviour is identical; the
change only tightens the rare in-transit window. Revives the qr-pay
insufficient-balance test (skipped on mock drift) and adds a request-view
regression for the spendable-balance display fix.
@coderabbitai coderabbitai Bot added the enhancement New feature or request label Jun 23, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/components/Request/__tests__/request-states.test.tsx (1)

415-437: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a mirrored regression test for the direct-request view.

This new guard only covers CreateRequestLinkView, but the bug/fix scope includes both request entry views. Please add the same printableUsdc argument assertion for DirectRequestInitialView to lock both paths.

Proposed test addition pattern
+describe('GROUP 0b: Direct request balance affordance', () => {
+    test('formats spendableBalance (smart + collateral), not smart-only balance', () => {
+        mockUseWallet.mockReturnValue({
+            address: '0x1234567890abcdef1234567890abcdef12345678',
+            isConnected: true,
+            balance: BigInt(100_000_000),
+            spendableBalance: BigInt(250_000_000),
+        })
+
+        renderDirectRequest()
+
+        expect(jest.mocked(printableUsdc)).toHaveBeenCalledWith(BigInt(250_000_000))
+        expect(jest.mocked(printableUsdc)).not.toHaveBeenCalledWith(BigInt(100_000_000))
+    })
+})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Request/__tests__/request-states.test.tsx` around lines 415 -
437, Add a mirrored regression test for DirectRequestInitialView within the same
test group GROUP 0: Balance affordance. The new test should follow the same
pattern as the existing test for CreateRequestLinkView: set up the mockUseWallet
with the same balance values (balance: BigInt(100_000_000) and spendableBalance:
BigInt(250_000_000)), render DirectRequestInitialView instead of
renderCreateRequest, and assert that printableUsdc is called with
BigInt(250_000_000) but not with BigInt(100_000_000). This ensures both request
entry views are guarded against the regression where balance is used instead of
spendableBalance.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/`(mobile-ui)/qr-pay/page.tsx:
- Line 101: The destructuring assignment in the useWallet() hook call on line
101 includes sendMoney which is not used anywhere in the component, causing a
lint failure. Remove sendMoney from the destructuring statement so only the
actually used properties (spendableBalance aliased as balance and
hasSufficientSpendableBalance) are destructured from useWallet().

---

Nitpick comments:
In `@src/components/Request/__tests__/request-states.test.tsx`:
- Around line 415-437: Add a mirrored regression test for
DirectRequestInitialView within the same test group GROUP 0: Balance affordance.
The new test should follow the same pattern as the existing test for
CreateRequestLinkView: set up the mockUseWallet with the same balance values
(balance: BigInt(100_000_000) and spendableBalance: BigInt(250_000_000)), render
DirectRequestInitialView instead of renderCreateRequest, and assert that
printableUsdc is called with BigInt(250_000_000) but not with
BigInt(100_000_000). This ensures both request entry views are guarded against
the regression where balance is used instead of spendableBalance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f3c87fa8-74f3-459d-a02b-412ac0ac69be

📥 Commits

Reviewing files that changed from the base of the PR and between 7ea2b35 and d2cfdfe.

📒 Files selected for processing (9)
  • src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx
  • src/app/(mobile-ui)/qr-pay/page.tsx
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
  • src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx
  • src/app/(mobile-ui)/withdraw/page.tsx
  • src/components/Request/__tests__/request-states.test.tsx
  • src/components/Send/link/views/Initial.link.send.view.tsx
  • src/hooks/wallet/useWallet.ts

Comment thread src/app/(mobile-ui)/qr-pay/page.tsx Outdated
@Hugo0

Hugo0 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Heads-up: 2nd commit expands the scope of this PR

Pushed d2cfdfeda on top of the original display fix. I've updated the PR description with full detail — short version here so reviewers/@abalinda aren't surprised by the extra files.

Why the scope grew. The original one-line fix (show spendableBalance on /request) corrected a symptom. The same "reads the wrong balance field" bug also lives in the affordability gates of the legacy money-flows. The displayed spendable balance includes in-transit card-collateral top-ups (#2170, so it doesn't crater to $0 during the ~10–45s smart→collateral handoff), but Send/link, qr-pay and bank/manteca withdraw validate the entered amount against that display number rather than the available-now hasSufficientSpendableBalance() predicate the newer features/payments/flows already use. During a card top-up that can green-light a spend whose funds aren't routable yet (fails at execution instead of being blocked at input).

What the commit does. Routes those gates through the shared predicate and exposes availableSpendableBalance for the withdraw ceiling. Displayed balances are unchanged. Outside the top-up window available == display, so behaviour is identical — it only tightens the rare in-transit window.

Tests. Revived the qr-pay insufficient-balance test (was .skipped on mock drift) + added a request-view regression. typecheck ✅, prettier ✅, affected jest suites ✅.

One by-design nuance to be aware of: during a top-up the displayed balance can read higher than the gate allows for ~30s (self-heals once collateral lands) — consistent with useWallet's documented intent.

- qr-pay: drop the now-unused `sendMoney` destructuring (was pre-existing dead
  binding on the line I touched; flagged as a no-unused-vars lint failure).
- request tests: mirror the spendable-balance regression for the direct-request
  entry view (the report + fix covered both /request views, only one was locked).
  Renders in the loading state — printableUsdc runs in a useMemo before the early
  return, so the field-choice assertion fires without the full form harness.
…display formatting

Two DRY follow-ups to the gate fix, both single-sourced in the wallet hook:

1. Messaging — useWallet.spendBlockReason(amount) classifies a blocked spend as
   'settling' (covered by the displayed balance but part is still mid-rebalance:
   <= display, > available-now) vs 'insufficient' (exceeds the displayed total).
   The five legacy gates (send-link, qr-pay, withdraw page/bank/manteca) map that
   to one shared SPEND_BLOCK_MESSAGE instead of four bespoke strings. The
   'settling' copy is deliberately generic ("Your balance is updating...") so it
   never exposes the card-collateral mechanic, and it only shows in the rare
   ~10-45s in-transit window: in the 99% case display == available so only the
   normal "insufficient" path is reachable.

2. Display formatting — send-link, withdraw page/manteca and both request views
   render the hook's formattedSpendableBalance instead of re-deriving locally
   (printableUsdc vs formatAmount diverged: commas vs none). One formatter,
   consistent across screens; orphaned imports removed.

Displayed balance is unchanged: always the full spendable total (smart +
collateral, incl. in-transit). Only the spend gate is strict, and only briefly.
hasSufficientSpendableBalance stays for the features/payments flows; both share a
parseUsdToBaseUnits helper. Tests revived/extended; full unit suite green.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/`(mobile-ui)/withdraw/manteca/page.tsx:
- Line 691: The walletBalance prop assignment on line 691 uses a truthy guard on
the balance variable, which causes the balance text to be hidden when balance is
0n (a valid falsy value). Replace the truthy check with an explicit
null/undefined check so that falsy but valid values like 0n are still passed
through to the walletBalance prop. Change the condition from checking if balance
is truthy to explicitly checking if balance is not null and not undefined.

In `@src/components/Send/link/views/Initial.link.send.view.tsx`:
- Around line 138-147: The spend block validation using spendBlockReason is only
applied in the effect to set error state display, but the handleOnNext function
does not re-check the block before calling createLink, allowing blocked
transactions to be submitted. Add a spend block validation check at the
beginning of the handleOnNext function that calls spendBlockReason with the
current tokenValue, and if a block reason is returned, prevent the function from
proceeding to createLink. This ensures the spend block is enforced at submission
time, not just displayed as a visual error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 09211471-97e8-4f23-8b04-751699090eb4

📥 Commits

Reviewing files that changed from the base of the PR and between d2cfdfe and 8541340.

📒 Files selected for processing (12)
  • src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx
  • src/app/(mobile-ui)/qr-pay/page.tsx
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
  • src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx
  • src/app/(mobile-ui)/withdraw/page.tsx
  • src/components/Request/__tests__/request-states.test.tsx
  • src/components/Request/direct-request/views/Initial.direct.request.view.tsx
  • src/components/Request/link/views/Create.request.link.view.tsx
  • src/components/Send/link/views/Initial.link.send.view.tsx
  • src/hooks/wallet/useWallet.ts
  • src/utils/balance.utils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/hooks/wallet/useWallet.ts

Comment thread src/app/(mobile-ui)/withdraw/manteca/page.tsx Outdated
Comment thread src/components/Send/link/views/Initial.link.send.view.tsx Outdated
CodeRabbit review on the messaging/format commit:
- Send-link: the Retry button isn't disabled on a balance error (unlike the
  other flows, which disable their submit), so handleOnNext could reach
  createLink with a blocked amount and fail at execution. Re-check
  spendBlockReason at the top of handleOnNext so the block holds at submit too.
- withdraw/manteca: use an explicit `balance !== undefined` guard for the
  displayed balance so a real $0 balance shows "$0.00" instead of being hidden,
  matching the other consolidated flows (the old truthy guard dropped 0n).
@Hugo0

Hugo0 commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Course-correction: simplify to fail-late + refetch (dropping the input-gate tightening)

After review, the earlier "gate on available-now" approach (blocking in-transit spends at input) was the wrong call, and I'm reverting it. Reasoning:

  • The FE balance lags up to ~30s (useBalance and useRainCardOverview both poll at staleTime/refetchInterval: 30s), but the spend routing reads the live on-chain balance at submit (fix(card): route spends on live on-chain smart balance (fixes #2230 incident, keeps smart-first) #2234). So gating at input on the stale FE number can block a spend that would actually succeed live.
  • Blocking early also defeats the natural overlap where the ~15s flow time lets the ~10–45s collateral rebalance settle — by submit, the funds have often landed.

So the right model is fail-late: gate only on the displayed balance (block when the amount exceeds the full visible balance — a true shortfall), let everything else proceed, and rely on live execution.

What this commit does (net simpler than before):

  1. Gates back to the displayed balance via the single hasSufficientSpendableBalance predicate — deletes the spendBlockReason / availableSpendableBalance / SPEND_BLOCK_MESSAGE "settling at input" apparatus (and with it the Rain-mechanic copy).
  2. Refetch-on-failure (TanStack): on InsufficientSpendableError the routing already re-read live smart balance, so we just invalidate [RAIN_CARD_OVERVIEW_QUERY_KEY] at the two throw sites — the FE de-stales immediately instead of waiting out the 30s poll.
  3. Informative failure copy: the post-gate failure (in-transit not yet landed) now says "Your balance isn't fully available yet — please try again in a few seconds," centralized in one constant, instead of a misleading "add funds."
  4. Keeps the genuinely good parts: Aleks's display fix + the formattedSpendableBalance formatting consolidation.

Followed by a full /code-review pass over the assumptions and flows.

…op settling-at-input)

Gating money-flows at input on an "available-now" subset was wrong: the FE
balance is only ~30s-polled while the spend routing reads the chain live at
submit (#2234), so an input-time available-now gate blocks spends that would
actually succeed — and the ~15s flow naturally lets the ~10-45s collateral
rebalance settle. So gate on the DISPLAYED balance (block only a true shortfall)
and let in-transit spends fail late on live execution.

- useWallet: hasSufficientSpendableBalance now gates on the displayed
  spendableBalance. Deletes spendBlockReason / availableSpendableBalance /
  SPEND_BLOCK_MESSAGE and the whole "settling at input" apparatus (−55 net).
- Refetch on failure (TanStack): on InsufficientSpendableError the routing
  already re-read live smart balance, so the two bundle hooks
  (useSpendBundle / useSignSpendBundle) invalidate [RAIN_CARD_OVERVIEW_QUERY_KEY]
  before throwing — the displayed balance + a retry de-stale immediately instead
  of waiting out the 30s poll.
- Informative failure copy: the post-gate in-transit failure now says "Your
  balance isn't fully available yet — try again in a few seconds" (one shared
  BALANCE_SETTLING_MESSAGE) across send (ErrorHandler), qr-pay, manteca,
  useSendMoney — instead of a misleading "add funds".
- Keeps the display-field fix + the formattedSpendableBalance formatting
  consolidation. Net -39 lines.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/`(mobile-ui)/withdraw/page.tsx:
- Around line 84-91: Treat the wallet balance as “unknown” while useWallet() is
still loading instead of defaulting maxDecimalAmount to 0 in WithdrawPage.
Update the max-balance calculation in the withdraw page component so the
insufficient-balance check is skipped until spendableBalance is actually
resolved, and keep the Continue flow from showing the false “Add funds” error
during loading. Use the existing useWallet(), maxDecimalAmount, and the balance
validation branch around the Continue handler to locate the fix.

In `@src/components/Send/link/views/Initial.link.send.view.tsx`:
- Around line 153-160: The balance check in Initial.link.send.view.tsx is
clearing errorState too broadly, which can erase submit-time failures when
loading returns to idle. Update the logic around hasSufficientSpendableBalance
and setErrorState so only the insufficient-balance message is added/removed, and
preserve any existing non-balance errorMessage set during submit handling. Use
the existing error state transitions in the send view to keep late-failure
messaging visible until explicitly cleared by the relevant flow.

In `@src/hooks/wallet/useWallet.ts`:
- Around line 266-278: The hasSufficientSpendableBalance callback in useWallet
should reject non-finite amounts before converting to BigInt, because parseFloat
can produce Infinity/NaN and make Math.floor(...) throw. Update the amount
validation in this function to return false when the parsed value is not finite
(alongside the existing negative check) before computing amountInBaseUnits, so
the spendability guard safely fails instead of crashing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 57e78e68-1b7e-43e1-8d94-7eb856d81af0

📥 Commits

Reviewing files that changed from the base of the PR and between d466312 and ec95c9d.

📒 Files selected for processing (13)
  • src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx
  • src/app/(mobile-ui)/qr-pay/page.tsx
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
  • src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx
  • src/app/(mobile-ui)/withdraw/page.tsx
  • src/components/Send/link/views/Initial.link.send.view.tsx
  • src/hooks/wallet/useSendMoney.ts
  • src/hooks/wallet/useSignSpendBundle.ts
  • src/hooks/wallet/useSpendBundle.ts
  • src/hooks/wallet/useWallet.ts
  • src/utils/balance.utils.ts
  • src/utils/friendly-error.utils.tsx

Comment thread src/app/(mobile-ui)/withdraw/page.tsx
Comment thread src/components/Send/link/views/Initial.link.send.view.tsx
Comment thread src/hooks/wallet/useWallet.ts Outdated
Hugo0 added 2 commits June 23, 2026 23:20
… docs, test)

Verified findings from the high-effort review pass:

- Send handleOnNext: gate only once balance has loaded (`balance !== undefined`),
  else a tap before the query resolves false-rejected with "not enough balance"
  (hasSufficientSpendableBalance returns false on undefined).
- useSendMoney.onError: invalidate ['balance', address] after the optimistic
  rollback — the rollback was discarding the fresh live balance useSpendBundle
  fetched mid-flight, leaving the smart portion stale until the next 30s poll.
- contribute-pot RequestPotActionList: gate the loading-flash on
  isFetchingSpendableBalance (smart + Rain overview), not isFetchingBalance
  (smart only) — the latter flashed a false "insufficient" for split-funds users
  while the overview loaded.
- Extracted the gate to a pure, exported `isDisplayBalanceSufficient` and unit-
  tested the gate-on-display contract (CONTRIBUTING: hooks that gate need a test).
- Fixed stale JSDoc that still claimed the gate runs on available-now
  (formattedSpendableBalance note, computeAvailableSpendable/DisplaySpendable),
  and scoped the "shared copy" comment to send/pay/withdraw.

Not changed (by design / accepted): gate widened to fail-late for the
features/payments flows (intended); manteca float-rounding at the boundary
fails-safe; the cosmetic toast+inline double-surface on a sendMoney settling
failure (follow-up).
…unts

BigInt(Math.floor(Infinity * 1e6)) throws a RangeError; a pasted/oversized amount
(parseFloat -> Infinity) must fail the gate, not crash the render. isNaN didn't
catch Infinity; use Number.isFinite. + tests.
…nd, precision)

Max-effort worst-case review found two real bugs I'd introduced plus robustness gaps:

- [money] Orphan charges: the features/payments flows (direct-send, contribute-pot,
  semantic-request) createCharge BEFORE sendMoney, so widening their gate to the
  displayed total let an in-transit amount pass, create a backend charge, then fail
  late — an unpaid charge per retry. Restore the meaningful split:
  hasSufficientSpendableBalance now gates on AVAILABLE-NOW (those charge-first flows),
  while the no-pre-charge flows (send-link, qr-pay, withdraw) gate on the DISPLAYED
  total via the renamed pure helper isAmountWithinBalance(amount, balance).
- [stuck] qr-pay Pay + manteca Withdraw buttons dead-ended after a settling failure:
  the settling message made isBlockingError / disabled true with no way to clear, while
  the copy says "try again". Exempt BALANCE_SETTLING_MESSAGE so the retry button stays live.
- [precision/crash] Gate now parses the amount with parseUnits (exactly what the spend
  uses) instead of float Math.floor(amount*1e6): kills the boundary divergence AND fails
  closed (returns false, never a BigInt(Infinity) RangeError) on adversarial input.
- [load] withdraw page no longer false-blocks ("insufficient") during the balance-load
  window (maxDecimalAmount=0); guarded on a loaded balance, re-validates when it lands.
- [robustness] ErrorHandler matches the typed error name, not just the message string.
- [cleanup] removed the orphaned PEANUT_WALLET_TOKEN_DECIMALS import in bank/page.

Tests: pure isAmountWithinBalance (incl. Infinity/overflow), useSendMoney onError refetch.
Full unit suite green.
…ring load

CodeRabbit on the adversarial-fix commit:
- Send-link (Major): the balance effect cleared errorState on every sufficient-
  balance pass, wiping a submit-time failure message (e.g. the settling copy) the
  moment loading returned to idle. Now it only clears OUR balance-gate error
  (INSUFFICIENT_BALANCE_MESSAGE), never a handleOnNext failure message.
- withdraw page (Minor): isContinueDisabled used maxDecimalAmount (=0 while the
  balance loads), disabling Continue during the load window. Guard it on a loaded
  balance, matching the validateAmount fix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants