Skip to content

feat: chain-aware ENS resolution + visible resolved address in withdraw#2501

Merged
jjramirezn merged 4 commits into
devfrom
feat/ens-withdraw-resolved-address
Jul 24, 2026
Merged

feat: chain-aware ENS resolution + visible resolved address in withdraw#2501
jjramirezn merged 4 commits into
devfrom
feat/ens-withdraw-resolved-address

Conversation

@0xkkonrad

@0xkkonrad 0xkkonrad commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Companion to peanut-api-ts#1236. External tester feedback on the withdraw flow's ENS handling:

  1. Wrong-chain resolution (funds-loss risk): ENS names can hold a distinct address per chain (ENSIP-11). We resolved every name to its mainnet record and sent on the selected chain — test.ses.eth on Arbitrum went to its Ethereum address. The flow now passes the destination chainId through resolveEns/ens/:name?chainId=, and switching chains after typing a name re-resolves it for the new chain (failure invalidates the input instead of keeping the stale address).
  2. Resolved address invisible until after the warning: the resolved 0x… now renders under the input the moment the name validates ("name resolves to 0x1234…abcd"), and the "Is this address compatible?" modal shows the concrete destination address.

Risks / breaking changes

  • Deploy order: API first. Until fix: apply zerodev patching #1236 is live, the extra chainId query param is ignored by the old route schema → behavior identical to today (mainnet record). No hard dependency, no breakage either way.
  • GeneralRecipientInput gains an optional chainId prop — all other consumers unchanged (prop omitted = today's behavior).
  • Non-EVM (Solana/Tron) paths untouched: chain-change re-resolution only runs for the EVM family.

QA

  • src/lib/validation/recipient.test.ts: chainId forwarded to resolveEns; legacy callers resolve without it.
  • Full suite: 149 suites / 2038 tests green; typecheck clean.
  • Screenshots below (sandbox harness: worktree UI + local API + real JustaName resolution; on-chain balance read stubbed — harness EOA is unfunded, orthogonal to this change).

🤖 Generated with Claude Code

Screenshots

Resolved address under input (new) Compatibility modal with destination (new)
resolved modal

Assets branch pr-assets-2501 is deleted post-merge.

ENS names can hold a different address per chain (ENSIP-11). The
withdraw flow resolved every name to its mainnet record and sent on
the selected chain — funds loss when the name points elsewhere on
that chain (reported by an external ENS-savvy tester with
test.ses.eth: mainnet and Arbitrum records differ).

- resolveEns/validateAndResolveRecipient accept the destination
  chainId and forward it to /ens/:name?chainId= (api-ts #1236).
- Switching chains after typing a name re-resolves it for the new
  chain instead of silently keeping the old address.
- The resolved address is now shown under the input as soon as the
  name validates, and again in the compatibility warning modal — the
  user sees where funds go BEFORE any warning/confirm step.
@vercel

vercel Bot commented Jul 24, 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 24, 2026 5:33pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 34c7ae11-fbfc-4d22-bc57-239afdeac25c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ens-withdraw-resolved-address

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 24, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6265.6 → 6268.24 (+2.64)
Findings: +2 net (+20 new, -18 resolved)

🆕 New findings (20)

  • critical complexity — src/app/(mobile-ui)/withdraw/crypto/page.tsx — CC 89, MI 52.01, SLOC 376
  • high complexity — src/components/Withdraw/views/Initial.withdraw.view.tsx — CC 43, MI 58.63, SLOC 134
  • high hotspot — src/app/(mobile-ui)/withdraw/crypto/page.tsx — 31 commits, +400/-219 lines since 6 months ago
  • medium react-long-component — src/app/(mobile-ui)/withdraw/crypto/page.tsx:43 — WithdrawCryptoPage is 614 lines — split it
  • medium high-mdd — src/app/(mobile-ui)/withdraw/crypto/page.tsx:43 — WithdrawCryptoPage: MDD 105.1 (uses across many lines from declarations)
  • medium high-dlt — src/app/(mobile-ui)/withdraw/crypto/page.tsx:43 — WithdrawCryptoPage: DLT 56 (calls 56 distinct functions — high context load)
  • medium high-mdd — src/components/Global/GeneralRecipientInput/index.tsx:38 — GeneralRecipientInput: MDD 42.8 (uses across many lines from declarations)
  • medium high-mdd — src/components/Withdraw/views/Initial.withdraw.view.tsx:27 — InitialWithdrawView: MDD 33.5 (uses across many lines from declarations)
  • medium complexity — src/components/Global/GeneralRecipientInput/index.tsx — CC 22, MI 50.57, SLOC 100
  • medium high-mdd — src/components/Global/GeneralRecipientInput/index.tsx:54 — : MDD 20.2 (uses across many lines from declarations)
  • medium complexity — src/lib/validation/recipient.ts — CC 17, MI 55.61, SLOC 75
  • medium method-complexity — src/components/Withdraw/views/Initial.withdraw.view.tsx:27 — InitialWithdrawView CC 16 SLOC 49
  • medium complexity — src/app/actions/ens.ts — CC 6, MI 63.4, SLOC 17
  • medium react-effect-derives-state — src/components/Withdraw/views/Initial.withdraw.view.tsx:67 — small useEffect that only sets state from deps
  • medium react-effect-derives-state — src/components/Withdraw/views/Initial.withdraw.view.tsx:153 — useEffect with empty deps + setState — derived state anti-pattern
  • low high-dlt — src/components/Withdraw/views/Initial.withdraw.view.tsx:27 — InitialWithdrawView: DLT 23 (calls 23 distinct functions — high context load)
  • low high-mdd — src/lib/validation/recipient.ts:13 — validateAndResolveRecipient: MDD 19.8 (uses across many lines from declarations)
  • low high-dlt — src/components/Global/GeneralRecipientInput/index.tsx:38 — GeneralRecipientInput: DLT 16 (calls 16 distinct functions — high context load)
  • low high-mdd — src/components/Global/GeneralRecipientInput/index.tsx:117 — : MDD 12.7 (uses across many lines from declarations)
  • low missing-return-type — src/components/Withdraw/views/Initial.withdraw.view.tsx:27 — InitialWithdrawView: exported fn missing return type annotation

✅ Resolved (18)

  • src/app/(mobile-ui)/withdraw/crypto/page.tsx — CC 88, MI 52.06, SLOC 375
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx — 30 commits, +380/-217 lines since 6 months ago
  • src/components/Withdraw/views/Initial.withdraw.view.tsx — CC 30, MI 58.4, SLOC 98
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:43 — WithdrawCryptoPage is 596 lines — split it
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:43 — WithdrawCryptoPage: MDD 105.0 (uses across many lines from declarations)
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:43 — WithdrawCryptoPage: DLT 55 (calls 55 distinct functions — high context load)
  • src/components/Global/GeneralRecipientInput/index.tsx:35 — GeneralRecipientInput: MDD 40.1 (uses across many lines from declarations)
  • src/components/Withdraw/views/Initial.withdraw.view.tsx:26 — InitialWithdrawView: MDD 27.1 (uses across many lines from declarations)
  • src/components/Global/GeneralRecipientInput/index.tsx — CC 22, MI 50.62, SLOC 100
  • src/lib/validation/recipient.ts — CC 17, MI 55.63, SLOC 75
  • src/components/Withdraw/views/Initial.withdraw.view.tsx:66 — small useEffect that only sets state from deps
  • src/components/Withdraw/views/Initial.withdraw.view.tsx:116 — useEffect with empty deps + setState — derived state anti-pattern
  • src/components/Global/GeneralRecipientInput/index.tsx:50 — : MDD 18.6 (uses across many lines from declarations)
  • src/components/Withdraw/views/Initial.withdraw.view.tsx:26 — InitialWithdrawView: DLT 19 (calls 19 distinct functions — high context load)
  • src/lib/validation/recipient.ts:13 — validateAndResolveRecipient: MDD 18.5 (uses across many lines from declarations)
  • src/components/Global/GeneralRecipientInput/index.tsx:35 — GeneralRecipientInput: DLT 16 (calls 16 distinct functions — high context load)
  • src/components/Global/GeneralRecipientInput/index.tsx:108 — : MDD 12.7 (uses across many lines from declarations)
  • src/components/Withdraw/views/Initial.withdraw.view.tsx:26 — InitialWithdrawView: exported fn missing return type annotation

📈 Painscore deltas (top movers)

File Before After Δ
src/app/actions/ens.ts 5.3 6.7 +1.4
src/components/Withdraw/views/Initial.withdraw.view.tsx 10.7 11.4 +0.6

@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2137 ran, 0 failed, 0 skipped, 27.6s

📊 Coverage (unit)

metric %
statements 60.8%
branches 44.3%
functions 50.0%
lines 61.2%
⏱ 10 slowest test cases
time test
2.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
0.9s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
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/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.2s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.2s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
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/utils/__tests__/url.utils.test.ts › uses the public BASE_URL in Capacitor, not the localhost WebView origin
0.1s src/utils/__tests__/demo-balance.test.ts › keeps a spent-down balance across a cold start within the TTL
0.1s src/utils/__tests__/demo-balance.test.ts › auto-refills a wallet older than the TTL on cold start
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@0xkkonrad

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 24, 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 and others added 2 commits July 24, 2026 14:23
Conflict in Initial.withdraw.view.tsx: dev added a chain-switch effect that
clears chain-scoped errors at the same spot as this branch's ENS re-resolve
effect. Kept both as separate effects (separate prev-chain refs) — merging
them would put error.showError in the re-resolve effect's deps and its
cleanup would abort in-flight resolutions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…flight

Between switching chains and the new resolution landing, recipient.address
still holds the previous chain's address and Review stayed clickable —
the exact wrong-chain send this PR fixes, in a narrow race window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jjramirezn

Copy link
Copy Markdown
Contributor

Reviewed (Jota, via Claude Code). Two things pushed to the branch:

  1. Merged dev — conflict with the chain-switch error-clearing effect from feat(invites): root-domain campaign links + stackable multi-campaign awarding #2503's branch line in Initial.withdraw.view.tsx. Kept both effects separate (separate prev-chain refs) rather than combining: the error-clearer's error.showError dep would abort an in-flight re-resolution through the cleanup.
  2. Race fix (679c359): between switching chains and the re-resolution landing, recipient.address still held the old chain's address and Review stayed clickable — the exact wrong-chain send this PR exists to prevent, in a small window. The effect now invalidates the recipient before resolving and revalidates on success.

Also verified: client-side unstable_cache keys on stringified args so chainId is part of the cache key (no cross-chain stale hits), non-EVM families short-circuit before the ENS path, and the repo-wide eslint failure was the same flake as on the API PR (re-run passed).

API #1236 is merged, so the ?chainId= param is live on dev. Merging once CI is green on the new head.

@jjramirezn
jjramirezn marked this pull request as ready for review July 24, 2026 17:25
crypto-withdraw-confirm.test.tsx (new on dev) mocks general.utils with only
the functions the page used at the time; the compatibility-modal change now
also renders printableAddress from that module.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jjramirezn
jjramirezn merged commit 46eb72c into dev Jul 24, 2026
18 of 20 checks passed
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.

2 participants