Skip to content

feat: Solana + Tron withdrawals + chain-expansion polish#2398

Draft
Hugo0 wants to merge 19 commits into
devfrom
feat/solana-tron-withdrawals
Draft

feat: Solana + Tron withdrawals + chain-expansion polish#2398
Hugo0 wants to merge 19 commits into
devfrom
feat/solana-tron-withdrawals

Conversation

@Hugo0

@Hugo0 Hugo0 commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Enables Solana (USDC+USDT) and Tron (USDT) as withdraw destinations — both verified against Rhino prod with live quotes + real outflow-SDA creates — plus the polish items from the #2396 rollout review.

How it works: the user-side transaction is unchanged (ERC20 transfer to a Rhino SDA on Arbitrum; Rhino delivers on the destination chain). The work is FE plumbing that was keyed on EVM chainIds:

  • Synthetic non-EVM chain records (nonEvmWithdraw.consts.ts) merged into the token selector in withdraw mode only — send/pay/claim surfaces and URL parsing keep their EVM+wagmi assumptions and never see them
  • Per-family recipient validation: Solana base58 / Tron base58check, family derived from the selected chain, never string-sniffed (every Tron address is also valid base58 in Solana's length range); switching family resets the entered address
  • chainIdToRhinoName handles slugs + EVM numeric ids on the withdraw path

Polish:

  • Linea logo fixed — chain-details ships an ipfs-hosted SVG that next/image refuses (rendered as 'LI' initials); CoinGecko raster via the existing CHAIN_ICON_OVERRIDES mechanism
  • Deposit fee note (~0.1%) — the crypto deposit flow previously showed no fee messaging at all
  • Kaia/Plasma deposit chips annotated 'USDT only' (+ Tempo/Celo/Gnosis annotations) — a USDC deposit on a USDT-only chain has no webhook; this properly retires the accepted-risk from feat: sync Rhino chain support with live catalogs (Avalanche + 8 more withdraw destinations, Tempo deposits) #2396
  • Cross-chain withdraw receipts fall back to the source-chain (Arbitrum) explorer when the destination has no explorer entry — Tempo receipts were linkless (the recorded hash is the Arbitrum tx anyway)
  • Withdraw view shell switched to the mandated flex/gap layout (space-y clipped the CTA on short viewports — Hugo's screenshot)

Risks / breaking changes

  • ⚠️ Deploy order: requires peanut-api-ts#1166 live first — the old destination-RPC delivery probe cannot verify non-EVM deliveries; the Rhino-native verification in fix: close kyc modal btn #1166 can.
  • Non-EVM entries are scoped to the withdraw selector via the existing restrictToRhino flag — verified that claim/send/pay filter by wagmi network ids and can't surface them.
  • CrossChainDestinationInfo.recipientAddress/tokenAddress widened from viem Address to string — the same-chain path (EVM-only by construction) narrows back with a cast; cross-chain tx construction never uses the recipient.

QA

  • Jest: 1,820 passing incl. new addressFamily suite (cross-family rejection, base58 alphabet edge cases)
  • typecheck 0 · prettier ✅ · next build
  • POC proof: quote + SDA create ARBITRUM→SOLANA and ARBITRUM→TRON against Rhino prod (2026-07-11)
  • Pending before ready-for-review: sandbox visual pass (selector entries, logos incl. Linea fix, drawer clipping on 375px, deposit annotations) + staging e2e $1 withdrawal to Solana after BE deploys

jjramirezn and others added 17 commits July 9, 2026 12:33
Prod release SP-150 — dev → main
Mixed spends from unmigrated pre-2025-09-18 accounts always reverted with
'Delegatecall failed': the SDK wraps their userOp in migrateWithCall, which
swaps the root validator v0.0.2->v0.0.3 BEFORE withdrawAsset verifies the
pre-signed (v0.0.2-routed) admin EIP-712 signature via ERC-1271. Proven by
on-chain simulation at the failing block: migration alone succeeds, the
withdrawal alone succeeds, combined they revert. 8 users / ~$1k currently
blocked, and since their wallets are empty they can never organically migrate.

Fix: before a mixed spend on an unmigrated account, fire the migration as a
standalone no-op userOp, then rebuild the kernel client so the admin sig is
signed AND verified under v0.0.3. The client cache is now ref-backed so the
rebuilt client reaches closures captured before the rebuild (grant flow), and
the admin EIP-712 payload is built in exactly one place for both spend paths.
…igration beat

useSignSpendBundle (qr-pay, Manteca withdraw, card cancel/lock refunds) has the
identical trap: it pre-signs the admin EIP-712 and a migration-wrapped userOp
the backend submits later, so unmigrated accounts revert the same way. Gate it
with the same ensureRootValidatorMigrated pre-flight, and swap its two verbatim
EIP-712 blocks for the shared builder (4 copies -> 1 across both hooks).

UX: the migration tap now runs under the existing security-verification overlay
(the same intentional 'Verifying security…' beat the mixed flow already uses),
gated on wrapper accounts only so the common path never flickers.
…learClients effect dep

Base had 2 eslint errors in useSpendBundle (restricted @/interfaces barrel +
unused PEANUT_WALLET_TOKEN_DECIMALS); both are one-liners inside this PR's
diff, so boy-scout them rather than ship a red-on-arrival file. Also adds the
new clearClients callback to its effect deps (stable identity, no behavior
change) so this PR introduces zero new lint findings.
… how bugs ship twice

The migration-ordering bug existed on BOTH spend engines (useSpendBundle and
useSignSpendBundle) precisely because their preflight sequences were parallel
copies — the original fix would have covered one surface and silently missed
qr-pay, Manteca withdrawals, and card cancel/lock refunds. Extract the entire
drift-prone sequence (live-balance routing -> insufficient rejection ->
migration gate -> grant gate) into spendPreflight.ts so the next gate is
physically impossible to add to only one engine. The engines keep only their
legitimately-different execution branches (broadcast vs sign-and-return).

Routing primitives + shared errors move with it; all importers updated, no
re-export shims. 8 new tests lock the orchestration order and gating.
…ndings

The big one (CONFIRMED): ERC-4337 semantics — a REVERTED userOp still yields
receipt.status='success' on the bundle tx, so the gate could declare a failed
migration successful and walk straight back into the 'Delegatecall failed'
revert. Migration is now verified against ground truth (the on-chain root
validator, short poll for propagation), never the receipt; a null receipt
(timeout) heals if the op actually landed. A reverted bundle throws a
deterministic KernelMigrationFailedError (no 'please retry' framing), and the
rebuilt client is asserted to be off the wrapper (lagging public RPC would
otherwise hand back a v0.0.2-signing wrapper) with one grace retry.

Cache races (CONFIRMED): builds now carry a per-chain monotonic sequence — a
stale build that resolves after logout or after a rebuild superseded it can no
longer clobber the cache (previous user's client post-logout / pre-migration
wrapper post-rebuild), and .finally only clears the dedupe slot it owns.

Also: fail-closed overview check hoisted ABOVE the migration tap (never charge
a passkey tap for a doomed flow), loadingState reset on the receipt-timeout
path, failure-capture parity for the sign-only engine (card_withdraw_failed
now emitted), and the card-recovery page's fourth copy of the Rain EIP-712
payload migrated to the shared builder.

Declined finding: 'permanent per-spend rebuild tax' — createKernelMigrationAccount
returns a plain (non-wrapper) v0.0.3 account once the chain shows the account
migrated, so the gate becomes a no-op after the first successful rebuild.
card-recovery's orphaned Address import was OURS (the typed-data builder swap
made it unused). The rest are pre-existing mechanical fixes in files this PR
already touches: qr-pay's four dead imports + the '@/context' barrel (test
mock repointed to the specific file), useWallet's '@/interfaces' barrel.
Deliberately NOT touched: the two 'any's and the unused transactionUsd —
those need type judgment, not cleanup, and belong to the backlog.
…xed-spend

fix: migrate root validator before mixed spends sign the Rain admin sig
/api/send-discord-notification forwarded arbitrary POST body.message
to DISCORD_WEBHOOK_URL with no auth, rate limit, or mention filtering —
actively abused to post @everyone into our Discord. Zero callers exist;
all real Discord alerting is server-side in peanut-api-ts.
…d-relay

fix: remove unauthenticated public Discord relay endpoint
Rhino's live bridge config supports 9 EVM withdraw destinations our
chainId→name mapping never listed, so users picking them got
'Unsupported Rhino chain mapping' (Avalanche was user-visible today:
it's in chain-details.json but had no mapping entry). Each addition
was verified against Rhino prod (getBridgeConfig status=enabled +
real ARBITRUM→X quote + outflow SDA create): AVALANCHE, HYPEREVM,
INK, KATANA, LINEA, MANTLE, PLASMA, STABLE, TEMPO. SCROLL removed —
Rhino disabled it and quotes now 400. KAIA/OPBNB excluded: SDA create
rejects their tokenOut (DepositAddressTokenOutNotSupported).

Deposit side: TEMPO added to SUPPORTED_EVM_CHAINS (SDA catalog lists
USDC+USDT, meeting the REQUIRED_TOKENS bar). KAIA/PLASMA stay off the
deposit list — USDT-only, and the EVM-family token list advertises
USDC, which Rhino would silently swallow on those chains.

chain-details/token-details entries use Rhino's authoritative token
addresses (getBridgeConfig) and chainid.network registry metadata;
USDT-only routes (PLASMA, STABLE) expose only USDT so the token
selector can't offer an unbridgeable pair.
The withdraw destination list is gated by
RHINO_WITHDRAW_SUPPORTED_TOKENS_BY_CHAIN ∩ the wagmi source-network
list — so the chainId→name mapping alone never surfaced the new
chains. Extend the gate with the 9 verified destinations (USDT-only
for Plasma/Stable), and derive the withdraw list from the gate's own
keys instead of the wagmi list: destinations need no wallet
connection or balance reads, and several deliverable chains
(Avalanche, Linea, Ink…) are intentionally not source chains.

Also exclude Scroll from the source selector: Rhino disabled it and
it isn't an SDA deposit chain, so every cross-chain route from it
dead-ends (completes the mapping removal in the previous commit).
CodeRabbit catch: with the withdraw list now derived from the Rhino
gate, the search path still scanned the wagmi source list — so
searching 'USDT' before picking a network omitted destination-only
chains (Linea, Avalanche, …).
Both are in Rhino's live SDA catalog (USDT-only) and the backend
already provisions inflow SDAs accepting them — only the FE list hid
them. The deposit UI advertises tokens per EVM family (incl. USDC),
so a USDC deposit on these chains relies on Rhino support to return
it — accepted risk (Hugo, 2026-07-10); per-chain token gating is the
proper fix, tracked as a follow-up.
…og-sync

feat: sync Rhino chain support with live catalogs (Avalanche + 8 more withdraw destinations, Tempo deposits)
Rhino delivers to Solana (USDC+USDT) and Tron (USDT) — verified with
live quotes and outflow-SDA creates. The user-side tx is unchanged (an
ERC20 transfer to the SDA on Arbitrum); what was missing was FE
plumbing keyed on EVM chainIds:
- synthetic non-EVM chain records merged into the withdraw selector
  ONLY (send/pay/claim keep their EVM+wagmi assumptions)
- per-family recipient validation (base58 / base58check), family from
  the SELECTED chain — never string-sniffed (every Tron address is
  also valid base58 in Solana's length range)
- chainIdToRhinoName handles slugs + EVM ids for the withdraw path

Polish from the #2396 rollout review:
- Linea logo: chain-details ships an ipfs SVG that next/image refuses
  → CHAIN_ICON_OVERRIDES raster (the Arbitrum mechanism)
- deposit fee note (~0.1%) — the crypto deposit flow showed no fee
  messaging at all
- Kaia/Plasma deposit chips annotated 'USDT only' (a USDC deposit
  there is Rhino-unrecoverable-by-webhook; removes the accepted-risk)
- cross-chain withdraw receipts: fall back to the source-chain
  explorer when the destination has none (Tempo receipts were
  linkless; the recorded hash is the Arbitrum tx anyway)
- withdraw view outer shell to flex/gap per the page-layout rules
  (space-y clipped the CTA on short viewports)

Depends on peanut-api-ts#1166 (Rhino-native delivery verification)
deploying first — the old destination-RPC probe can't verify non-EVM
deliveries.
@vercel

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

Request Review

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR centralizes spend preflight and kernel migration handling, adds Rain typed-data utilities, expands Rhino and non-EVM withdrawal support with rollout gating, updates recipient validation and transfer typing, and adjusts related UI, transaction, loading-state, and configuration integrations.

Changes

Spend preflight and migration

Layer / File(s) Summary
Rain withdrawal typed-data builder
src/utils/rainWithdraw.utils.ts, src/app/(mobile-ui)/card-recovery/page.tsx
Rain EIP-712 payload construction is centralized and reused by card recovery and spend-signing flows.
Kernel migration gate and client rebuild
src/utils/kernelMigration.utils.ts, src/context/kernelClient.context.tsx, src/constants/analytics.consts.ts
Migration no-op submission, status verification, client rebuilding, sequencing, and migration analytics are added.
Shared spend preflight
src/hooks/wallet/spendPreflight.ts, src/hooks/wallet/__tests__/spendPreflight.test.ts
Spend strategy resolution, affordability checks, migration orchestration, and session-key grant handling are centralized and tested.
Spend hook integration
src/hooks/wallet/useSpendBundle.ts, src/hooks/wallet/useSignSpendBundle.ts, src/hooks/wallet/useSendMoney.ts, src/components/Card/*, src/app/(mobile-ui)/withdraw/manteca/page.tsx
Spend and sign-only flows consume shared preflight results and rebuilt clients, with updated error imports and typed-data signing.

Non-EVM withdrawals and chain rollout

Layer / File(s) Summary
Chain and token catalogs
src/constants/nonEvmWithdraw.consts.ts, src/constants/rhino.consts.ts, src/constants/chain-details.json, src/constants/token-details.json, src/components/Global/TokenSelector/TokenSelector.consts.ts
Solana, Tron, and additional EVM chains, tokens, mappings, and Rhino token restrictions are added or revised.
Feature-flag rollout gating
src/hooks/useFeatureFlag.ts, src/hooks/useChainRollout.ts
Reactive PostHog feature-flag helpers gate chain availability while preserving legacy chains by default.
Token and network selection
src/components/AddMoney/components/*, src/components/Global/TokenSelector/TokenSelector.tsx
Displayed networks, token labels, search scope, and coming-soon behavior now respect rollout and Rhino restrictions.
Address and transfer handling
src/lib/validation/*, src/components/Global/GeneralRecipientInput/*, src/components/Withdraw/views/Initial.withdraw.view.tsx, src/features/payments/shared/hooks/useCrossChainTransfer.ts, src/services/rhino-sda.ts
Recipient validation, withdrawal state, token inference, and destination types support non-EVM address formats.

Supporting UI and integration fixes

Layer / File(s) Summary
Payment and transaction integrations
src/app/(mobile-ui)/qr-pay/*, src/hooks/useZeroDev.ts, src/components/TransactionDetails/transactionTransformer.ts, src/hooks/wallet/useWallet.ts
Context mocks and imports are aligned, failed UserOps reset loading state, explorer fallback is added, and wallet imports are reorganized.
Deposit configuration and notices
src/app/actions/supported-chains.ts, src/components/AddMoney/views/CryptoDeposit.view.tsx
Linea icon rendering uses an override URL, and non-offramp deposits display a bridging-fee notice.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: jjramirezn

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.48% 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 summarizes the main change: Solana and Tron withdrawals plus related chain-expansion polish.
Description check ✅ Passed The description is detailed and directly matches the changeset, covering non-EVM withdrawals, validation, and the polish items.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6099.08 → 6124.38 (+25.3)
Findings: +3 net (+44 new, -41 resolved)

🆕 New findings (44)

  • critical complexity — src/components/Global/TokenSelector/TokenSelector.tsx — CC 128, MI 60.78, SLOC 333
  • critical complexity — src/components/TransactionDetails/transactionTransformer.ts — CC 98, MI 43.94, SLOC 280
  • high complexity — src/components/AddMoney/views/CryptoDeposit.view.tsx — CC 46, MI 62.91, SLOC 53
  • high hotspot — src/components/TransactionDetails/transactionTransformer.ts — 40 commits, +908/-774 lines since 6 months ago
  • high method-complexity — src/components/AddMoney/views/CryptoDeposit.view.tsx:57 — CC 37 SLOC 33
  • high method-complexity — src/components/TransactionDetails/transactionTransformer.ts:465 — mapTransactionDataForDrawer CC 34 SLOC 138
  • high complexity — src/features/payments/shared/hooks/useCrossChainTransfer.ts — CC 33, MI 45.85, SLOC 330
  • high method-complexity — src/components/Global/TokenSelector/TokenSelector.tsx:65 — CC 32 SLOC 97
  • medium high-mdd — src/components/AddMoney/views/CryptoDeposit.view.tsx:57 — CryptoDepositView: MDD 141.1 (uses across many lines from declarations)
  • medium high-mdd — src/components/Global/TokenSelector/TokenSelector.tsx:65 — TokenSelector: MDD 124.5 (uses across many lines from declarations)
  • medium high-mdd — src/components/TransactionDetails/transactionTransformer.ts:465 — mapTransactionDataForDrawer: MDD 98.5 (uses across many lines from declarations)
  • medium high-dlt — src/components/Global/TokenSelector/TokenSelector.tsx:65 — TokenSelector: DLT 42 (calls 42 distinct functions — high context load)
  • medium high-mdd — src/components/Global/GeneralRecipientInput/index.tsx:35 — GeneralRecipientInput: MDD 40.1 (uses across many lines from declarations)
  • medium high-dlt — src/features/payments/shared/hooks/useCrossChainTransfer.ts:172 — useCrossChainTransfer: DLT 34 (calls 34 distinct functions — high context load)
  • medium high-mdd — src/features/payments/shared/hooks/useCrossChainTransfer.ts:172 — useCrossChainTransfer: MDD 31.2 (uses across many lines from declarations)
  • medium complexity — src/components/Withdraw/views/Initial.withdraw.view.tsx — CC 27, MI 58.42, SLOC 88
  • medium high-mdd — src/components/Withdraw/views/Initial.withdraw.view.tsx:28 — InitialWithdrawView: MDD 27.0 (uses across many lines from declarations)
  • medium high-mdd — src/features/payments/shared/hooks/useCrossChainTransfer.ts:415 — runBridgePath: MDD 26.7 (uses across many lines from declarations)
  • medium high-mdd — src/components/Global/TokenSelector/TokenSelector.tsx:225 — : MDD 22.8 (uses across many lines from declarations)
  • medium complexity — src/components/Global/GeneralRecipientInput/index.tsx — CC 22, MI 50.61, SLOC 100

…and 24 more.

✅ Resolved (41)

  • src/components/Global/TokenSelector/TokenSelector.tsx — CC 124, MI 60.76, SLOC 326
  • src/components/TransactionDetails/transactionTransformer.ts — CC 96, MI 44.17, SLOC 275
  • src/components/AddMoney/views/CryptoDeposit.view.tsx — CC 45, MI 62.95, SLOC 53
  • src/components/TransactionDetails/transactionTransformer.ts — 39 commits, +899/-773 lines since 6 months ago
  • src/components/AddMoney/views/CryptoDeposit.view.tsx:57 — CC 36 SLOC 33
  • src/components/TransactionDetails/transactionTransformer.ts:457 — mapTransactionDataForDrawer CC 34 SLOC 138
  • src/components/Global/TokenSelector/TokenSelector.tsx:63 — CC 31 SLOC 92
  • src/features/payments/shared/hooks/useCrossChainTransfer.ts — CC 31, MI 45.16, SLOC 320
  • src/components/AddMoney/views/CryptoDeposit.view.tsx:57 — CryptoDepositView: MDD 137.7 (uses across many lines from declarations)
  • src/components/Global/TokenSelector/TokenSelector.tsx:63 — TokenSelector: MDD 122.6 (uses across many lines from declarations)
  • src/components/TransactionDetails/transactionTransformer.ts:457 — mapTransactionDataForDrawer: MDD 98.5 (uses across many lines from declarations)
  • src/components/Global/TokenSelector/TokenSelector.tsx:63 — TokenSelector: DLT 41 (calls 41 distinct functions — high context load)
  • src/components/Global/GeneralRecipientInput/index.tsx:30 — GeneralRecipientInput: MDD 34.0 (uses across many lines from declarations)
  • src/features/payments/shared/hooks/useCrossChainTransfer.ts:160 — useCrossChainTransfer: DLT 34 (calls 34 distinct functions — high context load)
  • src/features/payments/shared/hooks/useCrossChainTransfer.ts:160 — useCrossChainTransfer: MDD 31.2 (uses across many lines from declarations)
  • src/features/payments/shared/hooks/useCrossChainTransfer.ts:403 — runBridgePath: MDD 26.7 (uses across many lines from declarations)
  • src/components/Global/TokenSelector/TokenSelector.tsx:205 — : MDD 22.8 (uses across many lines from declarations)
  • src/components/Withdraw/views/Initial.withdraw.view.tsx — CC 23, MI 57.47, SLOC 73
  • src/components/Withdraw/views/Initial.withdraw.view.tsx:25 — InitialWithdrawView: MDD 21.7 (uses across many lines from declarations)
  • src/components/Global/GeneralRecipientInput/index.tsx — CC 19, MI 51.87, SLOC 91

…and 21 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/constants/nonEvmWithdraw.consts.ts 0.0 5.7 +5.7
src/lib/validation/addressFamily.ts 0.0 5.1 +5.1
src/hooks/useChainRollout.ts 0.0 5.1 +5.1
src/hooks/useFeatureFlag.ts 0.0 3.6 +3.6
src/components/AddMoney/components/SupportedNetworksModal.tsx 3.2 5.4 +2.2
src/components/AddMoney/components/ChooseNetworkDrawer.tsx 4.1 5.0 +0.9
src/components/Global/GeneralRecipientInput/index.tsx 10.4 11.3 +0.9
src/lib/validation/recipient.ts 7.5 8.1 +0.6

@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 1823 ran, 0 failed, 0 skipped, 28.9s

📊 Coverage (unit)

metric %
statements 57.7%
branches 41.8%
functions 46.2%
lines 57.9%
⏱ 10 slowest test cases
time test
3.8s 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.4s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in updateUserById body
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 › 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 US account with spaces
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle invalid ETH address (invalid characters)
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid ETH address
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid Italian IBAN
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@Hugo0

Hugo0 commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 11, 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.

Marketing wants to launch the new chains one at a time with a fuss
(Konrad). One PostHog flag per chain — toggling in the PostHog UI
enables/disables a chain on prod instantly, no deploy. Staging/preview/
local bypass the flags entirely (QA tests before launch). Fail-closed
on prod: if PostHog is unavailable a gated chain stays hidden — a
rollout gate must never fail into 'launched'. Legacy chains are
unflagged and always on.
…domain wrapper

The team-facing concept is the generic primitive (reactive PostHog
flag read with explicit per-feature failure semantics); chain rollout
keeps a named wrapper because isChainRolledOut('solana') reads better
at call sites than a raw flag string. New features use useFeatureFlag
directly.

@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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/AddMoney/components/ChooseNetworkDrawer.tsx (1)

33-58: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

EVM network count no longer matches the filtered chip list.

Line 37's ${SUPPORTED_EVM_CHAINS.length} Networks - 1 Address still counts the full unfiltered array, while the chips below are now .filter(isChainRolledOut). Once a chain is gated off in prod, the header count will overstate what's actually shown.

💡 Proposed fix
+                            const evmChains = SUPPORTED_EVM_CHAINS.filter(isChainRolledOut)
                             <ActionListCard
                                 title="EVM"
-                                description={`${SUPPORTED_EVM_CHAINS.length} Networks - 1 Address`}
+                                description={`${evmChains.length} Networks - 1 Address`}

And reuse evmChains in the .map() below instead of recomputing the filter inline.

🤖 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/AddMoney/components/ChooseNetworkDrawer.tsx` around lines 33 -
58, Define a reusable evmChains collection from SUPPORTED_EVM_CHAINS filtered
with isChainRolledOut, use evmChains.length for the ActionListCard network
count, and map over evmChains for the expanded chip list instead of filtering
inline.
🧹 Nitpick comments (4)
src/features/payments/shared/hooks/useCrossChainTransfer.ts (1)

157-163: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Case-insensitive comparison is wrong for base58 (Solana/Tron) addresses.

Base58 is case-sensitive (unlike EVM hex, where lowercasing is standard practice for checksum-agnostic comparison). Lowercasing both sides before comparing t.address against tokenAddress could cause a false match if a future entry differs from another only by letter case. Harmless today with the current fixed 3-token list, but worth using exact string equality here to avoid a latent bug as more non-EVM tokens are added.

♻️ Suggested fix
     const nonEvm = NON_EVM_WITHDRAW_CHAINS[String(chainId).toLowerCase()]
     if (nonEvm) {
-        return nonEvm.tokens.find((t) => t.address.toLowerCase() === tokenAddress.toLowerCase())?.symbol.toUpperCase()
+        return nonEvm.tokens.find((t) => t.address === tokenAddress)?.symbol.toUpperCase()
     }
🤖 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/features/payments/shared/hooks/useCrossChainTransfer.ts` around lines 157
- 163, Update inferTokenSymbol’s non-EVM token lookup to compare t.address and
tokenAddress with exact, case-sensitive string equality; preserve the existing
symbol normalization and undefined behavior when no token matches.
src/lib/validation/addressFamily.ts (1)

12-15: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use real address validation for Solana/Tron withdrawals
src/lib/validation/addressFamily.ts: these regexes only enforce shape, so they still accept malformed Solana/Tron strings that fail decoded-byte-length or Base58Check rules. Swap this for a real decode/checksum check before accepting a withdrawal destination.

🤖 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/lib/validation/addressFamily.ts` around lines 12 - 15, Replace the
shape-only SOLANA_ADDRESS_REGEX and TRON_ADDRESS_REGEX checks with real
validation in the address-family validation flow: Base58-decode Solana
destinations and require the expected decoded byte length, and
Base58Check-decode Tron destinations while verifying the checksum and network
format. Ensure withdrawals reject malformed destinations before acceptance,
reusing existing decoding utilities if available.
src/lib/validation/recipient.ts (1)

11-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Non-EVM branch may be unused by production code — see companion comment in GeneralRecipientInput/index.tsx.

GeneralRecipientInput.checkAddress reimplements this exact isValidAddressForFamily branching independently instead of delegating to this function, so this addition appears reachable only from tests unless another caller (e.g. useCrossChainTransfer.ts) invokes it with a non-'evm' family. Also note this branch doesn't trim recipient before validating, unlike the UI's trimmedInput — a latent inconsistency if this path is ever exercised directly. See the follow-up comment in GeneralRecipientInput/index.tsx for the suggested consolidation.

🤖 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/lib/validation/recipient.ts` around lines 11 - 27, The non-EVM validation
in validateAndResolveRecipient is duplicated and may not be used by production
code; consolidate GeneralRecipientInput.checkAddress to delegate to this
function for non-EVM addresses. Ensure recipient is trimmed before validation
and resolution so direct and UI-driven paths apply identical normalization and
address-family checks.
src/components/Global/GeneralRecipientInput/index.tsx (1)

59-71: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Delegate to validateAndResolveRecipient instead of reimplementing the non-EVM check.

validateAndResolveRecipient in recipient.ts was extended in this same PR with an addressFamily parameter that performs this exact check (validate via isValidAddressForFamily, throw RecipientValidationError on failure, otherwise return an ADDRESS-typed result). This branch reimplements that logic locally instead of calling it, so the two copies can drift (e.g. differing trim behavior, differing error message).

♻️ Proposed consolidation
                 // Non-EVM destination: base58 address or nothing — never IBAN,
                 // US-routing, ENS, or username.
                 if (addressFamily !== 'evm') {
-                    const familyValid = isValidAddressForFamily(trimmedInput, addressFamily)
-                    if (familyValid) {
-                        resolvedAddress.current = trimmedInput
-                    } else {
-                        errorMessage.current = `Invalid ${addressFamily === 'solana' ? 'Solana' : 'Tron'} address`
-                    }
-                    recipientType.current = 'address'
-                    return familyValid
+                    recipientType.current = 'address'
+                    try {
+                        const validation = await validateAndResolveRecipient(trimmedInput, isWithdrawal, addressFamily)
+                        resolvedAddress.current = validation.resolvedAddress
+                        return true
+                    } catch (error: unknown) {
+                        errorMessage.current = (error as Error).message
+                        return false
+                    }
                 }
🤖 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/Global/GeneralRecipientInput/index.tsx` around lines 59 - 71,
Replace the local non-EVM validation branch in the recipient input flow with a
call to validateAndResolveRecipient, passing the trimmed input and
addressFamily. Reuse its returned ADDRESS result to update the recipient state,
and handle RecipientValidationError through the existing error-state path
instead of calling isValidAddressForFamily or constructing a separate error
message locally.
🤖 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/__tests__/qr-pay-states.test.tsx:
- Line 509: Fix the test setup around loadingStateContext by replacing both
local require() bindings with a static import, removing the unused
mockSetLoadingState declaration, and passing that mock to the provider’s
setLoadingState prop if the provider requires it; otherwise remove the mock
entirely.

In `@src/components/TransactionDetails/transactionTransformer.ts`:
- Around line 253-261: The explorer URL selection around baseUrl must use
PEANUT_WALLET_CHAIN for every CRYPTO_WITHDRAW, not only when the destination URL
is missing. Update the logic using intentKindOf(entry) so withdrawals directly
select the source-chain explorer, while preserving destination-based explorer
selection for other transaction types.

In `@src/context/kernelClient.context.tsx`:
- Line 501: Update the client build flow around storeClient so primary
initialization uses the same sequencing/arbitration as migration rebuilds. When
a build is superseded, do not return its stale kernelClient; instead await the
latest build or reject. Ensure superseded builds cannot overwrite the current
cache entry or remove the newer in-flight slot, preventing exposure of an
outdated signing client.

In `@src/hooks/useChainRollout.ts`:
- Around line 42-46: Update useChainRollout so its returned checker reflects the
useFeatureFlags subscription: return a render-scoped wrapper around
isChainRolledOut whose identity changes when flags update, ensuring memoized
consumers such as TokenSelector recompute allowedChainIds after PostHog flags
load.

In `@src/hooks/useFeatureFlag.ts`:
- Around line 38-45: Update useFeatureFlags so the returned checker callback is
recreated when the flag reload state changes, rather than always returning the
stable isFeatureFlagEnabled reference. Apply the same changing-identity behavior
to useChainRollout so consumers such as TokenSelector recompute after PostHog
flags load, while preserving their existing flag-checking logic.

---

Outside diff comments:
In `@src/components/AddMoney/components/ChooseNetworkDrawer.tsx`:
- Around line 33-58: Define a reusable evmChains collection from
SUPPORTED_EVM_CHAINS filtered with isChainRolledOut, use evmChains.length for
the ActionListCard network count, and map over evmChains for the expanded chip
list instead of filtering inline.

---

Nitpick comments:
In `@src/components/Global/GeneralRecipientInput/index.tsx`:
- Around line 59-71: Replace the local non-EVM validation branch in the
recipient input flow with a call to validateAndResolveRecipient, passing the
trimmed input and addressFamily. Reuse its returned ADDRESS result to update the
recipient state, and handle RecipientValidationError through the existing
error-state path instead of calling isValidAddressForFamily or constructing a
separate error message locally.

In `@src/features/payments/shared/hooks/useCrossChainTransfer.ts`:
- Around line 157-163: Update inferTokenSymbol’s non-EVM token lookup to compare
t.address and tokenAddress with exact, case-sensitive string equality; preserve
the existing symbol normalization and undefined behavior when no token matches.

In `@src/lib/validation/addressFamily.ts`:
- Around line 12-15: Replace the shape-only SOLANA_ADDRESS_REGEX and
TRON_ADDRESS_REGEX checks with real validation in the address-family validation
flow: Base58-decode Solana destinations and require the expected decoded byte
length, and Base58Check-decode Tron destinations while verifying the checksum
and network format. Ensure withdrawals reject malformed destinations before
acceptance, reusing existing decoding utilities if available.

In `@src/lib/validation/recipient.ts`:
- Around line 11-27: The non-EVM validation in validateAndResolveRecipient is
duplicated and may not be used by production code; consolidate
GeneralRecipientInput.checkAddress to delegate to this function for non-EVM
addresses. Ensure recipient is trimmed before validation and resolution so
direct and UI-driven paths apply identical normalization and address-family
checks.
🪄 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: 332c5a0a-25cd-44bc-a091-2b0865b70e81

📥 Commits

Reviewing files that changed from the base of the PR and between 558b1cf and f5e2156.

📒 Files selected for processing (40)
  • src/app/(mobile-ui)/card-recovery/page.tsx
  • 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/manteca/page.tsx
  • src/app/actions/supported-chains.ts
  • src/app/api/send-discord-notification/route.ts
  • src/components/AddMoney/components/ChooseNetworkDrawer.tsx
  • src/components/AddMoney/components/SupportedNetworksModal.tsx
  • src/components/AddMoney/views/CryptoDeposit.view.tsx
  • src/components/Card/CancelCardModal.tsx
  • src/components/Card/LockCardModal.tsx
  • src/components/Global/GeneralRecipientInput/index.tsx
  • src/components/Global/TokenSelector/TokenSelector.consts.ts
  • src/components/Global/TokenSelector/TokenSelector.tsx
  • src/components/TransactionDetails/transactionTransformer.ts
  • src/components/Withdraw/views/Initial.withdraw.view.tsx
  • src/constants/analytics.consts.ts
  • src/constants/chain-details.json
  • src/constants/nonEvmWithdraw.consts.ts
  • src/constants/rhino.consts.ts
  • src/constants/token-details.json
  • src/context/kernelClient.context.tsx
  • src/features/payments/shared/hooks/useCrossChainTransfer.ts
  • src/hooks/useChainRollout.ts
  • src/hooks/useFeatureFlag.ts
  • src/hooks/useZeroDev.ts
  • src/hooks/wallet/__tests__/spendPreflight.test.ts
  • src/hooks/wallet/spendPreflight.ts
  • src/hooks/wallet/useSendMoney.ts
  • src/hooks/wallet/useSignSpendBundle.ts
  • src/hooks/wallet/useSpendBundle.ts
  • src/hooks/wallet/useWallet.ts
  • src/lib/validation/__tests__/addressFamily.test.ts
  • src/lib/validation/addressFamily.ts
  • src/lib/validation/recipient.ts
  • src/services/rhino-sda.ts
  • src/utils/__tests__/kernelMigration.utils.test.ts
  • src/utils/__tests__/rainWithdraw.utils.test.ts
  • src/utils/kernelMigration.utils.ts
  • src/utils/rainWithdraw.utils.ts
💤 Files with no reviewable changes (1)
  • src/app/api/send-discord-notification/route.ts

// Loading state context provider
const LoadingStateProvider = ({ children }: { children: React.ReactNode }) => {
const loadingStateContext = require('@/context').loadingStateContext
const loadingStateContext = require('@/context/loadingStates.context').loadingStateContext

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Fix the test setup’s lint errors.

require() at Lines 509 and 533 violates @typescript-eslint/no-require-imports, while mockSetLoadingState at Line 521 is unused. Import loadingStateContext statically, remove the local require() bindings, and either remove the mock or pass it as the provider’s setLoadingState value.

Proposed cleanup
+import { loadingStateContext } from '`@/context/loadingStates.context`'

-    const loadingStateContext = require('`@/context/loadingStates.context`').loadingStateContext
-
-const mockSetLoadingState = jest.fn()
-
-    const { loadingStateContext } = require('`@/context/loadingStates.context`')

Also applies to: 521-522, 533-533

🧰 Tools
🪛 ESLint

[error] 509-509: A require() style import is forbidden.

(@typescript-eslint/no-require-imports)

🤖 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/app/`(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx at line 509, Fix
the test setup around loadingStateContext by replacing both local require()
bindings with a static import, removing the unused mockSetLoadingState
declaration, and passing that mock to the provider’s setLoadingState prop if the
provider requires it; otherwise remove the mock entirely.

Source: Linters/SAST tools

Comment on lines +253 to +261
let baseUrl = getExplorerUrl(explorerUrlChainID)
// Cross-chain withdrawals record the ARBITRUM source tx hash while
// entry.chainId is the destination — and several destinations (Tempo,
// Solana, Tron, …) have no chain-details explorer entry at all, which
// left the receipt linkless. Fall back to the source-chain explorer so
// the receipt always links the tx that actually carries the hash.
if (!baseUrl && intentKindOf(entry) === 'CRYPTO_WITHDRAW') {
baseUrl = getExplorerUrl(PEANUT_WALLET_CHAIN.id.toString())
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the source-chain explorer for every crypto withdrawal.

The fallback only runs when the destination has no explorer URL. When a destination explorer exists, the Arbitrum/source-chain txHash is incorrectly linked under the destination chain. Select PEANUT_WALLET_CHAIN for CRYPTO_WITHDRAW directly, not only as a fallback.

Proposed fix
-    const explorerUrlChainID =
-        intentKindOf(entry) === 'CRYPTO_DEPOSIT' ? PEANUT_WALLET_CHAIN.id.toString() : entry.chainId
-    let baseUrl = getExplorerUrl(explorerUrlChainID)
+    const kind = intentKindOf(entry)
+    const explorerUrlChainID =
+        kind === 'CRYPTO_DEPOSIT' || kind === 'CRYPTO_WITHDRAW'
+            ? PEANUT_WALLET_CHAIN.id.toString()
+            : entry.chainId
+    const baseUrl = getExplorerUrl(explorerUrlChainID)

-    if (!baseUrl && intentKindOf(entry) === 'CRYPTO_WITHDRAW') {
-        baseUrl = getExplorerUrl(PEANUT_WALLET_CHAIN.id.toString())
-    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let baseUrl = getExplorerUrl(explorerUrlChainID)
// Cross-chain withdrawals record the ARBITRUM source tx hash while
// entry.chainId is the destination — and several destinations (Tempo,
// Solana, Tron, …) have no chain-details explorer entry at all, which
// left the receipt linkless. Fall back to the source-chain explorer so
// the receipt always links the tx that actually carries the hash.
if (!baseUrl && intentKindOf(entry) === 'CRYPTO_WITHDRAW') {
baseUrl = getExplorerUrl(PEANUT_WALLET_CHAIN.id.toString())
}
// Cross-chain withdrawals record the ARBITRUM source tx hash while
// entry.chainId is the destination — and several destinations (Tempo,
// Solana, Tron, …) have no chain-details explorer entry at all, which
// left the receipt linkless. Fall back to the source-chain explorer so
// the receipt always links the tx that actually carries the hash.
const kind = intentKindOf(entry)
const explorerUrlChainID =
kind === 'CRYPTO_DEPOSIT' || kind === 'CRYPTO_WITHDRAW'
? PEANUT_WALLET_CHAIN.id.toString()
: entry.chainId
const baseUrl = getExplorerUrl(explorerUrlChainID)
🤖 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/TransactionDetails/transactionTransformer.ts` around lines 253
- 261, The explorer URL selection around baseUrl must use PEANUT_WALLET_CHAIN
for every CRYPTO_WITHDRAW, not only when the destination URL is missing. Update
the logic using intentKindOf(entry) so withdrawals directly select the
source-chain explorer, while preserving destination-based explorer selection for
other transaction types.

// registering→not→registering UI flicker between retries.
if (isMounted) {
setClientsByChain((prev) => ({ ...prev, [primaryChainId]: kernelClient }))
storeClient(primaryChainId, kernelClient)

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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Fail closed for every superseded client build.

The sequence guard only prevents stale cache writes: the superseded promise still returns its old client to its caller. Additionally, the primary initializer commits outside this sequencing, so it can overwrite a newer migration rebuild and delete its in-flight slot. Either race can expose a v0.0.2-signing client after migration.

Route primary initialization through the same build arbitration, and make superseded builds await the latest build or reject rather than return their stale client.

Also applies to: 628-634

🤖 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/context/kernelClient.context.tsx` at line 501, Update the client build
flow around storeClient so primary initialization uses the same
sequencing/arbitration as migration rebuilds. When a build is superseded, do not
return its stale kernelClient; instead await the latest build or reject. Ensure
superseded builds cannot overwrite the current cache entry or remove the newer
in-flight slot, preventing exposure of an outdated signing client.

Comment on lines +42 to +46
/** Reactive variant — re-renders when PostHog's flags load. */
export function useChainRollout(): (chainKey: string) => boolean {
useFeatureFlags()
return isChainRolledOut
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm no other memoization wraps isChainRolledOut/useChainRollout results, and check other consumers.
rg -n "useChainRollout" -B2 -A6 --type=ts --type=tsx src 2>/dev/null || rg -n "useChainRollout" -B2 -A6 src

Repository: peanutprotocol/peanut-ui

Length of output: 6067


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== useFeatureFlag.ts outline ==\n'
ast-grep outline src/hooks/useFeatureFlag.ts --view expanded || true
printf '\n== useChainRollout.ts outline ==\n'
ast-grep outline src/hooks/useChainRollout.ts --view expanded || true
printf '\n== TokenSelector.tsx outline ==\n'
ast-grep outline src/components/Global/TokenSelector/TokenSelector.tsx --view expanded || true

printf '\n== useFeatureFlag.ts relevant lines ==\n'
nl -ba src/hooks/useFeatureFlag.ts | sed -n '1,220p'

printf '\n== useChainRollout.ts relevant lines ==\n'
nl -ba src/hooks/useChainRollout.ts | sed -n '1,200p'

printf '\n== TokenSelector.tsx relevant lines ==\n'
nl -ba src/components/Global/TokenSelector/TokenSelector.tsx | sed -n '180,360p'

Repository: peanutprotocol/peanut-ui

Length of output: 1442


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== useFeatureFlag.ts ==\n'
sed -n '1,220p' src/hooks/useFeatureFlag.ts

printf '\n== useChainRollout.ts ==\n'
sed -n '1,220p' src/hooks/useChainRollout.ts

printf '\n== TokenSelector.tsx (relevant section) ==\n'
sed -n '186,345p' src/components/Global/TokenSelector/TokenSelector.tsx

Repository: peanutprotocol/peanut-ui

Length of output: 11716


Make useChainRollout return a reactive checker
useFeatureFlags() re-renders this hook, but the returned callback is still the module-level isChainRolledOut, so memoized consumers like TokenSelector’s allowedChainIds won’t recompute when PostHog flags load. Return a wrapper whose identity changes with the flag subscription, or key the memo off a flag-version/state value instead.

🤖 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/hooks/useChainRollout.ts` around lines 42 - 46, Update useChainRollout so
its returned checker reflects the useFeatureFlags subscription: return a
render-scoped wrapper around isChainRolledOut whose identity changes when flags
update, ensuring memoized consumers such as TokenSelector recompute
allowedChainIds after PostHog flags load.

Comment on lines +38 to +45
export function useFeatureFlags(): (flagKey: string, options?: FeatureFlagOptions) => boolean {
const [, bump] = useReducer((n: number) => n + 1, 0)
useEffect(() => {
// returns an unsubscribe function
return posthog.onFeatureFlags(() => bump())
}, [])
return isFeatureFlagEnabled
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

git ls-files 'src/hooks/useFeatureFlag.ts' 'src/hooks/useChainRollout.ts' 'src/components/Global/TokenSelector/TokenSelector.tsx'

Repository: peanutprotocol/peanut-ui

Length of output: 273


🏁 Script executed:

ast-grep outline src/hooks/useFeatureFlag.ts --view expanded
ast-grep outline src/hooks/useChainRollout.ts --view expanded
ast-grep outline src/components/Global/TokenSelector/TokenSelector.tsx --view expanded

Repository: peanutprotocol/peanut-ui

Length of output: 1261


🏁 Script executed:

sed -n '1,140p' src/hooks/useFeatureFlag.ts
printf '\n--- useChainRollout ---\n'
sed -n '1,140p' src/hooks/useChainRollout.ts
printf '\n--- TokenSelector ---\n'
sed -n '180,230p' src/components/Global/TokenSelector/TokenSelector.tsx

Repository: peanutprotocol/peanut-ui

Length of output: 6614


useFeatureFlags needs a changing function identity when flags load

bump() re-renders the hook, but it always returns the same isFeatureFlagEnabled reference. Any consumer that memoizes on the checker — including useChainRollout and TokenSelector’s allowedChainIds — will never recompute after PostHog flags arrive. Return a callback whose identity changes with the reload state, and update useChainRollout to do the same.

🤖 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/hooks/useFeatureFlag.ts` around lines 38 - 45, Update useFeatureFlags so
the returned checker callback is recreated when the flag reload state changes,
rather than always returning the stable isFeatureFlagEnabled reference. Apply
the same changing-identity behavior to useChainRollout so consumers such as
TokenSelector recompute after PostHog flags load, while preserving their
existing flag-checking logic.

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