Skip to content

fix: exempt crypto withdrawals from the fiat $1 minimum#2433

Merged
jjramirezn merged 2 commits into
mainfrom
fix/crypto-withdraw-no-fiat-min
Jul 16, 2026
Merged

fix: exempt crypto withdrawals from the fiat $1 minimum#2433
jjramirezn merged 2 commits into
mainfrom
fix/crypto-withdraw-no-fiat-min

Conversation

@abalinda

@abalinda abalinda commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Crypto withdrawals were blocked below $1 by the shared withdraw amount step: minUsdAmount derives a country-based bank minimum via getMinimumAmount(), and crypto (no country) fell into the $1 default. The minimum was introduced for bank offramps (295c8ec, June 2025 — Bridge's $1 floor) and crypto only inherited it by sharing the page. Inconsistent with send-via-link, which allows sub-$1 amounts with the same sponsored gas.

Fix: minUsdAmount is 0 when the withdrawal is crypto — any amount > 0 passes. Bank rails keep their country minimums (US/EU $1, GB £3, MX 50 MXN) untouched.

Review hardening (multi-agent review on the first cut):

  • Exemption follows routing. The crypto predicate follows selectedMethod when set (selectedMethod ? selectedMethod.type === 'crypto' : isCryptoFromSend): a stale bank method surviving in the app-wide WithdrawFlowContext (entered via /withdraw?method=crypto) still routes Continue to the bank flow, so it keeps the bank minimum. Regression test included.
  • Cross-chain floor restored where it's real. Removing the blanket $1 exposed the Rhino SDA path to sub-minimum deposits — accepted on-chain, never bridged (stranded, uncredited). The confirm CTA now blocks when payAmount < minDepositLimitUsd (value the page previously discarded), with unit-tested helper isBelowRhinoMinDeposit. Direct same-chain USDC transfers keep no minimum.
  • The two other inline type !== 'crypto' checks (limits card, Continue limits gate) consolidated onto the same predicate; exchange-rate polling now disabled for crypto.

Design notes / accepted trade-offs

  • Cross-chain dust fees can exceed principal (e.g. ~$1.50 flat fee on a $0.10 mainnet withdrawal). The existing preview + non-blocking high-fee warning disclose this honestly; adding a proportionality block is a product decision deliberately left out.
  • Follow-ups (pre-existing, not this PR): getMinimumAmount('') silently returns the $1 default for an unknown country — the next countryless method re-arms the trap; the minUsdAmount computation (country derivation + FX conversion + exemption) is a candidate for an extracted, tested hook — add-money already re-implements it with a different FX hook.

Risks / breaking changes

  • FE-only; no backend or cross-repo impact (BE enforces no minimum on this path).
  • Dust withdrawals cost sponsored paymaster gas — same exposure sub-$1 links already have.
  • Targets main per request → back-merge debt main→dev after merge.

QA

  • withdraw-states: sub-$1 crypto enables Continue and routes to /withdraw/crypto; sub-$1 bank stays blocked with the $1 message; stale-bank-method + ?method=crypto stays blocked.
  • withdraw.utils: isBelowRhinoMinDeposit unit tests (below/at/above/unknown values).
  • Full jest suite green locally (127 suites / 1842 tests).

The shared withdraw amount step applied getMinimumAmount() to every
method; crypto has no country so it fell into the $1 default — blocking
sub-$1 on-chain sends that send-via-link already allows. The minimum
was introduced for bank offramps (295c8ec, Bridge's $1 floor) and
crypto only inherited it by sharing the page. Bank rails keep their
country minimums.
@vercel

vercel Bot commented Jul 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 Jul 16, 2026 4:12pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@abalinda, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 5 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8655436d-da5f-4eef-aa7b-36aed17a7772

📥 Commits

Reviewing files that changed from the base of the PR and between 80a5a24 and 7edcf09.

📒 Files selected for processing (6)
  • src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx
  • src/app/(mobile-ui)/withdraw/page.tsx
  • src/components/Withdraw/views/Confirm.withdraw.view.tsx
  • src/utils/__tests__/withdraw.utils.test.ts
  • src/utils/withdraw.utils.ts

Walkthrough

Changes

Withdrawal amount validation

Layer / File(s) Summary
Crypto exemption and validation regression coverage
src/app/(mobile-ui)/withdraw/page.tsx, src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx
Crypto withdrawals use a zero minimum amount, while bank withdrawals continue enforcing the $1 minimum; tests cover navigation, disabled state, error reporting, and blocked navigation.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested labels: enhancement

Suggested reviewers: hugo0

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly summarizes the main change: crypto withdrawals are exempted from the fiat $1 minimum.
Description check ✅ Passed The description is directly related to the withdrawal minimum validation changes and added test coverage.

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 Jul 16, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6122.12 → 6123.14 (+1.02)
Findings: 0 net (+27 new, -27 resolved)

🆕 New findings (27)

  • critical complexity — src/app/(mobile-ui)/withdraw/page.tsx — CC 125, MI 53.75, SLOC 339
  • critical complexity — src/app/(mobile-ui)/withdraw/crypto/page.tsx — CC 79, MI 53.64, SLOC 335
  • critical complexity — src/utils/withdraw.utils.ts — CC 70, MI 53.78, SLOC 250
  • high complexity — src/components/Withdraw/views/Confirm.withdraw.view.tsx — CC 44, MI 62.05, SLOC 42
  • medium react-long-component — src/app/(mobile-ui)/withdraw/crypto/page.tsx:42 — WithdrawCryptoPage is 527 lines — split it
  • medium react-long-component — src/app/(mobile-ui)/withdraw/page.tsx:28 — WithdrawPage is 449 lines — split it
  • medium high-mdd — src/app/(mobile-ui)/withdraw/page.tsx:28 — WithdrawPage: MDD 104.5 (uses across many lines from declarations)
  • medium high-mdd — src/app/(mobile-ui)/withdraw/crypto/page.tsx:42 — WithdrawCryptoPage: MDD 91.7 (uses across many lines from declarations)
  • medium high-dlt — src/app/(mobile-ui)/withdraw/crypto/page.tsx:42 — WithdrawCryptoPage: DLT 51 (calls 51 distinct functions — high context load)
  • medium high-mdd — src/components/Withdraw/views/Confirm.withdraw.view.tsx:67 — ConfirmWithdrawView: MDD 51.2 (uses across many lines from declarations)
  • medium method-complexity — src/app/(mobile-ui)/withdraw/page.tsx:28 — WithdrawPage CC 28 SLOC 111
  • medium method-complexity — src/components/Withdraw/views/Confirm.withdraw.view.tsx:67 — ConfirmWithdrawView CC 28 SLOC 21
  • medium high-mdd — src/app/(mobile-ui)/withdraw/crypto/page.tsx:277 — : MDD 23.6 (uses across many lines from declarations)
  • medium method-complexity — src/app/(mobile-ui)/withdraw/page.tsx:274 — CC 17 SLOC 55
  • medium method-complexity — src/app/(mobile-ui)/withdraw/crypto/page.tsx:277 — CC 16 SLOC 82
  • medium react-effect-derives-state — src/app/(mobile-ui)/withdraw/crypto/page.tsx:122 — small useEffect that only sets state from deps
  • medium react-effect-derives-state — src/app/(mobile-ui)/withdraw/crypto/page.tsx:130 — small useEffect that only sets state from deps
  • medium react-effect-derives-state — src/app/(mobile-ui)/withdraw/page.tsx:178 — useEffect with empty deps + setState — derived state anti-pattern
  • low high-mdd — src/app/(mobile-ui)/withdraw/page.tsx:274 — handleAmountContinue: MDD 17.3 (uses across many lines from declarations)
  • low high-mdd — src/app/(mobile-ui)/withdraw/crypto/page.tsx:166 — : MDD 16.4 (uses across many lines from declarations)

…and 7 more.

✅ Resolved (27)

  • src/app/(mobile-ui)/withdraw/page.tsx — CC 122, MI 53.88, SLOC 336
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx — CC 76, MI 53.34, SLOC 329
  • src/utils/withdraw.utils.ts — CC 66, MI 53.52, SLOC 242
  • src/components/Withdraw/views/Confirm.withdraw.view.tsx — CC 40, MI 62.27, SLOC 42
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:41 — WithdrawCryptoPage is 514 lines — split it
  • src/app/(mobile-ui)/withdraw/page.tsx:28 — WithdrawPage is 443 lines — split it
  • src/app/(mobile-ui)/withdraw/page.tsx:28 — WithdrawPage: MDD 102.0 (uses across many lines from declarations)
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:41 — WithdrawCryptoPage: MDD 89.1 (uses across many lines from declarations)
  • src/components/Withdraw/views/Confirm.withdraw.view.tsx:61 — ConfirmWithdrawView: MDD 51.2 (uses across many lines from declarations)
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:41 — WithdrawCryptoPage: DLT 50 (calls 50 distinct functions — high context load)
  • src/app/(mobile-ui)/withdraw/page.tsx:28 — WithdrawPage CC 26 SLOC 110
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:275 — : MDD 23.6 (uses across many lines from declarations)
  • src/components/Withdraw/views/Confirm.withdraw.view.tsx:61 — ConfirmWithdrawView CC 24 SLOC 21
  • src/app/(mobile-ui)/withdraw/page.tsx:266 — CC 17 SLOC 55
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:275 — CC 16 SLOC 82
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:120 — small useEffect that only sets state from deps
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:128 — small useEffect that only sets state from deps
  • src/app/(mobile-ui)/withdraw/page.tsx:170 — useEffect with empty deps + setState — derived state anti-pattern
  • src/app/(mobile-ui)/withdraw/page.tsx:266 — handleAmountContinue: MDD 17.3 (uses across many lines from declarations)
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:164 — : MDD 16.4 (uses across many lines from declarations)

…and 7 more.

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 1845 ran, 0 failed, 0 skipped, 28.1s

📊 Coverage (unit)

metric %
statements 58.3%
branches 42.3%
functions 47.1%
lines 58.4%
⏱ 10 slowest test cases
time test
3.7s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
0.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in updateUserById body
0.2s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in updateUserById
0.2s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › keeps stickers off the username pill (final pass respects the keep-out)
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 valid ETH address in lowercase
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 maximum length (17 digits) US account
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

…m Rhino deposits

Review findings on the initial cut:

- The exemption keyed off the URL param independently of routing: a stale
  bank selectedMethod (app-wide context) entering via /withdraw?method=crypto
  got min=$0 while Continue still routed to the bank flow. The predicate now
  follows selectedMethod when set; the URL only covers the pre-effect frame.
- Removing the blanket $1 floor exposed the cross-chain path to sub-minimum
  Rhino SDA deposits, which are accepted on-chain but never bridged (stranded,
  uncredited — known incident class). The confirm CTA now blocks below the
  route's minDepositLimitUsd, which the page previously discarded.
- Consolidated the two other inline is-crypto checks onto the same predicate,
  stopped the exchange-rate poll when crypto is selected, dropped a vacuous
  test assertion.
@abalinda

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@abalinda
abalinda marked this pull request as ready for review July 16, 2026 16:13
@abalinda

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@jjramirezn
jjramirezn merged commit 40887a6 into main Jul 16, 2026
24 of 26 checks passed
@abalinda

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.


Your plan includes PR reviews subject to rate limits. More reviews will be available in 5 minutes.

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.

2 participants