Skip to content

fix: x-chain payments (daimo + peanut)#1447

Merged
kushagrasarathe merged 4 commits intopeanut-wallet-devfrom
fix/payments
Nov 12, 2025
Merged

fix: x-chain payments (daimo + peanut)#1447
kushagrasarathe merged 4 commits intopeanut-wallet-devfrom
fix/payments

Conversation

@kushagrasarathe
Copy link
Contributor

  • fixes TASK-16439 : add cross chain token selector for peanut wallet payments to semantic requests
  • also fixes TASK-16803 : fix daimo x-chain destination chain issue

@vercel
Copy link

vercel bot commented Nov 12, 2025

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

Project Deployment Preview Comments Updated (UTC)
peanut-wallet Ready Ready Preview Comment Nov 12, 2025 6:43pm

@notion-workspace
Copy link

@notion-workspace
Copy link

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 12, 2025

Walkthrough

Removes the external-wallet fulfillment UI and related context state, deletes associated components, and adds token/chain customization and external-invocation support for Daimo Pay plus TokenSelector integration for external ADDRESS/ENS recipient flows.

Changes

Cohort / File(s) Summary
App entry & context
src/app/[...recipient]/client.tsx, src/context/RequestFulfillmentFlowContext.tsx
Removed ExternalWalletFulfilManager usage and the showExternalWalletFulfillMethods / externalWalletFulfillMethod context properties and types; eliminated related state initialization, reset logic, and provider exports.
Deleted external-wallet views
src/components/Request/views/ExternalWalletFulfilManager.tsx, src/components/Request/views/ExternalWalletFulfilMethods.tsx
Deleted components that provided external wallet/exchange method selection and view orchestration.
Daimo Pay / ActionList changes
src/components/Global/DaimoPayButton/index.tsx, src/components/Common/ActionListDaimoPayButton.tsx, src/components/Common/ActionList.tsx
Added optional toChainId?: number and toTokenAddress?: string to DaimoPayButton; added `clickHandlerRef: React.MutableRefObject<(() => void)
Payment form / TokenSelector integration
src/components/Payment/PaymentForm/index.tsx
Integrated TokenSelector for external (ADDRESS/ENS) recipients; added initialization/defaulting of chain/token (uses PEANUT_WALLET_CHAIN and USDC fallback), adjusted balance checks and payment initiation conditions for external recipients, removed external-wallet-specific navigation shortcut, and updated effect dependencies.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay special attention to src/components/Payment/PaymentForm/index.tsx for state initialization, dependency arrays, and balance-check logic for external recipients.
  • Verify removal of context properties is consistent across the codebase (no remaining references to ExternalWalletFulfilMethod, showExternalWalletFulfillMethods, or externalWalletFulfillMethod).
  • Confirm clickHandlerRef is correctly threaded: ActionListActionListDaimoPayButtonDaimoPayButton and that the external invocation works with timing/closure semantics.
  • Check DaimoPayButton new toChainId / toTokenAddress handling for default fallbacks and type correctness.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: fixing cross-chain payment functionality for both Daimo and Peanut Wallet, which aligns with the substantial refactoring removing external wallet flows and adding cross-chain token/chain selection.
Description check ✅ Passed The description directly references the two specific tasks being fixed (TASK-16439 for cross-chain token selector and TASK-16803 for Daimo destination chain), which relate directly to the changeset's modifications.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/payments

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 710a7e6 and 6f4e0cc.

📒 Files selected for processing (1)
  • src/components/Common/ActionListDaimoPayButton.tsx (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/Common/ActionListDaimoPayButton.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
src/components/Payment/PaymentForm/index.tsx (1)

181-207: Fix default token initialization for external recipients

On the first render selectedChainID is still '', so the USDC fallback inside the else if (isExternalRecipient && !selectedTokenAddress && selectedChainID) branch never executes. We then call setInitialSetupDone(true), the effect short-circuits on the next pass, and external ADDRESS/ENS flows end up without a default token (canInitiatePayment stays false until the user manually picks one). Please derive the chain id for defaults before the state updates (or delay flipping initialSetupDone) so the token fallback can run in the same pass. One option is:

-        if (chain) {
-            setSelectedChainID((chain.chainId || requestDetails?.chainId) ?? '')
-            if (!token && !requestDetails?.tokenAddress) {
-                const defaultToken = chain.tokens.find((t) => t.symbol.toLowerCase() === 'usdc')
-                if (defaultToken) {
-                    setSelectedTokenAddress(defaultToken.address)
-                    // Note: decimals automatically derived by useTokenPrice hook
-                }
-            }
-        } else if (isExternalRecipient && !selectedChainID) {
-            // default to arbitrum for external recipients if no chain specified
-            setSelectedChainID(PEANUT_WALLET_CHAIN.id.toString())
-        }
-
-        if (token) {
-            setSelectedTokenAddress((token.address || requestDetails?.tokenAddress) ?? '')
-            // Note: decimals automatically derived by useTokenPrice hook
-        } else if (isExternalRecipient && !selectedTokenAddress && selectedChainID) {
-            // default to USDC for external recipients if no token specified
-            const chainData = supportedSquidChainsAndTokens[selectedChainID]
-            const defaultToken = chainData?.tokens.find((t) => t.symbol.toLowerCase() === 'usdc')
-            if (defaultToken) {
-                setSelectedTokenAddress(defaultToken.address)
-            }
-        }
+        const effectiveChainId =
+            chain?.chainId?.toString() ??
+            requestDetails?.chainId?.toString() ??
+            (isExternalRecipient ? PEANUT_WALLET_CHAIN.id.toString() : undefined)
+
+        if (effectiveChainId && effectiveChainId !== selectedChainID) {
+            setSelectedChainID(effectiveChainId)
+        }
+
+        if (token?.address || requestDetails?.tokenAddress) {
+            setSelectedTokenAddress((token?.address || requestDetails?.tokenAddress) ?? '')
+        } else if (isExternalRecipient && !selectedTokenAddress && effectiveChainId) {
+            const defaultToken =
+                chain?.tokens?.find((t) => t.symbol.toLowerCase() === 'usdc') ??
+                supportedSquidChainsAndTokens[effectiveChainId]?.tokens.find(
+                    (t) => t.symbol.toLowerCase() === 'usdc'
+                )
+            if (defaultToken) {
+                setSelectedTokenAddress(defaultToken.address)
+            }
+        }

This keeps the default behavior working for both URL-provided chains and the Arbitrum fallback.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d9806e3 and 710a7e6.

📒 Files selected for processing (8)
  • src/app/[...recipient]/client.tsx (0 hunks)
  • src/components/Common/ActionList.tsx (5 hunks)
  • src/components/Common/ActionListDaimoPayButton.tsx (4 hunks)
  • src/components/Global/DaimoPayButton/index.tsx (3 hunks)
  • src/components/Payment/PaymentForm/index.tsx (10 hunks)
  • src/components/Request/views/ExternalWalletFulfilManager.tsx (0 hunks)
  • src/components/Request/views/ExternalWalletFulfilMethods.tsx (0 hunks)
  • src/context/RequestFulfillmentFlowContext.tsx (0 hunks)
💤 Files with no reviewable changes (4)
  • src/context/RequestFulfillmentFlowContext.tsx
  • src/components/Request/views/ExternalWalletFulfilManager.tsx
  • src/app/[...recipient]/client.tsx
  • src/components/Request/views/ExternalWalletFulfilMethods.tsx
🧰 Additional context used
🧠 Learnings (26)
📓 Common learnings
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 919
File: src/components/Withdraw/views/Initial.withdraw.view.tsx:87-87
Timestamp: 2025-06-18T19:56:55.443Z
Learning: In withdraw flows for Peanut Wallet, the PeanutActionDetailsCard should always display "USDC" as the token symbol because it shows the amount being withdrawn from the Peanut Wallet (which holds USDC), regardless of the destination token/chain selected by the user. The TokenSelector is used for choosing the withdrawal destination, not the source display.
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 422
File: src/components/Request/Pay/Pay.consts.ts:34-34
Timestamp: 2024-10-08T20:13:42.967Z
Learning: In `src/components/Request/Pay` components, the `tokenPrice` property in the `IPayScreenProps` interface is only relevant to these views. Other components using `IPayScreenProps` do not need to handle `tokenPriceData` when it's updated in these components.
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 422
File: src/components/Request/Pay/Pay.consts.ts:34-34
Timestamp: 2024-10-07T15:50:29.173Z
Learning: In `src/components/Request/Pay` components, the `tokenPrice` property in the `IPayScreenProps` interface is only relevant to these views. Other components using `IPayScreenProps` do not need to handle `tokenPriceData` when it's updated in these components.
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1104
File: src/components/Payment/PaymentForm/index.tsx:522-545
Timestamp: 2025-08-22T07:28:32.281Z
Learning: In `src/components/Payment/PaymentForm/index.tsx`, the `handleCompleteDaimoPayment` function is only for updating payment status in the backend after a successful Daimo payment. Payment success/failure is handled by Daimo itself, so try/catch error handling and error display are not needed for backend sync failures - users shouldn't see errors if payment succeeded but database update failed.
📚 Learning: 2025-08-26T15:25:53.328Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1132
File: src/app/[...recipient]/client.tsx:394-397
Timestamp: 2025-08-26T15:25:53.328Z
Learning: In `src/components/Common/ActionListDaimoPayButton.tsx`, the `handleCompleteDaimoPayment` function should not display error messages to users when DB update fails because the Daimo payment itself has succeeded - showing errors would be confusing since the payment was successful.

Applied to files:

  • src/components/Common/ActionListDaimoPayButton.tsx
  • src/components/Common/ActionList.tsx
📚 Learning: 2025-08-22T07:28:32.281Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1104
File: src/components/Payment/PaymentForm/index.tsx:522-545
Timestamp: 2025-08-22T07:28:32.281Z
Learning: In `src/components/Payment/PaymentForm/index.tsx`, the `handleCompleteDaimoPayment` function is only for updating payment status in the backend after a successful Daimo payment. Payment success/failure is handled by Daimo itself, so try/catch error handling and error display are not needed for backend sync failures - users shouldn't see errors if payment succeeded but database update failed.

Applied to files:

  • src/components/Common/ActionListDaimoPayButton.tsx
  • src/components/Global/DaimoPayButton/index.tsx
  • src/components/Common/ActionList.tsx
📚 Learning: 2025-08-26T17:38:37.055Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1132
File: src/components/Common/ActionList.tsx:153-156
Timestamp: 2025-08-26T17:38:37.055Z
Learning: In ActionList.tsx, when there are circular dependency concerns with ACTION_METHODS being imported by other components, the preferred solution is to move ACTION_METHODS to a separate constants file (like src/constants/actionlist.consts.ts) rather than using prop drilling. This centralizes constants management and creates a cleaner dependency graph.

Applied to files:

  • src/components/Common/ActionListDaimoPayButton.tsx
  • src/components/Common/ActionList.tsx
📚 Learning: 2024-10-08T20:13:42.967Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 422
File: src/components/Request/Pay/Pay.consts.ts:34-34
Timestamp: 2024-10-08T20:13:42.967Z
Learning: In `src/components/Request/Pay` components, the `tokenPrice` property in the `IPayScreenProps` interface is only relevant to these views. Other components using `IPayScreenProps` do not need to handle `tokenPriceData` when it's updated in these components.

Applied to files:

  • src/components/Common/ActionListDaimoPayButton.tsx
  • src/components/Global/DaimoPayButton/index.tsx
  • src/components/Payment/PaymentForm/index.tsx
📚 Learning: 2024-10-07T13:42:00.443Z
Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 422
File: src/components/Request/Pay/Pay.tsx:103-111
Timestamp: 2024-10-07T13:42:00.443Z
Learning: When the token price cannot be fetched in `src/components/Request/Pay/Pay.tsx` within the `PayRequestLink` component, set `tokenPriceData.price` to 0 to ensure the UI remains functional. Since Squid uses their own price engine for x-chain fulfillment transactions, this approach will not affect the transaction computation.

Applied to files:

  • src/components/Common/ActionListDaimoPayButton.tsx
  • src/components/Payment/PaymentForm/index.tsx
📚 Learning: 2025-10-24T13:44:39.473Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1332
File: src/components/Global/TokenAmountInput/index.tsx:141-150
Timestamp: 2025-10-24T13:44:39.473Z
Learning: In the `TokenAmountInput` component (`src/components/Global/TokenAmountInput/index.tsx`), the slider feature (controlled by `showSlider` prop) is only shown for USD input mode. When the slider is used with `maxAmount`, the `selectedAmount` is computed in USD and `isInputUsd` is always `true`, so the conversion in `onChange` handles it correctly.

Applied to files:

  • src/components/Common/ActionListDaimoPayButton.tsx
  • src/components/Payment/PaymentForm/index.tsx
📚 Learning: 2024-12-02T17:19:18.532Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 551
File: src/components/Request/Create/Views/Initial.view.tsx:151-156
Timestamp: 2024-12-02T17:19:18.532Z
Learning: In the `InitialView` component at `src/components/Request/Create/Views/Initial.view.tsx`, when setting the default chain and token in the `useEffect` triggered by `isPeanutWallet`, it's acceptable to omit the setters from the dependency array and not include additional error handling for invalid defaults.

Applied to files:

  • src/components/Global/DaimoPayButton/index.tsx
  • src/components/Payment/PaymentForm/index.tsx
📚 Learning: 2024-10-29T12:19:41.968Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 495
File: src/components/Global/TokenAmountInput/index.tsx:23-30
Timestamp: 2024-10-29T12:19:41.968Z
Learning: In the `TokenAmountInput` component (`src/components/Global/TokenAmountInput/index.tsx`), when the 'Max' button is clicked, we intentionally set the input denomination to 'TOKEN' because we are setting the value as token.

Applied to files:

  • src/components/Global/DaimoPayButton/index.tsx
  • src/components/Payment/PaymentForm/index.tsx
  • src/components/Common/ActionList.tsx
📚 Learning: 2024-10-11T01:14:15.489Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 424
File: src/components/Global/TokenSelector/TokenSelector.tsx:197-211
Timestamp: 2024-10-11T01:14:15.489Z
Learning: In `src/components/Global/TokenSelector/TokenSelector.tsx`, when the calculation within functions like `byChainAndText` is not computationally expensive, it's acceptable to avoid using `useCallback` for memoization.

Applied to files:

  • src/components/Global/DaimoPayButton/index.tsx
  • src/components/Payment/PaymentForm/index.tsx
📚 Learning: 2025-06-18T19:56:55.443Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 919
File: src/components/Withdraw/views/Initial.withdraw.view.tsx:87-87
Timestamp: 2025-06-18T19:56:55.443Z
Learning: In withdraw flows for Peanut Wallet, the PeanutActionDetailsCard should always display "USDC" as the token symbol because it shows the amount being withdrawn from the Peanut Wallet (which holds USDC), regardless of the destination token/chain selected by the user. The TokenSelector is used for choosing the withdrawal destination, not the source display.

Applied to files:

  • src/components/Global/DaimoPayButton/index.tsx
  • src/components/Payment/PaymentForm/index.tsx
📚 Learning: 2024-10-08T20:13:42.967Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 413
File: src/context/tokenSelector.context.tsx:118-123
Timestamp: 2024-10-08T20:13:42.967Z
Learning: In the `TokenContextProvider` component within `src/context/tokenSelector.context.tsx`, in the TypeScript React application, when data changes and before calling `fetchAndSetTokenPrice`, it is necessary to reset `selectedTokenData`, `selectedTokenPrice`, `selectedTokenDecimals`, and `inputDenomination` to discard stale data.

Applied to files:

  • src/components/Payment/PaymentForm/index.tsx
📚 Learning: 2024-10-04T13:10:49.199Z
Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 413
File: src/components/Request/Pay/Views/Initial.view.tsx:71-72
Timestamp: 2024-10-04T13:10:49.199Z
Learning: In `src/components/Request/Pay/Views/Initial.view.tsx`, it's acceptable to use the `!` operator in TypeScript to assert that `selectedTokenData` is not `null` or `undefined`, and potential runtime errors from accessing its properties without checks can be disregarded.

Applied to files:

  • src/components/Payment/PaymentForm/index.tsx
  • src/components/Common/ActionList.tsx
📚 Learning: 2024-10-07T15:25:45.170Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 422
File: src/components/Request/Pay/Views/Initial.view.tsx:76-78
Timestamp: 2024-10-07T15:25:45.170Z
Learning: In `src/components/Request/Pay/Views/Initial.view.tsx`, both `txFee` and `utils.formatTokenAmount(...)` return strings, ensuring that `calculatedFee` consistently returns a string without the need for additional type conversion.

Applied to files:

  • src/components/Payment/PaymentForm/index.tsx
📚 Learning: 2025-01-16T13:14:40.363Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 631
File: src/components/Create/Create.tsx:108-112
Timestamp: 2025-01-16T13:14:40.363Z
Learning: In the Peanut UI codebase, the `resetTokenContextProvider` function from `tokenSelectorContext` is a stable function reference that doesn't change, so it doesn't need to be included in useEffect dependencies.

Applied to files:

  • src/components/Payment/PaymentForm/index.tsx
📚 Learning: 2024-10-03T09:57:43.885Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 404
File: src/context/tokenSelector.context.tsx:121-121
Timestamp: 2024-10-03T09:57:43.885Z
Learning: In `TokenContextProvider` within `tokenSelector.context.tsx`, when token data is loaded from preferences, it's acceptable to set `isTokenPriceFetchingComplete` to `true` because the token data is already available.

Applied to files:

  • src/components/Payment/PaymentForm/index.tsx
📚 Learning: 2025-09-18T09:30:42.901Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1230
File: src/app/(mobile-ui)/withdraw/page.tsx:92-97
Timestamp: 2025-09-18T09:30:42.901Z
Learning: In src/app/(mobile-ui)/withdraw/page.tsx, the useEffect that calls setShowAllWithdrawMethods(true) when amountFromContext exists is intentionally designed to run only on component mount (empty dependency array), not when amountFromContext changes. This is the correct behavior for the withdraw flow where showing all methods should only happen on initial load when an amount is already present.

Applied to files:

  • src/components/Payment/PaymentForm/index.tsx
  • src/components/Common/ActionList.tsx
📚 Learning: 2024-10-25T11:33:46.776Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 484
File: src/components/Cashout/Components/Initial.view.tsx:273-274
Timestamp: 2024-10-25T11:33:46.776Z
Learning: In the `InitialCashoutView` component (`src/components/Cashout/Components/Initial.view.tsx`), linked bank accounts should not generate error states, and the `ValidatedInput` component will clear any error messages if needed. Therefore, it's unnecessary to manually clear the error state when selecting or clearing linked bank accounts.

Applied to files:

  • src/components/Payment/PaymentForm/index.tsx
📚 Learning: 2025-05-23T19:26:58.220Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 873
File: src/components/Withdraw/views/Initial.withdraw.view.tsx:95-95
Timestamp: 2025-05-23T19:26:58.220Z
Learning: The GeneralRecipientInput component supports username validation and resolution through the validateAndResolveRecipient function in src/lib/validation/recipient.ts. The function automatically detects usernames (inputs that don't contain '.' for ENS and don't start with '0x' for addresses), validates them via API HEAD request, fetches user data, and resolves them to Ethereum addresses from the user's PEANUT_WALLET account.

Applied to files:

  • src/components/Payment/PaymentForm/index.tsx
📚 Learning: 2025-10-29T11:27:59.248Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1368
File: src/components/Common/ActionList.tsx:109-111
Timestamp: 2025-10-29T11:27:59.248Z
Learning: In `src/components/Common/ActionList.tsx`, the `balance` from `useWallet()` hook is always in USDC (as a formatted string), making it directly comparable to USD amounts without conversion. The comparison `Number(balance) >= amountInUsd` is intentional and correct.

Applied to files:

  • src/components/Payment/PaymentForm/index.tsx
  • src/components/Common/ActionList.tsx
📚 Learning: 2025-09-16T17:27:39.840Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1201
File: src/components/Payment/Views/MantecaFulfillment.view.tsx:56-56
Timestamp: 2025-09-16T17:27:39.840Z
Learning: In the TRequestResponse interface (src/services/services.types.ts), recipientAccount is a required field, not optional. When requestDetails is not null, recipientAccount and its nested user.username properties are guaranteed to exist. Only requestDetails itself can be null (typed as TRequestResponse | null).

Applied to files:

  • src/components/Payment/PaymentForm/index.tsx
📚 Learning: 2025-07-07T20:22:11.092Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 958
File: src/app/actions/tokens.ts:266-266
Timestamp: 2025-07-07T20:22:11.092Z
Learning: In `src/app/actions/tokens.ts`, within the `fetchWalletBalances` function, using the non-null assertion operator `!` on `process.env.MOBULA_API_KEY!` is intentional and correct, and should not be flagged for replacement with explicit validation.

Applied to files:

  • src/components/Payment/PaymentForm/index.tsx
📚 Learning: 2025-05-19T19:40:43.138Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 868
File: src/components/Payment/PaymentForm/index.tsx:284-293
Timestamp: 2025-05-19T19:40:43.138Z
Learning: When converting between USD and token amounts, always check if the token price (divisor) is valid and non-zero before performing the division to prevent Infinity, NaN, or errors. Implementing validation like `if (!tokenPrice || isNaN(tokenPrice) || tokenPrice === 0)` before division operations is crucial for handling cases where price data might be unavailable.

Applied to files:

  • src/components/Payment/PaymentForm/index.tsx
📚 Learning: 2025-09-05T07:31:11.396Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1185
File: src/components/Claim/useClaimLink.tsx:14-0
Timestamp: 2025-09-05T07:31:11.396Z
Learning: In the peanut-ui codebase, `window.history.replaceState` is preferred over `router.replace` when immediate/synchronous URL parameter updates are required, as `router.replace` is asynchronous and doesn't guarantee instant URL changes that subsequent code can rely on. This pattern is used consistently across usePaymentInitiator.ts, Confirm.payment.view.tsx, and useClaimLink.tsx.

Applied to files:

  • src/components/Payment/PaymentForm/index.tsx
  • src/components/Common/ActionList.tsx
📚 Learning: 2024-12-11T10:13:22.806Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 564
File: src/components/Request/Pay/Views/Initial.view.tsx:430-430
Timestamp: 2024-12-11T10:13:22.806Z
Learning: In the React TypeScript file `src/components/Request/Pay/Views/Initial.view.tsx`, when reviewing the `InitialView` component, do not flag potential issues with using non-null assertion `!` on the `slippagePercentage` variable, as handling undefined values in this context is considered out of scope.

Applied to files:

  • src/components/Payment/PaymentForm/index.tsx
📚 Learning: 2025-11-04T17:47:06.328Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1396
File: src/app/(mobile-ui)/home/page.tsx:295-304
Timestamp: 2025-11-04T17:47:06.328Z
Learning: In src/app/(mobile-ui)/home/page.tsx, when closing the KycCompletedModal, updateUserById is called without awaiting to provide instant feedback to the user. This fire-and-forget pattern for modal dismissals and UI preference updates is intentional and consistent across the codebase—user experience with instant UI feedback takes priority over waiting for backend sync operations.

Applied to files:

  • src/components/Common/ActionList.tsx
🧬 Code graph analysis (2)
src/components/Global/DaimoPayButton/index.tsx (1)
src/constants/zerodev.consts.ts (1)
  • PEANUT_WALLET_TOKEN (20-20)
src/components/Payment/PaymentForm/index.tsx (3)
src/context/RequestFulfillmentFlowContext.tsx (1)
  • useRequestFulfillmentFlow (104-110)
src/constants/zerodev.consts.ts (2)
  • PEANUT_WALLET_CHAIN (18-18)
  • PEANUT_WALLET_TOKEN (20-20)
src/utils/general.utils.ts (1)
  • areEvmAddressesEqual (423-430)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (9)
src/components/Global/DaimoPayButton/index.tsx (2)

16-19: LGTM!

The new optional props are well-documented and appropriately typed for enabling cross-chain payment customization.


148-151: The review comment is incorrect—defensive validation is already implemented.

The code at line 151 already includes defensive handling: toToken={getAddress(toTokenAddress ?? PEANUT_WALLET_TOKEN)}. If toTokenAddress is undefined or null, the expression safely falls back to PEANUT_WALLET_TOKEN, preventing an invalid address from being passed to getAddress(). While viem's getAddress does throw when address is invalid, the ?? operator mitigates this by ensuring a fallback constant is used when the primary value is falsy. No additional validation or error handling is necessary.

Likely an incorrect or invalid review comment.

src/components/Common/ActionListDaimoPayButton.tsx (3)

20-20: LGTM!

The new clickHandlerRef prop enables external control of the Daimo button click handler, which aligns with the PR's integration pattern for triggering payments after state transitions.


156-157: LGTM!

The props are correctly passed through with appropriate null/undefined handling using optional chaining and conditional logic.


185-191: LGTM!

The ref assignment pattern correctly stores the onClick handler in both the internal ref and the parent-provided ref (when available), enabling external control of the Daimo button.

src/components/Common/ActionList.tsx (4)

111-112: LGTM!

The ref is properly typed and clearly documented for storing the Daimo button click handler for external triggering from the balance modal.


259-259: LGTM!

The removal of the 'exchange-or-wallet' case from the switch statement is appropriate since this method is now exclusively handled by the ActionListDaimoPayButton component (lines 315-335).


331-331: LGTM!

The clickHandlerRef is correctly passed to enable external triggering of the Daimo button from the parent component.


430-439: Verify the setTimeout pattern handles all edge cases.

The setTimeout(fn, 0) pattern defers execution until after state updates complete, which is appropriate here. The optional chaining on line 434 safely handles the case where the ref might be null.

However, consider if there are edge cases where the ref might not be populated yet or where state updates take longer than expected. The pattern should work for the typical event loop timing, but it's worth testing thoroughly.

@kushagrasarathe
Copy link
Contributor Author

@coderabbitai review in depth.

Evaluate:

  • risk of introducing new bugs and breaking existing functionality
  • performance impact and latency
  • code quality and DRY

Also, write a very concise testing guide for QA

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 12, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the modified files to analyze the changes in detail

echo "=== client.tsx ==="
cat "src/app/[...recipient]/client.tsx"

echo -e "\n\n=== ActionList.tsx ==="
cat "src/components/Common/ActionList.tsx"

echo -e "\n\n=== ActionListDaimoPayButton.tsx ==="
cat "src/components/Common/ActionListDaimoPayButton.tsx"

echo -e "\n\n=== DaimoPayButton/index.tsx ==="
cat "src/components/Global/DaimoPayButton/index.tsx"

echo -e "\n\n=== PaymentForm/index.tsx ==="
cat "src/components/Payment/PaymentForm/index.tsx"

Length of output: 111081


🏁 Script executed:

#!/bin/bash
# Check for any consumers of the removed context properties to identify potential breaking changes

echo "=== Checking for showExternalWalletFulfillMethods usage ==="
rg -n "showExternalWalletFulfillMethods" --type=tsx --type=ts -C 2

echo -e "\n\n=== Checking for externalWalletFulfillMethod usage ==="
rg -n "externalWalletFulfillMethod" --type=tsx --type=ts -C 2

echo -e "\n\n=== Checking for ExternalWalletFulfilManager imports ==="
rg -n "ExternalWalletFulfilManager" --type=tsx --type=ts -C 2

echo -e "\n\n=== Checking for ExternalWalletFulfilMethods imports ==="
rg -n "ExternalWalletFulfilMethods" --type=tsx --type=ts -C 2

Length of output: 346


🏁 Script executed:

#!/bin/bash
# Check the context file to see what was removed

echo "=== RequestFulfillmentFlowContext.tsx ==="
cat "src/context/RequestFulfillmentFlowContext.tsx"

Length of output: 4533


🏁 Script executed:

#!/bin/bash
# Look for other payment flow related files that might be affected

echo "=== Finding payment flow related files ==="
fd -e tsx -e ts "payment" --type f | head -20

echo -e "\n\n=== Finding request flow related files ==="
fd -e tsx -e ts "request" --type f | grep -i "view\|manager" | head -20

Length of output: 755


🏁 Script executed:

#!/bin/bash
# Check for any test files related to these components

echo "=== Looking for test files ==="
fd -e test.tsx -e test.ts -e spec.tsx -e spec.ts --type f | grep -i "payment\|action\|daimo" | head -20

echo -e "\n\n=== Checking for any TODO or FIXME comments in modified files ==="
rg -n "TODO|FIXME" "src/app/[...recipient]/client.tsx" "src/components/Common/ActionList.tsx" "src/components/Common/ActionListDaimoPayButton.tsx" "src/components/Global/DaimoPayButton/index.tsx" "src/components/Payment/PaymentForm/index.tsx"

Length of output: 279


🏁 Script executed:

#!/bin/bash
# Check for any remaining references to removed components/types
echo "=== Searching for ExternalWalletFulfil references ==="
rg -n "ExternalWalletFulfil" --type-add 'web:*.{ts,tsx,js,jsx}' --type web -C 2

echo -e "\n\n=== Searching for exchange-or-wallet method references ==="
rg -n "exchange-or-wallet" --type-add 'web:*.{ts,tsx,js,jsx}' --type web -C 2

echo -e "\n\n=== Check for Daimo integration points ==="
rg -n "DaimoPay" --type-add 'web:*.{ts,tsx,js,jsx}' --type web -g '!node_modules' | head -30

echo -e "\n\n=== Check for token selector usage ==="
rg -n "TokenSelector" --type-add 'web:*.{ts,tsx,js,jsx}' --type web -C 1 | head -40

Length of output: 10045


🏁 Script executed:

#!/bin/bash
# Check for potential race conditions with refs and state
echo "=== Checking useRef usage patterns ==="
rg -n "useRef|clickHandlerRef|daimoButtonClickRef" --type-add 'web:*.{ts,tsx,js,jsx}' --type web src/components/Common/ActionList.tsx src/components/Common/ActionListDaimoPayButton.tsx -C 3

echo -e "\n\n=== Check setTimeout usage (potential race condition indicator) ==="
rg -n "setTimeout" --type-add 'web:*.{ts,tsx,js,jsx}' --type web src/components/Common/ActionList.tsx -C 3

Length of output: 7047


🏁 Script executed:

#!/bin/bash
# Check for chain ID and token address handling patterns
echo "=== Checking chain ID handling ==="
rg -n "toChainId|chainId|PEANUT_WALLET_CHAIN" --type-add 'web:*.{ts,tsx,js,jsx}' --type web src/components/Payment/PaymentForm/index.tsx | head -30

echo -e "\n\n=== Checking token address handling ==="
rg -n "toTokenAddress|tokenAddress|PEANUT_WALLET_TOKEN" --type-add 'web:*.{ts,tsx,js,jsx}' --type web src/components/Payment/PaymentForm/index.tsx | head -30

Length of output: 1385


🏁 Script executed:

#!/bin/bash
# Check action list constants to understand available methods
echo "=== Checking ACTION_METHODS constants ==="
cat src/constants/actionlist.consts.ts | head -100

echo -e "\n\n=== Checking payment constants ==="
cat src/constants/payment.consts.ts

Length of output: 2987


🏁 Script executed:

#!/bin/bash
# Check for any error handling patterns
echo "=== Checking error handling in modified files ==="
rg -n "catch|error|Error" --type-add 'web:*.{ts,tsx,js,jsx}' --type web src/components/Common/ActionListDaimoPayButton.tsx -C 2 | head -50

echo -e "\n\n=== Checking dispatch patterns in PaymentForm ==="
rg -n "dispatch\(paymentActions" --type-add 'web:*.{ts,tsx,js,jsx}' --type web src/components/Payment/PaymentForm/index.tsx | head -30

Length of output: 4313


@kushagrasarathe, I've completed an in-depth review of this PR. Here's my analysis:

🚨 Critical Issues

1. Chain ID Fallback Logic Bug (High Priority)

Location: ActionListDaimoPayButton.tsx:121

destinationchainId: Number(chargeDetails.chainId) ?? Number(daimoPaymentResponse.payment.destination.chainId)

Problem: The nullish coalescing operator (??) doesn't work as intended here. Number(chargeDetails.chainId) can return 0 or NaN, which are falsy but not nullish, so the fallback won't trigger properly.

Fix:

destinationchainId: chargeDetails.chainId 
    ? Number(chargeDetails.chainId) 
    : Number(daimoPaymentResponse.payment.destination.chainId)

2. Race Condition with setTimeout (High Priority)

Location: ActionList.tsx:432-435

setTimeout(() => {
    daimoButtonClickRef.current?.()
}, 0)

Problem: Using setTimeout(0) to "ensure state updates are processed" is unreliable. React state updates are asynchronous, and there's no guarantee they're complete after one event loop tick. This could cause the Daimo modal to open with stale state.

Recommendation: Use React's state update callbacks or useEffect to properly sequence operations:

// Instead of setTimeout, trigger from useEffect
useEffect(() => {
    if (shouldTriggerDaimo && daimoButtonClickRef.current) {
        daimoButtonClickRef.current()
        setShouldTriggerDaimo(false)
    }
}, [shouldTriggerDaimo])

3. Missing Null Safety in Division Operations (Medium Priority)

Location: PaymentForm/index.tsx:485-502

if (isNaN(requestedTokenPriceData.price) || requestedTokenPriceData.price === 0) {
    // error handling
}
const usdAmount = parseFloat(inputUsdValue)
tokenAmount = (usdAmount / requestedTokenPriceData.price).toString()

Problem: While there's validation, the division still happens outside the validated scope. If requestedTokenPriceData is undefined or null, accessing .price will throw.

Fix: Add explicit null checks before any property access.


⚠️ High-Risk Areas

4. Complex Balance Check Logic

Location: PaymentForm/index.tsx:223-318

The balance checking logic has 96 lines with deeply nested conditions checking:

  • View state (CONFIRM, STATUS)
  • Processing state
  • Pending transactions
  • External wallet flow
  • Peanut wallet vs external wallet
  • Token types and recipient types
  • Multiple balance sources

Risk: Very difficult to reason about all edge cases. High probability of bugs when conditions are added/modified.

Recommendation: Extract into a separate hook with clear responsibilities:

const useBalanceValidation = ({ 
    amount, 
    isExternalWallet, 
    recipientType,
    currentView,
    isProcessing 
}) => {
    // Centralized, testable balance logic
}

5. Default Chain/Token Race Condition

Location: PaymentForm/index.tsx:182-206

if (isExternalRecipient && !selectedChainID) {
    setSelectedChainID(PEANUT_WALLET_CHAIN.id.toString())
}
// Later...
const chainData = supportedSquidChainsAndTokens[selectedChainID]

Problem: If supportedSquidChainsAndTokens hasn't loaded yet (it's from an async hook), chainData will be undefined and the default token won't be set. Users might see a broken token selector.

Fix: Add loading/fallback logic or ensure data is loaded before component renders.


📊 Performance Concerns

6. Frequent Balance Check Re-renders

Location: PaymentForm/index.tsx:223-318

The balance check useEffect has 20+ dependencies, including:

  • selectedTokenBalance
  • peanutWalletBalance
  • selectedTokenAddress
  • inputTokenAmount
  • selectedTokenData
  • currentView
  • isProcessing
  • etc.

Impact: This effect will fire very frequently, potentially on every keystroke in the amount input, causing:

  • Repeated validation calculations
  • Multiple dispatch calls
  • Unnecessary re-renders

Recommendation: Debounce the validation or split into smaller, more targeted effects.

7. setTimeout Adds Latency

Every time the balance modal flow is used with Daimo, there's an artificial delay introduced by setTimeout(0). While minimal (~4ms), it's unnecessary and creates a sluggish UX.


🧹 Code Quality Issues

8. Massive Component Violation (Critical)

  • PaymentForm/index.tsx: 922 lines
  • ActionList.tsx: 473 lines

These components violate Single Responsibility Principle and are:

  • Difficult to test
  • Hard to maintain
  • Easy to introduce bugs in

Recommendation: Break down into smaller components:

PaymentForm/
├── index.tsx (orchestration only)
├── components/
│   ├── RecipientCard.tsx
│   ├── AmountInput.tsx
│   ├── TokenSelection.tsx
│   ├── BalanceDisplay.tsx
│   └── ActionButtons.tsx
└── hooks/
    ├── usePaymentValidation.ts
    └── useBalanceCheck.ts

9. DRY Violations

Duplicate Default Logic: Chain/token defaults for external recipients appear in multiple places with slight variations.

Duplicate Error Handling: Some places use console.error + dispatch(paymentActions.setError()), others just one or the other.

Duplicate Type Checks: recipient?.recipientType === 'ADDRESS' || recipient?.recipientType === 'ENS' appears 8+ times.

Fix: Extract to constants/helpers:

const isExternalRecipient = (recipient) => 
    ['ADDRESS', 'ENS'].includes(recipient?.recipientType)

const DEFAULT_EXTERNAL_CHAIN = PEANUT_WALLET_CHAIN.id.toString()
const getDefaultToken = (chainId, tokens) => 
    tokens.find(t => t.symbol.toLowerCase() === 'usdc')

10. Magic Values

  • Chain IDs hardcoded in multiple places
  • Token addresses duplicated
  • Minimum amounts scattered across components

11. Incomplete Migration

Location: ActionList.tsx:214

// @dev TODO: Fix req fulfillment with bank properly post devconnect

This TODO indicates the bank fulfillment flow may not be properly tested or complete. This is concerning for a PR that's supposed to fix payment flows.


✅ Positive Aspects

  1. Clean Removal: No dangling references to removed components
  2. Type Safety: Proper TypeScript usage throughout
  3. Prop Threading: New clickHandlerRef prop is well-typed and optional
  4. Consistent Patterns: Uses modern React patterns (hooks, functional components)

🧪 Concise QA Testing Guide

Critical Path Tests

Test 1: Cross-Chain Daimo Payment (REQUEST flow)

  1. Login to Peanut Wallet with sufficient balance ($10 USDC)
  2. Navigate to a semantic request payment link (ADDRESS/ENS recipient)
  3. Enter amount > $5
  4. Select "Exchange or Wallet" from action list
  5. Verify: Balance modal appears asking to use Peanut balance
  6. Click "Continue" (not "Pay with Peanut")
  7. Verify: Daimo modal opens without errors
  8. Verify: Correct destination chain is shown (from URL, not default)
  9. Complete payment
  10. Verify: Status view shows success
  11. Verify: Correct chain ID reflected in payment details

Test 2: Token Selector for External Recipients

  1. Navigate to /send/0x[valid-ethereum-address]
  2. Verify: Token selector appears
  3. Verify: Defaults to Arbitrum + USDC
  4. Change to different chain (e.g., Base)
  5. Verify: Token list updates
  6. Select non-USDC token
  7. Verify: Warning appears about free USDC transactions
  8. Enter amount and complete payment
  9. Verify: Payment uses selected chain/token

Test 3: Peanut Balance Modal Bypass

  1. Create request for $5
  2. Ensure Peanut wallet has < $5 balance
  3. Select "Exchange or Wallet"
  4. Verify: No balance modal appears (insufficient funds)
  5. Verify: Daimo modal opens directly
  6. Repeat with $10 balance
  7. Verify: Balance modal appears
  8. Click "Pay with Peanut"
  9. Verify: Payment completes via Peanut wallet (not Daimo)

Test 4: Request Pot Payment

  1. Navigate to request pot link with existing contributors
  2. Verify: Contributor cards display
  3. Verify: Amount slider defaults to median contribution
  4. Adjust slider to 33%
  5. Click "Choose payment method"
  6. Verify: Action list appears with correct methods
  7. Complete payment via any method
  8. Verify: Contribution shows in pot

Test 5: Invite Flow Integrity

  1. As guest user, navigate to request from user who can invite
  2. Select "Exchange or Wallet"
  3. Verify: Invite modal appears
  4. Click "Continue with Peanut"
  5. Verify: Redirected to signup with correct invite code
  6. Complete signup
  7. Verify: Redirected back to payment
  8. Verify: Payment completes successfully

Edge Cases

Test 6: Invalid Chain ID Fallback

  1. Navigate to request with invalid chainId param in URL
  2. Verify: Graceful fallback to Arbitrum (no crashes)
  3. Verify: Error message if applicable

Test 7: Missing Token Price Data

  1. Use browser dev tools to block token price API
  2. Try to make payment
  3. Verify: Error message displays
  4. Verify: "Retry" button appears and works

Test 8: Rapid Amount Changes

  1. Open payment form
  2. Rapidly type amount: "1", "10", "100", "1000"
  3. Verify: No crashes or race conditions
  4. Verify: Balance validation updates correctly
  5. Verify: Final amount is accurate

Test 9: Concurrent State Updates

  1. Open Daimo modal, don't complete
  2. Change network/account in wallet
  3. Verify: Modal closes or shows error
  4. Verify: Can retry payment

Regression Tests

Test 10: Bank/Mercado Pago/Pix Flows Still Work

  1. Test each method with amounts < $5
  2. Verify: Minimum amount error displays
  3. Test with amounts > $5
  4. Verify: Each flow completes successfully

Test 11: Username Payments Unaffected

  1. Send payment to /@username
  2. Verify: No token selector appears (should use USDC always)
  3. Verify: Payment completes normally

📝 Recommendations Summary

Before Merge:

  1. Fix the chain ID fallback logic bug (added wagmi-->ethers signer conversion for sdk compatibility #1)
  2. Address race condition with refs/setTimeout (Fetch Chain details from SDK #2)
  3. Add null safety checks (hotfix: sdk version update #3)
  4. Test the incomplete bank fulfillment TODO (patch: retroactively add GA #11)

Post-Merge (Technical Debt):

  1. Extract balance validation into separate hook (Claim #4)
  2. Split PaymentForm and ActionList into smaller components (fix: peanutman and header #8)
  3. Create shared constants for DRY violations (feat: initialise GA #9)
  4. Add integration tests (currently minimal test coverage)
  5. Implement debouncing for balance checks (fix: more ui work #6)

Estimated Code Review Effort: High (complex state management, multiple integration points, large components)

Overall Risk Level: Medium-High (critical bugs present, but clean architecture changes otherwise)


🧠 Learnings used
Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 852
File: src/hooks/useTransactionHistory.ts:177-239
Timestamp: 2025-05-14T21:16:06.369Z
Learning: When reviewing code, pay attention to and resolve CodeRabbit comments as they can contain useful refactoring suggestions and insights.

Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 594
File: src/components/Request/Pay/Views/Initial.view.tsx:188-213
Timestamp: 2024-12-31T15:08:15.640Z
Learning: Always resolve coderabbit comments when reviewing code, since they can contain useful suggestions.

Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 919
File: src/components/Withdraw/views/Initial.withdraw.view.tsx:87-87
Timestamp: 2025-06-18T19:56:55.443Z
Learning: In withdraw flows for Peanut Wallet, the PeanutActionDetailsCard should always display "USDC" as the token symbol because it shows the amount being withdrawn from the Peanut Wallet (which holds USDC), regardless of the destination token/chain selected by the user. The TokenSelector is used for choosing the withdrawal destination, not the source display.

// for exchange-or-wallet, trigger daimo button after state updates
if (selectedPaymentMethod.id === 'exchange-or-wallet' && daimoButtonClickRef.current) {
// use setTimeout to ensure state updates are processed before triggering daimo
setTimeout(() => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

not using a useeffect here cuz it will just add unnecessary complexity, setimeout a bit hacky but it makes sure state is updated properly

Copy link
Contributor

@jjramirezn jjramirezn left a comment

Choose a reason for hiding this comment

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

lgtm

@Hugo0
Copy link
Contributor

Hugo0 commented Nov 12, 2025

@coderabbitai do a thorough review and risk analysis. What bugs could this introduce in existing payment flows?

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 12, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all payment flow entry points and consumers

echo "=== Finding all payment-related component usages ==="
rg -n "ActionList" --type-add 'web:*.{ts,tsx}' --type web -g '!node_modules' -g '!*.test.*' | head -40

echo -e "\n\n=== Finding PaymentForm usages ==="
rg -n "PaymentForm" --type-add 'web:*.{ts,tsx}' --type web -g '!node_modules' -g '!*.test.*' | head -30

echo -e "\n\n=== Finding all recipient type checks ==="
rg -nP "recipientType\s*===\s*['\"]" --type-add 'web:*.{ts,tsx}' --type web -g '!node_modules' | head -40

echo -e "\n\n=== Finding all payment method handling ==="
rg -n "handleMethodClick|METHOD_" --type-add 'web:*.{ts,tsx}' --type web src/components/Common/ActionList.tsx -C 3

echo -e "\n\n=== Checking for balance-related logic ==="
rg -n "peanutWalletBalance|selectedTokenBalance" --type-add 'web:*.{ts,tsx}' --type web -g '!node_modules' | head -30

Length of output: 13592


🏁 Script executed:

#!/bin/bash
# Check for all flows that use RequestFulfillmentFlowContext

echo "=== Finding RequestFulfillmentFlowContext consumers ==="
rg -n "useRequestFulfillmentFlow" --type-add 'web:*.{ts,tsx}' --type web -g '!node_modules' -C 2 | head -60

echo -e "\n\n=== Finding ReqFulfillBankFlowManager usage ==="
rg -n "ReqFulfillBankFlowManager" --type-add 'web:*.{ts,tsx}' --type web -g '!node_modules' -C 3

echo -e "\n\n=== Checking for other manager components ==="
fd -e tsx "Manager" src/components/Request/views/

Length of output: 8525


🏁 Script executed:

#!/bin/bash
# Analyze token selection and chain handling

echo "=== Finding all chain selection logic ==="
rg -n "setSelectedChainID|selectedChainID" --type-add 'web:*.{ts,tsx}' --type web src/components/Payment/PaymentForm/index.tsx -C 2 | head -80

echo -e "\n\n=== Finding USDC token references ==="
rg -n "USDC|usdc" --type-add 'web:*.{ts,tsx}' --type web src/components/Payment/PaymentForm/index.tsx | head -30

echo -e "\n\n=== Checking TokenSelector usage and props ==="
cat src/components/Payment/TokenSelector/index.tsx | head -100

Length of output: 3746


🏁 Script executed:

#!/bin/bash
# Check for error handling and validation patterns

echo "=== Checking dispatch error patterns ==="
rg -n "dispatch\(paymentActions\.set" --type-add 'web:*.{ts,tsx}' --type web src/components/Payment/PaymentForm/index.tsx

echo -e "\n\n=== Checking balance validation patterns ==="
rg -n "canInitiatePayment|isBalanceSufficient" --type-add 'web:*.{ts,tsx}' --type web src/components/Payment/PaymentForm/index.tsx -C 3

echo -e "\n\n=== Finding all view state transitions ==="
rg -n "setCurrentView|currentView" --type-add 'web:*.{ts,tsx}' --type web src/components/Payment/PaymentForm/index.tsx | head -40

Length of output: 3049


🏁 Script executed:

#!/bin/bash
# Check for other payment flows that might be affected

echo "=== Finding all payment-related views ==="
fd -e tsx "view.tsx" src/components/Payment/

echo -e "\n\n=== Finding claim flow interactions ==="
rg -n "claimType|isClaimFlow" --type-add 'web:*.{ts,tsx}' --type web src/components/Common/ActionList.tsx -C 3

echo -e "\n\n=== Checking for bank/pix/mercadopago references ==="
rg -n "bank|pix|mercado" --type-add 'web:*.{ts,tsx}' --type web src/components/Common/ActionList.tsx -i | head -30

Length of output: 4089


🏁 Script executed:

#!/bin/bash
# Look for tests related to these components

echo "=== Finding test files ==="
fd -e test.tsx -e test.ts -e spec.tsx -e spec.ts . src/ | head -20

echo -e "\n\n=== Checking for existing payment flow tests ==="
rg -n "describe|test|it\(" --type-add 'web:*.{test.ts,test.tsx,spec.ts,spec.tsx}' --type web src/ | grep -i "payment\|action\|daimo" | head -30

Length of output: 4612


🏁 Script executed:

#!/bin/bash
# Check for potential state synchronization issues

echo "=== Finding all useState in PaymentForm ==="
rg -n "useState" --type-add 'web:*.{ts,tsx}' --type web src/components/Payment/PaymentForm/index.tsx

echo -e "\n\n=== Finding all useEffect in PaymentForm ==="
rg -n "useEffect" --type-add 'web:*.{ts,tsx}' --type web src/components/Payment/PaymentForm/index.tsx

echo -e "\n\n=== Checking ActionList state management ==="
rg -n "useState|useEffect" --type-add 'web:*.{ts,tsx}' --type web src/components/Common/ActionList.tsx | head -40

Length of output: 1716


@Hugo0, I've conducted a thorough risk analysis. Here are the specific bugs this could introduce in existing payment flows:

🚨 Critical Risk: Existing Payment Flows

1. Bank/Pix/Mercado Pago Broken for External Recipients (HIGH SEVERITY)

Affected Flow: Request payment to ADDRESS/ENS with bank transfer

Bug Scenario:

  1. User creates request to 0x123... (ADDRESS recipient)
  2. New code adds token selector, defaults to Arbitrum + USDC
  3. Payer selects "Bank Transfer" as payment method
  4. User changes chain to Base or Optimism in token selector
  5. CRASH: Bank fulfillment only supports specific chain/token configs (mapped to fiat rails), not arbitrary chains

Root Cause: Token selector is shown for external recipients, but bank/pix/mercado methods don't support cross-chain. No validation prevents incompatible chain selection.

Evidence from code:

// PaymentForm line 842: Token selector rendered for external recipients
(recipient?.recipientType === 'ADDRESS' || recipient?.recipientType === 'ENS') && (
    <TokenSelector viewType="payment" />
)

// But bank methods are still available in ActionList
case 'bank': // No check that selected chain is compatible!

Who's affected: Any payer trying to use bank/pix/mercado for ADDRESS/ENS recipients.


2. Balance Modal -> Daimo Flow Broken (HIGH SEVERITY)

Affected Flow: Request payment with Peanut balance modal

Bug Scenario:

  1. User has $10 in Peanut wallet
  2. Request is for $8 to USERNAME recipient
  3. User clicks "Exchange or Wallet"
  4. Balance modal shows: "Use your $10 balance?"
  5. User clicks "Continue" (wants to use Daimo, not Peanut)
  6. Code tries to call daimoButtonClickRef.current?.()
  7. BUG: If ActionListDaimoPayButton isn't rendered (conditions not met), ref is null → nothing happens, user stuck

Root Cause: Line 432-435 in ActionList.tsx relies on ref being set, but doesn't handle case where ref is null:

if (
    selectedPaymentMethod?.id === 'exchange-or-wallet' &&
    daimoButtonClickRef.current
) {
    setTimeout(() => {
        daimoButtonClickRef.current?.() // What if this is null?
    }, 0)
}

Additional Risk: The 'exchange-or-wallet' method was removed from handleMethodClick switch statement (line 259), so fallback behavior is gone. If ref fails, user is stuck with no error message.

Who's affected: Users with Peanut wallet balance trying to pay via Daimo.


3. Request Pot + External Recipients = Conflict (MEDIUM SEVERITY)

Affected Flow: Request pot to ADDRESS recipient

Bug Scenario:

  1. User creates request pot with address 0x123... as recipient
  2. Request pot has 5 contributors with median of $20
  3. New contributor opens link
  4. BUG: Token selector appears for external recipient, allowing chain changes
  5. Contributor selects Polygon + MATIC
  6. But request pot logic expects all contributions in same token/chain
  7. Payment might succeed but accounting breaks, pot total incorrect

Root Cause: Token selector logic doesn't check if it's a request pot scenario:

// Line 181-206: External recipient gets token selector
if (isExternalRecipient && !selectedChainID) {
    setSelectedChainID(PEANUT_WALLET_CHAIN.id.toString())
}
// But no check: if (isRequestPotLink) { /* don't allow token changes */ }

Who's affected: Request pot creators using ADDRESS/ENS recipients.


4. Username Payments Get Token Selector (Logic Leak) (MEDIUM SEVERITY)

Affected Flow: Regular username payment

Bug Scenario:

  1. User pays /@alice for $50
  2. BUG: If any recipient type check fails or is bypassed, token selector appears
  3. User changes chain/token from default USDC Arbitrum
  4. Payment fails because username payments MUST use USDC on Arbitrum

Root Cause: Token selector rendering has duplicate checks (line 842):

(recipient?.recipientType === 'ADDRESS' || recipient?.recipientType === 'ENS')

But if recipient is malformed, undefined, or type checking fails, this evaluates to falsy and might skip validation elsewhere. With 8+ duplicate checks across codebase, one failure cascades.

Who's affected: All username payments if recipient parsing fails.


5. Invite Flow Breaks for External Recipients (MEDIUM SEVERITY)

Affected Flow: Guest user paying ADDRESS request from inviter

Bug Scenario:

  1. Alice invites Bob via request to Bob's address 0xBob...
  2. Bob (guest) opens link, sees payment form
  3. Bob clicks "Exchange or Wallet"
  4. Invite modal shows: "Alice can invite you!"
  5. Bob accepts invite, gets redirected to signup
  6. BUG: After signup, Bob returns to payment
  7. Token selector now shows (external recipient) but state is stale
  8. Bob's selected chain/token don't match what was initialized before invite flow
  9. Payment fails or wrong token used

Root Cause: Invite flow doesn't preserve token/chain selection through redirect:

// ActionList.tsx line 383-389: After invite modal
handleMethodClick(selectedMethod) // Re-triggers method
// But token selector state might have changed during invite flow

Who's affected: Guests accepting invites for ADDRESS/ENS payments.


6. Bank Fulfillment Incomplete (CRITICAL - Per TODO)

Affected Flow: Request fulfillment via bank transfer

Evidence: Line 214 in ActionList.tsx:

// @dev TODO: Fix req fulfillment with bank properly post devconnect
// if (method.id === 'bank') {

Risk: The bank fulfillment flow for requests is commented out and marked TODO. This PR doesn't address it. If users try bank fulfillment for requests, behavior is undefined:

  • Might fall through to wrong flow
  • Might silently fail
  • Might show no error

Who's affected: Anyone using bank transfer to fulfill a request (not claim).


7. Daimo Chain/Token No Validation (MEDIUM SEVERITY)

Affected Flow: Cross-chain Daimo payment

Bug Scenario:

  1. User pays ADDRESS recipient
  2. Selects chain: ZKSync (hypothetical, not supported by Daimo)
  3. Selects token: DAI
  4. Clicks pay, Daimo modal opens
  5. BUG: Daimo SDK receives toChainId: 324, toTokenAddress: 0xDAI...
  6. Daimo doesn't support ZKSync → SDK error or silent failure
  7. User stuck, no clear error message

Root Cause: No validation that selected chain/token are supported by Daimo:

// ActionListDaimoPayButton.tsx line 181-190
<DaimoPayButton
    toChainId={parsedPaymentData.chain.chainId} // No validation!
    toTokenAddress={parsedPaymentData.token.address} // No validation!
/>

Who's affected: Users selecting non-Daimo-supported chains.


🔴 State Synchronization Risks

8. Race Condition: Balance Check vs Token Selection (MEDIUM SEVERITY)

Bug Scenario:

  1. User types amount: "$50"
  2. Balance check effect fires (line 223-318, 20+ dependencies)
  3. User rapidly changes token: USDC → DAI → WETH
  4. Each change triggers balance check
  5. Multiple dispatch(paymentActions.setError()) calls in flight
  6. BUG: Last error might not reflect current token selection
  7. Shows "Insufficient balance" even though current token has enough

Root Cause: No debouncing on balance validation, fires on every keystroke/change:

// Line 223: Effect runs on ANY dependency change
useEffect(() => {
    // Complex validation...
    dispatch(paymentActions.setError(...))
}, [
    selectedTokenBalance, // Changes on every token switch
    inputTokenAmount, // Changes on every keystroke
    // ...18 more dependencies
])

9. setTimeout Race in Balance Modal (Already Identified, Expanded)

Additional Risk Beyond Previous Review:
The setTimeout(0) pattern assumes React state updates complete in one tick. But if:

  • Network request pending (token price fetch)
  • Multiple state updates queued
  • Browser throttling event loop

Then Daimo modal opens with:

  • Wrong amount displayed
  • Wrong token selected
  • Stale balance data

Evidence: Line 432-435:

setTimeout(() => {
    daimoButtonClickRef.current?.() // Assumes state is ready
}, 0)

No check that selectedTokenAddress, selectedChainID, inputTokenAmount are in sync.


📊 Risk Matrix by Flow Type

Flow Type Risk Level Primary Concern Mitigation Priority
Username → Username 🟡 Low Token selector leak Medium
Username → ADDRESS 🔴 High Bank/balance modal bugs Urgent
Address → ADDRESS 🔴 High All issues compound Urgent
Request Pot 🟠 Medium Token consistency High
Bank/Pix/Mercado 🔴 Critical Chain compatibility Urgent
Daimo Cross-Chain 🟠 Medium Validation missing High
Invite Flow 🟠 Medium State preservation High

🧪 Missing Test Coverage

Critical Gap: Zero integration tests found for:

  • ActionList payment method switching
  • PaymentForm token selection
  • Balance modal flow
  • Daimo button integration

Risk: All bugs above will only be caught in production.


✅ Recommended Pre-Merge Actions

Must Fix Before Merge:

  1. Add validation: Disable bank/pix/mercado for external recipients with custom chains:
if (isExternalRecipient && selectedChainID !== PEANUT_WALLET_CHAIN.id) {
    // Hide bank/pix/mercado from action list
}
  1. Fix balance modal fallback: Add error handling if ref is null:
if (daimoButtonClickRef.current) {
    daimoButtonClickRef.current()
} else {
    dispatch(paymentActions.setError('Payment method unavailable'))
    setShowUsePeanutBalanceModal(false)
}
  1. Fix chain ID bug: Use explicit check (already mentioned):
destinationchainId: chargeDetails.chainId 
    ? Number(chargeDetails.chainId) 
    : Number(daimoPaymentResponse.payment.destination.chainId)
  1. Disable token selector for request pots:
if (isRequestPotLink) {
    // Don't render token selector
}
  1. Address bank fulfillment TODO: Either implement or disable bank method for requests.

Should Add Soon:

  1. Replace setTimeout with proper state management
  2. Add Daimo chain/token validation
  3. Debounce balance checks
  4. Add integration tests for at least 3 critical flows

🎯 Testing Priority (For QA)

Test these flows FIRST (highest risk):

  1. ✅ Bank transfer to ADDRESS recipient with default + non-default chains
  2. ✅ Balance modal → Daimo flow with insufficient/sufficient balance
  3. ✅ Request pot to ADDRESS with multiple contributors
  4. ✅ Invite flow for ADDRESS recipient
  5. ✅ Username payment that everything still works (regression)

Automated test scenarios (should write):

describe('PaymentForm - External Recipients', () => {
    it('should hide bank methods when non-default chain selected', () => {
        // Render with ADDRESS recipient
        // Change chain to Base
        // Assert bank/pix/mercado not in action list
    })
    
    it('should preserve token selection through invite flow', () => {
        // Select Polygon + USDC
        // Trigger invite modal
        // Complete invite
        // Assert chain/token still Polygon/USDC
    })
})

Bottom Line: This PR introduces 7 high-severity bugs in existing flows due to insufficient validation, missing null checks, and complex state interactions. The lack of tests means these will likely surface in production. Do not merge until at least the 5 "Must Fix" items are addressed.

Would you like me to create follow-up issues for the medium-priority items, or should we block this PR until all critical issues are resolved?


🧠 Learnings used
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 564
File: src/components/Request/Pay/Views/Initial.view.tsx:430-430
Timestamp: 2024-12-11T10:13:22.806Z
Learning: In the React TypeScript file `src/components/Request/Pay/Views/Initial.view.tsx`, when reviewing the `InitialView` component, do not flag potential issues with using non-null assertion `!` on the `slippagePercentage` variable, as handling undefined values in this context is considered out of scope.

Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 551
File: src/components/Request/Create/Views/Initial.view.tsx:151-156
Timestamp: 2024-12-02T17:19:18.532Z
Learning: In the `InitialView` component at `src/components/Request/Create/Views/Initial.view.tsx`, when setting the default chain and token in the `useEffect` triggered by `isPeanutWallet`, it's acceptable to omit the setters from the dependency array and not include additional error handling for invalid defaults.

Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 852
File: src/hooks/useTransactionHistory.ts:177-239
Timestamp: 2025-05-14T21:16:06.369Z
Learning: When reviewing code, pay attention to and resolve CodeRabbit comments as they can contain useful refactoring suggestions and insights.

Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 535
File: src/components/Claim/Claim.tsx:142-146
Timestamp: 2024-11-18T21:36:11.486Z
Learning: In `src/components/Claim/Claim.tsx`, external calls like token price fetching and cross-chain details retrieval are already encapsulated within existing `try...catch` blocks, so additional error handling may be unnecessary.

Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 413
File: src/components/Request/Pay/Views/Initial.view.tsx:71-72
Timestamp: 2024-10-04T13:10:49.199Z
Learning: In `src/components/Request/Pay/Views/Initial.view.tsx`, it's acceptable to use the `!` operator in TypeScript to assert that `selectedTokenData` is not `null` or `undefined`, and potential runtime errors from accessing its properties without checks can be disregarded.

Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 413
File: src/components/Request/Pay/Views/Initial.view.tsx:71-72
Timestamp: 2024-10-08T20:13:42.967Z
Learning: In `src/components/Request/Pay/Views/Initial.view.tsx`, it's acceptable to use the `!` operator in TypeScript to assert that `selectedTokenData` is not `null` or `undefined`, and potential runtime errors from accessing its properties without checks can be disregarded.

Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 958
File: src/app/actions/tokens.ts:266-266
Timestamp: 2025-07-07T20:22:11.092Z
Learning: In `src/app/actions/tokens.ts`, within the `fetchWalletBalances` function, using the non-null assertion operator `!` on `process.env.MOBULA_API_KEY!` is intentional and correct, and should not be flagged for replacement with explicit validation.

Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1368
File: src/components/Common/ActionList.tsx:109-111
Timestamp: 2025-10-29T11:27:59.248Z
Learning: In `src/components/Common/ActionList.tsx`, the `balance` from `useWallet()` hook is always in USDC (as a formatted string), making it directly comparable to USD amounts without conversion. The comparison `Number(balance) >= amountInUsd` is intentional and correct.

Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1132
File: src/app/[...recipient]/client.tsx:394-397
Timestamp: 2025-08-26T15:25:53.328Z
Learning: In `src/components/Common/ActionListDaimoPayButton.tsx`, the `handleCompleteDaimoPayment` function should not display error messages to users when DB update fails because the Daimo payment itself has succeeded - showing errors would be confusing since the payment was successful.

Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 942
File: src/components/AddMoney/consts/index.ts:2151-2162
Timestamp: 2025-06-30T10:44:08.048Z
Learning: Hugo0 often agrees with refactoring suggestions but defers implementation due to time constraints, preferring to track improvements as follow-up issues when they're part of larger architectural changes.

@kushagrasarathe kushagrasarathe merged commit 24f5956 into peanut-wallet-dev Nov 12, 2025
5 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.

3 participants