Skip to content

[TASK-14052] Prod release 105#1121

Closed
jjramirezn wants to merge 17 commits intopeanut-walletfrom
peanut-wallet-dev
Closed

[TASK-14052] Prod release 105#1121
jjramirezn wants to merge 17 commits intopeanut-walletfrom
peanut-wallet-dev

Conversation

@jjramirezn
Copy link
Contributor

No description provided.

kushagrasarathe and others added 17 commits August 13, 2025 16:52
* reafactor: create reusable country list component and use it for all the flows

* feat: reusable user accounts components

* feat: handle different cases based on kyc status for bank claim

* fix: account creation

* chore: add docstring to hooks

* chore: better comments for bank flow manager

* fix: kyc modal closing after tos acceptance issue

* fix: remove bank acc caching from withdraw flow

* fix: update confirm claim modal copy

* fix: remove bank acc caching from claim flow

* fix: navheader title
* lpv2.1 part 1

* Add exchange widget

* add and integrate exchange API

* add yourMoney component bg

* update landing countries svg

* integrate frankfurter API

* fixes and improvements

* decrease hero section height

* allow max 2 decimal places

* Add `/exchange` route

* fix: overlay

* make destination amount editable and bugg fixes

* some fixes & currency improvements

* crucial commit

* fix checkmark, font size and weight

---------

Co-authored-by: Hugo Montenegro <h@hugo0.com>
)

* refactor: use networkName instead of axelarChainName

* fix: types
* fix: scientific notation in eip681 parsing

* fix: qr handling tests

* fix: peanut sdk mock
* fix: cross chain claim

* fix: full name issue on confirm bank claim view

* fix: back navigation on desktop views
* Fix back button not working

* fix public profile page

* extract internal navigation logic to utility function
* fix: usa bank account claims

* fix: show bank account details in confirm claim view
* reduce clouds size and update font

* fix: hero section responsiveness issue

* fix: formatting errors

* add currency animation
#1115)

* fix: don't allow claiming on xChain if route is not found

* fix(claim): use correct decimals for min receive amount
* feat: handle redirect uri when on unsupported browsers

* fix: confirm bank claim ui rows for iban guest claim
@notion-workspace
Copy link

Prod Release 105

@vercel
Copy link

vercel bot commented Aug 21, 2025

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

Project Deployment Preview Comments Updated (UTC)
peanut-ui (peanut-wallet-staging) Ready Ready Preview Comment Aug 21, 2025 4:53pm
peanut-wallet Ready Ready Preview Comment Aug 21, 2025 4:53pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 21, 2025

Walkthrough

Adds an exchange-rate API and comparison script, overhauls claim-to-bank flow via a new ClaimBankFlow context and views, replaces axelarChainName with networkName across chain data, revamps landing pages (DropLink, RegulatedRails, Footer, NoFees with live rates, CurrencySelect), introduces new hooks/utilities, updates icons/assets, and adjusts related components/tests.

Changes

Cohort / File(s) Summary
Exchange rate tooling
src/app/api/exchange-rate/route.ts, scripts/compare-rates.mjs, src/hooks/useExchangeRate.ts
New API route serving cached FX (Bridge/Frankfurter with 0.995 adj). CLI script to compare rates across sources. Hook to fetch and compute two-way conversions with debounced inputs.
Claim bank flow migration
src/context/ClaimBankFlowContext.tsx, src/components/Claim/Link/Initial.view.tsx, src/components/Claim/Link/views/BankFlowManager.view.tsx, src/components/Common/ActionList.tsx, src/components/Common/CountryList*.tsx, src/components/Common/SavedAccountsView.tsx, src/components/Claim/Link/Onchain/{Confirm.view.tsx,Success.view.tsx}, src/components/Claim/Link/views/Confirm.bank-claim.view.tsx, src/hooks/useDetermineBankClaimType.ts, src/hooks/useSavedAccounts.tsx, src/components/GuestActions/MethodList.tsx (deleted), src/components/Claim/Link/views/ClaimCountryList.view.tsx (deleted)
Replaces guest flow with ClaimBankFlow state machine and components (ActionList, CountryList Router/Skeleton, SavedAccountsView). Integrates KYC paths, external/peanut wallet branching, and new confirm/success behaviors. Removes obsolete guest views.
Chain name refactor (networkName)
src/app/actions/squid.ts, src/context/tokenSelector.context.tsx, src/context/WithdrawFlowContext.tsx, src/components/Global/TokenSelector/**/*, src/components/AddWithdraw/views/NetworkSelection.view.tsx, src/components/Withdraw/views/{Initial.withdraw.view.tsx,Confirm.withdraw.view.tsx}, src/components/Claim/Claim.utils.ts, src/hooks/useTokenChainIcons.ts, src/lib/validation/token.test.ts, src/interfaces/interfaces.ts
Adds/uses networkName on chain objects; replaces axelarChainName references; updates types, displays, and tests; removes IChain interface.
Add/Withdraw flow updates
src/components/AddWithdraw/{AddWithdrawRouterView.tsx,AddWithdrawCountriesList.tsx,DynamicBankAccountForm.tsx}, src/components/AddMoney/components/DepositMethodList.tsx, src/utils/withdraw.utils.ts, src/utils/bridge.utils.ts
Refactors router view to CountryList/SavedAccounts, removes sessionStorage caching, enhances bank form (country-aware validation: IBAN/US/CLABE), adds withdraw utilities, adjusts US/USA handling and list UI.
Landing page revamp
src/app/page.tsx, src/app/exchange/page.tsx, src/components/LandingPage/{dropLink.tsx,RegulatedRails.tsx,Footer.tsx,noFees.tsx,hero.tsx,imageAssets.tsx,index.ts,CurrencySelect.tsx,securityBuiltIn.tsx,yourMoney.tsx}, src/assets/icons/index.ts, src/assets/illustrations/index.ts, src/styles/globals.css, src/constants/countryCurrencyMapping.ts
Introduces DropLink, RegulatedRails, Footer, interactive NoFees with live FX, CurrencySelect, hero API change (no heading), asset updates, new icons/illustrations, font class, and currency mappings.
Metadata/favicon updates
src/app/(mobile-ui)/*/page.tsx, src/app/[...recipient]/page.tsx
Updates Metadata icons from /logo-favicon.png to /favicon.ico.
Debounce/utilities/hooks
src/components/Global/ValidatedInput/index.tsx, src/hooks/useUserSearch.ts, src/utils/general.utils.ts, src/hooks/useGeoLocaion.ts, src/components/Global/UnsupportedBrowserModal/index.tsx, src/hooks/useKycFlow.ts
Replaces manual debouncing with useDebounce, adds internal navigation check, geolocation hook, Suspense-wrapped UnsupportedBrowserModal using search params, and clearer KYC iframe-close branching.
DirectSendQR parsing
src/components/Global/DirectSendQR/{utils.ts,__tests__/parseEip681.test.ts}
Amounts now parsed to human-readable units using formatUnits and token decimals; tests updated accordingly.
Profile navigation adjustments
src/components/Profile/{index.tsx,components/PublicProfile.tsx}
Back navigation checks internal referrer; falls back to /home; expands menu items.
Misc components/assets
src/components/0_Bruddle/Button.tsx, src/components/Global/Icons/{Icon.tsx,chevron-down.tsx}, src/components/Global/PeanutActionDetailsCard/index.tsx
Marks Button as client component; adds chevron-down icon; extends currency symbol display for ADD_MONEY_BANK_ACCOUNT.
Context provider wiring
src/context/contextProvider.tsx, src/app/(mobile-ui)/home/page.tsx
Switches to ClaimBankFlowProvider and corresponding hook usage.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

enhancement

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch peanut-wallet-dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai bot added the enhancement New feature or request label Aug 21, 2025
@jjramirezn jjramirezn closed this Aug 21, 2025
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: 28

Caution

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

⚠️ Outside diff range comments (18)
src/components/Global/UnsupportedBrowserModal/index.tsx (1)

114-131: Modal becomes non-closable because visible is hard-coded to true.

Currently, ActionModal always receives visible={true}. Even if handleModalClose runs, the modal stays open when the parent hasn’t set visible to false, making the close button ineffective and trapping users in the modal. Pass the computed visibility instead.

Apply this diff:

   return (
     <ActionModal
-            visible={true}
+            visible={showInAppBrowserModalViaDetection || visible}
             onClose={handleModalClose}
             title="Open this link in your browser"
src/utils/bridge.utils.ts (2)

14-33: Case sensitivity and 3-letter aliases can misroute to EUR/SEPA — normalize countryId

As-is, inputs like 'usa' or 'mex' fall through to EUR/SEPA. Given upstream sources may provide mixed-case or 3-letter codes (see repository learnings), normalize once and map common 3→2 aliases.

Apply:

-export const getCurrencyConfig = (countryId: string, operationType: BridgeOperationType): CurrencyConfig => {
-    if (countryId === 'US' || countryId === 'USA') {
+// Normalize to uppercase and convert common 3-letter aliases to 2-letter
+const normalizeCountryId = (id: string): string => {
+    const v = (id || '').toUpperCase().trim()
+    const threeToTwo: Record<string, string> = { USA: 'US', MEX: 'MX' }
+    return threeToTwo[v] || v
+}
+
+export const getCurrencyConfig = (countryId: string, operationType: BridgeOperationType): CurrencyConfig => {
+    const id = normalizeCountryId(countryId)
+    if (id === 'US') {
         return {
             currency: 'usd',
             paymentRail: operationType === 'onramp' ? 'ach_push' : 'ach',
         }
     }
 
-    if (countryId === 'MX') {
+    if (id === 'MX') {
         return {
             currency: 'mxn',
             paymentRail: 'spei', // SPEI works for both onramp and offramp in Mexico
         }
     }
 
     // All other countries use EUR/SEPA
     return {
         currency: 'eur',
         paymentRail: 'sepa', // SEPA works for both onramp and offramp in Europe
     }
 }

Follow-up: apply the same normalization for getMinimumAmount below. See next comment.


65-72: Use the same normalization in getMinimumAmount to avoid MX lower-case edge cases

Lower-case 'mx' would incorrectly return 1 today.

Apply:

-export const getMinimumAmount = (countryId: string): number => {
-    if (countryId === 'MX') {
+export const getMinimumAmount = (countryId: string): number => {
+    const id = normalizeCountryId(countryId)
+    if (id === 'MX') {
         return 50
     }
 
     // Default minimum for all other countries (including US and EU)
     return 1
 }
src/components/Global/PeanutActionDetailsCard/index.tsx (1)

196-214: Fix amount suffix for ADD_MONEY_BANK_ACCOUNT to avoid mixed fiat+token display

When transactionType is ADD_MONEY_BANK_ACCOUNT, you now prefix with a fiat symbol, but the suffix logic still appends the token symbol (since only ADD_MONEY is excluded). This can produce outputs like “$ 100 USDC”.

Exclude ADD_MONEY_BANK_ACCOUNT from the suffix:

-                        {tokenSymbol.toLowerCase() !== PEANUT_WALLET_TOKEN_SYMBOL.toLowerCase() &&
-                            transactionType !== 'ADD_MONEY' &&
+                        {tokenSymbol.toLowerCase() !== PEANUT_WALLET_TOKEN_SYMBOL.toLowerCase() &&
+                            transactionType !== 'ADD_MONEY' &&
+                            transactionType !== 'ADD_MONEY_BANK_ACCOUNT' &&
                             !(transactionType === 'CLAIM_LINK_BANK_ACCOUNT' && viewType === 'SUCCESS') &&
                             ` ${
                                 tokenSymbol.toLowerCase() === 'pnt'
                                     ? Number(amount) === 1
                                         ? 'Beer'
                                         : 'Beers'
                                     : tokenSymbol
                             }`}
src/components/AddMoney/components/DepositMethodList.tsx (1)

77-84: Add flagcdn.com to Next.js image remotePatterns configuration

It looks like your next.config.js defines an images.remotePatterns array (around line 28) but doesn’t include an entry for flagcdn.com. Without this, <Image> requests to flagcdn.com will error at runtime. Please update your config as follows:

// next.config.js
 module.exports = {
   images: {
     remotePatterns: [
       // …existing patterns…
+      {
+        protocol: 'https',
+        hostname: 'flagcdn.com',
+        port: '',
+        pathname: '/w160/**',
+      },
     ],
   },
   // …other config…
 };

Alternatively, if you prefer using the older domains approach, you can add:

 images: {
-  domains: [/* … */],
+  domains: [
+    /* …existing domains… */,
+    'flagcdn.com',
+  ],
 },

Either approach will ensure Next.js allows loading the flag images from flagcdn.com.

src/components/0_Bruddle/Button.tsx (2)

98-101: Forwarded ref is broken for function refs. Use useImperativeHandle.

Casting ref to RefObject drops support for callback refs and can silently fail. Bind an internal ref, then expose it via useImperativeHandle.

- import React, { forwardRef, useEffect, useRef, useState, useCallback } from 'react'
+ import React, { forwardRef, useEffect, useRef, useState, useCallback, useImperativeHandle } from 'react'
@@
-        const localRef = useRef<HTMLButtonElement>(null)
-        const buttonRef = (ref as React.RefObject<HTMLButtonElement>) || localRef
+        const innerRef = useRef<HTMLButtonElement>(null)
+        useImperativeHandle(ref, () => innerRef.current as HTMLButtonElement | null, [])
@@
-            if (!buttonRef.current) return
-            buttonRef.current.setAttribute('translate', 'no')
-            buttonRef.current.classList.add('notranslate')
+            if (!innerRef.current) return
+            innerRef.current.setAttribute('translate', 'no')
+            innerRef.current.classList.add('notranslate')
@@
-                ref={buttonRef}
+                ref={innerRef}

Also applies to: 108-111, 239-239, 2-2


209-216: Tailwind JIT cannot see dynamic class “active:translate-y-[${shadowSize}px]”.

Dynamic arbitrary values won’t be generated by Tailwind. Use a static mapping based on the finite shadowSize union.

-        const buttonClasses = twMerge(
-            `btn w-full flex items-center gap-2 transition-all duration-100 active:translate-x-[3px] active:translate-y-[${shadowSize}px] active:shadow-none notranslate`,
+        const activeYTranslate =
+            shadowSize === '4'
+                ? 'active:translate-y-[4px]'
+                : shadowSize === '6'
+                  ? 'active:translate-y-[6px]'
+                  : shadowSize === '8'
+                    ? 'active:translate-y-[8px]'
+                    : undefined
+
+        const buttonClasses = twMerge(
+            `btn w-full flex items-center gap-2 transition-all duration-100 active:translate-x-[3px] active:shadow-none notranslate`,
             buttonVariants[variant],
             variant === 'transparent' && props.disabled && 'disabled:bg-transparent disabled:border-transparent',
             size && buttonSizes[size],
             shape === 'square' && 'btn-square',
             shadowSize && buttonShadows[shadowType || 'primary'][shadowSize],
+            activeYTranslate,
 
             className
         )
src/app/actions/squid.ts (1)

8-14: Align chainId types in squid filters

The supportedPeanutChains array is built from IPeanutChainDetails, whose chainId property is declared as a string (src/interfaces/interfaces.ts). However, in the Peanut SDK’s API you pass chainId as a number (e.g. peanut.createLink({ chainId: 5, … })), so interfaces.ISquidChain.chainId is a number (github.com). Comparing a string to a number with === will always fail, causing valid chains or tokens to be filtered out.

Locations to update:

  • src/app/actions/squid.ts (lines 8–14)

Suggested diff:

 const supportedByPeanut = (chain: interfaces.ISquidChain): boolean =>
     'evm' === chain.chainType &&
-    supportedPeanutChains.some((supportedChain) => supportedChain.chainId === chain.chainId)
+    supportedPeanutChains.some((supportedChain) => supportedChain.chainId === chain.chainId.toString())

 const tokensSupportedByPeanut = (token: interfaces.ISquidToken): boolean =>
-    supportedPeanutChains.some((supportedChain) => supportedChain.chainId === token.chainId)
+    supportedPeanutChains.some((supportedChain) => supportedChain.chainId === token.chainId.toString())
src/components/Claim/Link/views/Confirm.bank-claim.view.tsx (1)

67-69: formatUnits default can throw; use bigint and a safe decimals fallback.

formatUnits expects a bigint and a number. The current fallback 0 is a number and decimals may be undefined; either can cause runtime errors.

Apply:

-                    amount={formatUnits(claimLinkData?.amount ?? 0, claimLinkData?.tokenDecimals) || '0.00'}
+                    amount={
+                        formatUnits(
+                            (claimLinkData?.amount as bigint | undefined) ?? 0n,
+                            (claimLinkData?.tokenDecimals as number | undefined) ?? 18
+                        ) || '0.00'
+                    }
src/lib/validation/token.test.ts (1)

9-15: Remove all remaining axelarChainName references to enforce networkName-only contract

Tests, types, and utility code still include axelarChainName. Please drop the field so that any accidental usage will immediately surface as a compilation or test error.

• src/lib/validation/token.test.ts (lines 9–15, 39–45, 67–73)
Apply the original diff to remove axelarChainName in each mock entry:

   '1': {
     chainId: '1',
-    axelarChainName: 'Ethereum',
     networkName: 'Ethereum',
     …
   },
   '10': {
     chainId: '10',
-    axelarChainName: 'optimism',
     networkName: 'optimism',
     …
   },
   '8453': {
     chainId: '8453',
-    axelarChainName: 'base',
     networkName: 'base',
     …
   },

• src/utils/tests/url-parser.test.ts (line 62)

-            axelarChainName: 'base',

• src/components/utils/utils.ts (around line 10)
Remove the axelarChainName: string property from the ChainData (or equivalent) type/interface so it no longer appears in parsed or serialized chain objects.

• src/components/Claim/Claim.interfaces.ts (line 5)

-    axelarChainName?: string;

After these removals, rerun your grep command to confirm zero matches for axelarChainName. This ensures no hidden usages remain and enforces the new networkName-only contract.

src/app/(mobile-ui)/home/page.tsx (2)

137-159: Guard against null user and consolidate duplicate “balance warning” effects

This effect dereferences user! and has a twin below (Lines 160–182). The non-null assertion can crash on initial renders; duplication risks double state updates.

Apply this diff to guard user and keep a single effect:

-    // effect for showing balance warning modal
-    useEffect(() => {
-        if (isFetchingBalance || balance === undefined) return
-
-        if (typeof window !== 'undefined') {
-            const hasSeenBalanceWarning = getFromLocalStorage(`${user!.user.userId}-hasSeenBalanceWarning`)
+    // effect for showing balance warning modal
+    useEffect(() => {
+        if (isFetchingBalance || balance === undefined || !user) return
+
+        if (typeof window !== 'undefined') {
+            const hasSeenBalanceWarning = getFromLocalStorage(`${user.user.userId}-hasSeenBalanceWarning`)
             const balanceInUsd = Number(formatUnits(balance, PEANUT_WALLET_TOKEN_DECIMALS))
 
             // show if:
             // 1. balance is above the threshold
             // 2. user hasn't seen this warning in the current session
             // 3. no other modals are currently active
             if (
                 balanceInUsd > BALANCE_WARNING_THRESHOLD &&
                 !hasSeenBalanceWarning &&
                 !showIOSPWAInstallModal &&
-                !showAddMoneyPromptModal &&
-                !isPostSignupActionModalVisible
+                !showAddMoneyPromptModal &&
+                !isPostSignupActionModalVisible
             ) {
                 setShowBalanceWarningModal(true)
             }
         }
-    }, [balance, isFetchingBalance, showIOSPWAInstallModal, showAddMoneyPromptModal])
+    }, [user, balance, isFetchingBalance, showIOSPWAInstallModal, showAddMoneyPromptModal, isPostSignupActionModalVisible])

160-182: Remove duplicate “balance warning” effect

This second copy overlaps the first but with slightly different conditions; keep one source of truth to avoid surprises.

Apply this diff:

-    // effect for showing balance warning modal
-    useEffect(() => {
-        if (isFetchingBalance || balance === undefined) return
-
-        if (typeof window !== 'undefined') {
-            const hasSeenBalanceWarning = getFromLocalStorage(`${user!.user.userId}-hasSeenBalanceWarning`)
-            const balanceInUsd = Number(formatUnits(balance, PEANUT_WALLET_TOKEN_DECIMALS))
-
-            // show if:
-            // 1. balance is above the threshold
-            // 2. user hasn't seen this warning in the current session
-            // 3. no other modals are currently active
-            if (
-                balanceInUsd > BALANCE_WARNING_THRESHOLD &&
-                !hasSeenBalanceWarning &&
-                !showIOSPWAInstallModal &&
-                !showAddMoneyPromptModal
-            ) {
-                setShowBalanceWarningModal(true)
-            }
-        }
-    }, [balance, isFetchingBalance, showIOSPWAInstallModal, showAddMoneyPromptModal])
src/hooks/useKycFlow.ts (1)

104-110: Fix malformed fields[iqt_token] parameter in convertPersonaUrl

The convertPersonaUrl helper builds the fields[iqt_token] query parameter incorrectly—it's missing the closing bracket before the = and thus emits fields[iqt_token=… instead of fields[iqt_token]=…. This will break the Persona widget URL.

• File: src/utils/bridge-accounts.utils.ts
• Return statement (around line 31)

Suggested patch:

-export const convertPersonaUrl = (url: string) => {
-  // … previous code …
-  return `https://bridge.withpersona.com/widget?environment=production&inquiry-template-id=${templateId}&fields[iqt_token=${iqtToken}&iframe-origin=${origin}&redirect-uri=${origin}&fields[developer_id]=${developerId}&reference-id=${referenceId}`
-}
+export const convertPersonaUrl = (url: string) => {
+  // … previous code …
+  return `https://bridge.withpersona.com/widget?environment=production&inquiry-template-id=${templateId}` +
+         `&fields[iqt_token]=${iqtToken}` +
+         `&iframe-origin=${origin}` +
+         `&redirect-uri=${origin}` +
+         `&fields[developer_id]=${developerId}` +
+         `&reference-id=${referenceId}`
+}

This correction closes the bracket for fields[iqt_token] and aligns it with the other query parameters.

src/components/Claim/Link/Onchain/Confirm.view.tsx (1)

87-93: Use route-derived chain/token to avoid parameter drift

To ensure that the on-chain claim call always matches the computed route, pull the destination chain and token directly from selectedRoute.rawResponse.route instead of the context’s selectedChainID/selectedTokenAddress, which may diverge if the user changes their selection after routing.

• File: src/components/Claim/Link/Onchain/Confirm.view.tsx
Lines: 87–93

Suggested change:

-               claimTxHash = await claimLinkXchain({
-                   address: recipient ? recipient.address : (address ?? ''),
-                   link: claimLinkData.link,
-                   destinationChainId: selectedChainID,
-                   destinationToken: selectedTokenAddress,
-               })
+               claimTxHash = await claimLinkXchain({
+                   address: recipient ? recipient.address : (address ?? ''),
+                   link: claimLinkData.link,
+                   destinationChainId: selectedRoute.rawResponse.route.params.toChain,
+                   destinationToken: selectedRoute.rawResponse.route.estimate.toToken.address,
+               })
src/components/AddWithdraw/DynamicBankAccountForm.tsx (2)

95-103: Normalize country detection (USA/MX) using alpha-2 codes; current checks are inconsistent.

You compare 'USA' (alpha-3) for US but 'MX' (alpha-2) for Mexico. This can misclassify depending on what country contains.

-const isUs = country.toUpperCase() === 'USA'
-const isMx = country.toUpperCase() === 'MX'
-const isIban = isUs || isMx ? false : isIBANCountry(country)
+// Normalize to alpha-3 and alpha-2
+const alpha3 = country.length === 2
+    ? (Object.keys(countryCodeMap).find((k) => countryCodeMap[k] === country.toUpperCase()) ?? country.toUpperCase())
+    : country.toUpperCase()
+const alpha2 = countryCodeMap[alpha3] ?? (country.length === 2 ? country.toUpperCase() : '')
+const isUs = alpha2 === 'US'
+const isMx = alpha2 === 'MX'
+const isIban = !(isUs || isMx) && isIBANCountry(alpha3)
-const isMx = country.toUpperCase() === 'MX'
-const isUs = country.toUpperCase() === 'USA'
-const isIban = isUs || isMx ? false : isIBANCountry(country)
+const isMx = alpha2 === 'MX'
+const isUs = alpha2 === 'US'
+const isIban = !(isUs || isMx) && isIBANCountry(alpha3)

Also applies to: 172-175


127-145: Use explicit ISO-3 and ISO-2 country codes in the payload

Before you build the payload, derive and reuse two standardized codes:

// inside onSubmit, after determining country param
const iso3 = country.toUpperCase();                                      // e.g. "USA", "GBR", "CZE"
const iso2 = countryCodeMap[iso3] ?? iso3;                                // maps to "US", "GB", "CZ"

Then update the payload to use those:

 const payload: Partial<AddBankAccountPayload> = {
     accountType,
     accountNumber: accountNumber.replace(/\s/g, ''),
-    countryCode: isUs ? 'USA' : country.toUpperCase(),
+    // API expects the 3-letter ISO code
+    countryCode: iso3,

     countryName: selectedCountry,
     accountOwnerType: BridgeAccountOwnerType.INDIVIDUAL,
     accountOwnerName: {
         firstName: firstName.trim(),
         lastName: lastName.trim(),
     },
     address: {
         street: data.street ?? '',
         city: data.city ?? '',
         state: data.state ?? '',
         postalCode: data.postalCode ?? '',
-        country: isUs ? 'USA' : country.toUpperCase(),
+        // address.country should be the canonical 2-letter ISO code
+        country: iso2,
     },
     ...(bic && { bic }),
 }

This change ensures

  • payload.countryCode is always a valid ISO 3166-1 alpha-3 code
  • payload.address.country is the corresponding alpha-2 code for downstream address handling and flag displays
src/context/ClaimBankFlowContext.tsx (2)

17-45: Make setter types React.Dispatch and remove optional+null duplication to restore type-safety

Right now the context interface uses custom setter signatures and mixes ? with | null. This forces an unsafe as ClaimBankFlowContextType cast later and weakens type guarantees for consumers.

  • Use React.Dispatch<React.SetStateAction<...>> for setters so they match React’s actual types.
  • Make properties always-present and encode absence via null or undefined (not both plus ?).
  • Keep senderKycStatus as KYCStatus | undefined (present but possibly undefined) for consistency.

Apply:

 interface ClaimBankFlowContextType {
-    claimToExternalWallet: boolean
-    setClaimToExternalWallet: (claimToExternalWallet: boolean) => void
+    claimToExternalWallet: boolean
+    setClaimToExternalWallet: React.Dispatch<React.SetStateAction<boolean>>
     flowStep: ClaimBankFlowStep | null
-    setFlowStep: (step: ClaimBankFlowStep | null) => void
+    setFlowStep: React.Dispatch<React.SetStateAction<ClaimBankFlowStep | null>>
     selectedCountry: CountryData | null
-    setSelectedCountry: (country: CountryData | null) => void
+    setSelectedCountry: React.Dispatch<React.SetStateAction<CountryData | null>>
     resetFlow: () => void
-    offrampDetails?: TCreateOfframpResponse | null
-    setOfframpDetails: (details: TCreateOfframpResponse | null) => void
-    claimError?: string | null
-    setClaimError: (error: string | null) => void
-    claimType?: 'claim-bank' | 'claim' | 'claimxchain' | null
-    setClaimType: (type: 'claim-bank' | 'claim' | 'claimxchain' | null) => void
+    offrampDetails: TCreateOfframpResponse | null
+    setOfframpDetails: React.Dispatch<React.SetStateAction<TCreateOfframpResponse | null>>
+    claimError: string | null
+    setClaimError: React.Dispatch<React.SetStateAction<string | null>>
+    claimType: 'claim-bank' | 'claim' | 'claimxchain' | null
+    setClaimType: React.Dispatch<React.SetStateAction<'claim-bank' | 'claim' | 'claimxchain' | null>>
     senderDetails: User | null
-    setSenderDetails: (details: User | null) => void
+    setSenderDetails: React.Dispatch<React.SetStateAction<User | null>>
     showVerificationModal: boolean
-    setShowVerificationModal: (show: boolean) => void
+    setShowVerificationModal: React.Dispatch<React.SetStateAction<boolean>>
     bankDetails: IBankAccountDetails | null
-    setBankDetails: (details: IBankAccountDetails | null) => void
+    setBankDetails: React.Dispatch<React.SetStateAction<IBankAccountDetails | null>>
-    savedAccounts: Account[]
-    setSavedAccounts: (accounts: Account[]) => void
+    savedAccounts: Account[]
+    setSavedAccounts: React.Dispatch<React.SetStateAction<Account[]>>
     selectedBankAccount: Account | null
-    setSelectedBankAccount: (account: Account | null) => void
-    senderKycStatus?: KYCStatus
-    setSenderKycStatus: (status?: KYCStatus) => void
+    setSelectedBankAccount: React.Dispatch<React.SetStateAction<Account | null>>
+    senderKycStatus: KYCStatus | undefined
+    setSenderKycStatus: React.Dispatch<React.SetStateAction<KYCStatus | undefined>>
     justCompletedKyc: boolean
-    setJustCompletedKyc: (status: boolean) => void
+    setJustCompletedKyc: React.Dispatch<React.SetStateAction<boolean>>
 }

80-109: Eliminate unsafe as cast by typing useMemo result directly

Once the interface is corrected, you can avoid the forced cast on the Provider. Type useMemo with the context type and pass value directly.

-    const value = useMemo(
+    const value = useMemo<ClaimBankFlowContextType>(
         () => ({
             claimToExternalWallet,
             setClaimToExternalWallet,
             flowStep,
             setFlowStep,
             selectedCountry,
             setSelectedCountry,
             resetFlow,
             offrampDetails,
             setOfframpDetails,
             claimError,
             setClaimError,
             claimType,
             setClaimType,
             senderDetails,
             setSenderDetails,
             showVerificationModal,
             setShowVerificationModal,
             bankDetails,
             setBankDetails,
             savedAccounts,
             setSavedAccounts,
             selectedBankAccount,
             setSelectedBankAccount,
             senderKycStatus,
             setSenderKycStatus,
             justCompletedKyc,
             setJustCompletedKyc,
         }),
         [
             claimToExternalWallet,
             flowStep,
             selectedCountry,
             resetFlow,
             offrampDetails,
             claimError,
             claimType,
             senderDetails,
             showVerificationModal,
             bankDetails,
             savedAccounts,
             selectedBankAccount,
             senderKycStatus,
             justCompletedKyc,
         ]
     )
 
-    return (
-        <ClaimBankFlowContext.Provider value={value as ClaimBankFlowContextType}>
-            {children}
-        </ClaimBankFlowContext.Provider>
-    )
+    return (
+        <ClaimBankFlowContext.Provider value={value}>
+            {children}
+        </ClaimBankFlowContext.Provider>
+    )

Also applies to: 128-132

Comment on lines +37 to 48
async (): Promise<
Record<string, interfaces.ISquidChain & { networkName: string; tokens: interfaces.ISquidToken[] }>
> => {
const [chains, tokens] = await Promise.all([getSquidChainsCache(), getSquidTokensCache()])

const chainsById = chains.reduce<Record<string, interfaces.ISquidChain & { tokens: interfaces.ISquidToken[] }>>(
(acc, chain) => {
acc[chain.chainId] = { ...chain, tokens: [] }
return acc
},
{}
)
const chainsById = chains.reduce<
Record<string, interfaces.ISquidChain & { networkName: string; tokens: interfaces.ISquidToken[] }>
>((acc, chain) => {
acc[chain.chainId] = { ...(chain as interfaces.ISquidChain & { networkName: string }), tokens: [] }
return acc
}, {})

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Unsound cast for networkName risks undefined at runtime — derive and set explicitly

{ ...(chain as ISquidChain & { networkName: string }), tokens: [] } trusts that SDK chains already include networkName. If the SDK doesn’t yet ship this field, downstream UI reading chain.networkName will render undefined.

Set networkName explicitly with safe fallbacks:

 export const getSquidChainsAndTokens = unstable_cache(
   async (): Promise<
     Record<string, interfaces.ISquidChain & { networkName: string; tokens: interfaces.ISquidToken[] }>
   > => {
     const [chains, tokens] = await Promise.all([getSquidChainsCache(), getSquidTokensCache()])

-    const chainsById = chains.reduce<
-      Record<string, interfaces.ISquidChain & { networkName: string; tokens: interfaces.ISquidToken[] }>
-    >((acc, chain) => {
-      acc[chain.chainId] = { ...(chain as interfaces.ISquidChain & { networkName: string }), tokens: [] }
-      return acc
-    }, {})
+    const chainsById = chains.reduce<
+      Record<string, interfaces.ISquidChain & { networkName: string; tokens: interfaces.ISquidToken[] }>
+    >((acc, chain) => {
+      const anyChain = chain as any
+      const networkName: string =
+        anyChain.networkName ??
+        anyChain.axelarChainName ?? // legacy field seen in prior codebase
+        anyChain.name ??
+        String(chain.chainId)
+      acc[chain.chainId] = { ...(chain as interfaces.ISquidChain), networkName, tokens: [] }
+      return acc
+    }, {})
📝 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
async (): Promise<
Record<string, interfaces.ISquidChain & { networkName: string; tokens: interfaces.ISquidToken[] }>
> => {
const [chains, tokens] = await Promise.all([getSquidChainsCache(), getSquidTokensCache()])
const chainsById = chains.reduce<Record<string, interfaces.ISquidChain & { tokens: interfaces.ISquidToken[] }>>(
(acc, chain) => {
acc[chain.chainId] = { ...chain, tokens: [] }
return acc
},
{}
)
const chainsById = chains.reduce<
Record<string, interfaces.ISquidChain & { networkName: string; tokens: interfaces.ISquidToken[] }>
>((acc, chain) => {
acc[chain.chainId] = { ...(chain as interfaces.ISquidChain & { networkName: string }), tokens: [] }
return acc
}, {})
const chainsById = chains.reduce<
Record<string, interfaces.ISquidChain & { networkName: string; tokens: interfaces.ISquidToken[] }>
>((acc, chain) => {
const anyChain = chain as any
const networkName: string =
anyChain.networkName ??
anyChain.axelarChainName ?? // legacy field seen in prior codebase
anyChain.name ??
String(chain.chainId)
acc[chain.chainId] = { ...(chain as interfaces.ISquidChain), networkName, tokens: [] }
return acc
}, {})
🤖 Prompt for AI Agents
In src/app/actions/squid.ts around lines 37 to 48, the code unsafely casts chain
to include networkName which may be missing at runtime; instead explicitly set
networkName with a safe fallback when building chainsById — e.g. avoid the cast
and construct the object by spreading the known chain properties and adding
networkName: chain.networkName ?? chain.name ?? String(chain.chainId) (or a
lookup map if you have canonical names), and keep tokens: [] so downstream
consumers never see undefined.

Comment on lines +3 to +5
interface ExchangeRateResponse {
rate: number
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Confirm response shape across the app; prior code used midmarket/buy/sell fields.

This route returns { rate: number }, which differs from the older ExchangeRateResponse shape found in src/app/actions/exchange-rate.ts. Ensure all consumers of /api/exchange-rate expect the new shape.

If any consumer expects mid/buy/sell, either adapt them or extend this route to include those fields.

Also applies to: 120-128, 151-159


🏁 Script executed:

#!/bin/bash
# Where do we call /api/exchange-rate?
rg -nP -C2 '/api/exchange-rate'
# Who imports/defines ExchangeRateResponse?
rg -nP -C2 '\binterface\s+ExchangeRateResponse\b|type\s+ExchangeRateResponse\b'
# Look for 'midmarket_rate' | 'buy_rate' | 'sell_rate' assumptions
rg -nP -C2 'midmarket_rate|buy_rate|sell_rate'

Length of output: 4967


Mismatch in ExchangeRateResponse shape – update API or consumers

The /api/exchange-rate route now returns only:

interface ExchangeRateResponse {
  rate: number
}

whereas the rest of the codebase (and scripts) still expect the legacy fields: midmarket_rate, buy_rate, and sell_rate. You must either extend the route’s response to include those properties or update all consumers to use the new rate field.

Consumers to address:

  • src/components/ExchangeRate/index.tsx (line 25):
    setExchangeRate(data.sell_rate)
  • src/hooks/useExchangeRate.ts:
    returns res.json() but components expect { buy_rate, sell_rate, ... }
  • scripts/compare-rates.mjs (around lines 76–78):
    const { midmarket_rate, buy_rate, sell_rate } = data || {}
  • src/app/actions/exchange-rate.ts:
    defines ExchangeRateResponse with midmarket_rate, buy_rate, sell_rate, updated_at, but this no longer matches the API route

Next steps:

  • Option A: Modify src/app/api/exchange-rate/route.ts to return
    {
      rate: number;
      midmarket_rate: string;
      buy_rate: string;
      sell_rate: string;
      // (plus any other legacy fields)
    }
  • Option B: Refactor all consumers to read from rate and remove legacy fields
🤖 Prompt for AI Agents
In src/app/api/exchange-rate/route.ts around lines 3 to 5, the route's response
type was reduced to { rate: number } but other modules still expect legacy
fields (midmarket_rate, buy_rate, sell_rate, updated_at); either restore the
legacy shape in the route or update all consumers to use rate. Preferred quick
fix: extend the route response to include midmarket_rate, buy_rate, sell_rate
(and updated_at if used) populated from rate (or computed/formatted as strings
if existing consumers expect strings) so the returned object contains both rate
and the legacy properties; alternatively, if you choose refactor, change these
consumers: src/components/ExchangeRate/index.tsx (line ~25) to use data.rate,
update src/hooks/useExchangeRate.ts to return { rate } shape and update typing,
change scripts/compare-rates.mjs to read rate instead of
midmarket_rate/buy_rate/sell_rate, and update src/app/actions/exchange-rate.ts
to match the new type; ensure types/returns and tests are updated accordingly.

Comment on lines +112 to +118
let rate = parseFloat(bridgeData[rateType])

// If we fetched the reverse pair (e.g., fetched USD→EUR for EUR→USD request),
// we need to invert the rate
if (shouldInvert) {
rate = 1 / rate
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard against invalid or zero Bridge rates before inversion.

If Bridge returns 0/NaN/negative, 1 / rate will crash or produce nonsense. Fail fast and fall back.

-        let rate = parseFloat(bridgeData[rateType])
+        let rate = parseFloat(bridgeData[rateType])
+        if (!Number.isFinite(rate) || rate <= 0) {
+            console.error(`Bridge returned invalid ${rateType}: ${bridgeData[rateType]}`)
+            return null
+        }
@@
-        if (shouldInvert) {
-            rate = 1 / rate
-        }
+        if (shouldInvert) {
+            rate = 1 / rate
+        }
📝 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 rate = parseFloat(bridgeData[rateType])
// If we fetched the reverse pair (e.g., fetched USD→EUR for EUR→USD request),
// we need to invert the rate
if (shouldInvert) {
rate = 1 / rate
}
let rate = parseFloat(bridgeData[rateType])
if (!Number.isFinite(rate) || rate <= 0) {
console.error(`Bridge returned invalid ${rateType}: ${bridgeData[rateType]}`)
return null
}
// If we fetched the reverse pair (e.g., fetched USD→EUR for EUR→USD request),
// we need to invert the rate
if (shouldInvert) {
rate = 1 / rate
}
🤖 Prompt for AI Agents
In src/app/api/exchange-rate/route.ts around lines 112 to 118, the code inverts
the Bridge rate without validating it first; before doing rate = 1 / rate, check
that parsed rate is a finite, positive number (e.g., Number.isFinite(rate) &&
rate > 0). If the check fails, handle it by logging the invalid value and
failing fast to trigger the fallback path (return an error or throw a specific
exception that the caller uses to try the next provider) instead of performing
the inversion; ensure this validation happens both before and after any
parseFloat and only invert when the check passes.

Comment on lines +149 to +153
const data = await response.json()

const exchangeRate: ExchangeRateResponse = {
rate: data.rates[to] * 0.995, // Subtract 50bps
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Validate Frankfurter payload shape and avoid NaN when rates[to] is undefined.

Add a guard and clear error message. Also consider making the bps adjustment configurable.

-    const data = await response.json()
-
-    const exchangeRate: ExchangeRateResponse = {
-        rate: data.rates[to] * 0.995, // Subtract 50bps
-    }
+    const data = await response.json()
+    const baseRate = data?.rates?.[to]
+    if (!Number.isFinite(baseRate)) {
+        console.error(`Frankfurter: missing/invalid rate for ${to}`, data)
+        return NextResponse.json({ error: `Rate not available for ${to}` }, { status: 502 })
+    }
+    const exchangeRate: ExchangeRateResponse = {
+        rate: Number(baseRate) * 0.995, // Subtract 50bps
+    }
📝 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
const data = await response.json()
const exchangeRate: ExchangeRateResponse = {
rate: data.rates[to] * 0.995, // Subtract 50bps
}
const data = await response.json()
const baseRate = data?.rates?.[to]
if (!Number.isFinite(baseRate)) {
console.error(`Frankfurter: missing/invalid rate for ${to}`, data)
return NextResponse.json({ error: `Rate not available for ${to}` }, { status: 502 })
}
const exchangeRate: ExchangeRateResponse = {
rate: Number(baseRate) * 0.995, // Subtract 50bps
}
🤖 Prompt for AI Agents
In src/app/api/exchange-rate/route.ts around lines 149 to 153, the code assumes
response.json() contains data.rates[to] and multiplies it, which can produce NaN
if rates or rates[to] is missing; add a runtime guard that checks data && typeof
data.rates === "object" && typeof data.rates[to] === "number" and if the check
fails throw or return a clear 400/500 error message indicating the Frankfurter
payload is malformed or the target currency is unsupported; calculate the
adjustment factor from a configurable source (e.g., an environment variable or a
function parameter with a safe default of 0.995) instead of hardcoding 0.995,
and use that factor when computing the rate to ensure no NaN is produced.

Comment on lines +157 to +159
const destination = getOfframpCurrencyConfig(account.country ?? selectedCountry!.id)

// handle offramp request creation
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Normalize destination country code before fetching currency config.

Saved accounts may store country as either 2- or 3-letter codes. Passing a normalized 3-letter code avoids mismatches in getOfframpCurrencyConfig.

Apply:

-            const destination = getOfframpCurrencyConfig(account.country ?? selectedCountry!.id)
+            const destination = getOfframpCurrencyConfig(
+                getCountryCodeForWithdraw((account.country ?? selectedCountry!.id) as string)
+            )
📝 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
const destination = getOfframpCurrencyConfig(account.country ?? selectedCountry!.id)
// handle offramp request creation
const destination = getOfframpCurrencyConfig(
getCountryCodeForWithdraw((account.country ?? selectedCountry!.id) as string)
)
// handle offramp request creation
🤖 Prompt for AI Agents
In the src/components/Claim/Link/views/BankFlowManager.view.tsx file, around
lines 157 to 159, the code is using the `getOfframpCurrencyConfig` function to
fetch the currency configuration based on the account's country. However, the
comment suggests that the country code stored in the account may be in either
2-letter or 3-letter format, which can lead to mismatches when fetching the
currency configuration. To fix this, normalize the country code to a consistent
3-letter format before passing it to the `getOfframpCurrencyConfig` function.
This will ensure that the correct currency configuration is retrieved,
regardless of the format of the stored country code.

Comment on lines +13 to +31
useEffect(() => {
// use ipapi.co to get the user's country code
const fetchCountry = async () => {
try {
const response = await fetch('https://ipapi.co/country')
if (!response.ok) {
throw new Error('Failed to fetch country')
}
const countryCode = await response.text()
setCountryCode(countryCode)
} catch (err: any) {
setError(err.message)
} finally {
setIsLoading(false)
}
}

fetchCountry()
}, [])
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Abort in-flight request on unmount to prevent state updates on unmounted component.

Without cancellation, fast navigations can trigger React warnings and memory leaks.

Use AbortController and guard abort errors:

-    useEffect(() => {
+    useEffect(() => {
+        const controller = new AbortController()
         // use ipapi.co to get the user's country code
         const fetchCountry = async () => {
             try {
-                const response = await fetch('https://ipapi.co/country')
+                const response = await fetch('https://ipapi.co/country', { signal: controller.signal })
                 if (!response.ok) {
                     throw new Error('Failed to fetch country')
                 }
-                const countryCode = await response.text()
-                setCountryCode(countryCode)
+                const code = (await response.text()).trim()
+                setCountryCode(code || null)
             } catch (err: any) {
-                setError(err.message)
+                if ((err as any)?.name !== 'AbortError') {
+                    setError((err as Error)?.message ?? 'Unknown error')
+                }
             } finally {
                 setIsLoading(false)
             }
         }
 
         fetchCountry()
-    }, [])
+        return () => controller.abort()
+    }, [])
📝 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
useEffect(() => {
// use ipapi.co to get the user's country code
const fetchCountry = async () => {
try {
const response = await fetch('https://ipapi.co/country')
if (!response.ok) {
throw new Error('Failed to fetch country')
}
const countryCode = await response.text()
setCountryCode(countryCode)
} catch (err: any) {
setError(err.message)
} finally {
setIsLoading(false)
}
}
fetchCountry()
}, [])
useEffect(() => {
const controller = new AbortController()
// use ipapi.co to get the user's country code
const fetchCountry = async () => {
try {
const response = await fetch('https://ipapi.co/country', { signal: controller.signal })
if (!response.ok) {
throw new Error('Failed to fetch country')
}
const code = (await response.text()).trim()
setCountryCode(code || null)
} catch (err: any) {
if ((err as any)?.name !== 'AbortError') {
setError((err as Error)?.message ?? 'Unknown error')
}
} finally {
setIsLoading(false)
}
}
fetchCountry()
return () => controller.abort()
}, [])
🤖 Prompt for AI Agents
In src/hooks/useGeoLocaion.ts around lines 13 to 31, the fetch call should be
cancellable to avoid state updates after unmount: create an AbortController
before calling fetch, pass controller.signal into fetch, and return a cleanup
function that calls controller.abort(); in the try/catch handle AbortError
specially (do not call setError for aborts) and ensure setCountryCode and
setIsLoading are only invoked when the request was not aborted (e.g., check
error.name === 'AbortError' and skip state updates), so no state is updated
after unmount.

Comment on lines +21 to +23
const countryCode = await response.text()
setCountryCode(countryCode)
} catch (err: any) {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Trim response text to avoid subtle bugs; also avoid shadowing state variable.

ipapi’s /country endpoint often returns a trailing newline. Not trimming can break equality checks and map lookups. Also, the local countryCode const shadows state.

Apply:

-                const countryCode = await response.text()
-                setCountryCode(countryCode)
+                const code = (await response.text()).trim()
+                setCountryCode(code || null)
📝 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
const countryCode = await response.text()
setCountryCode(countryCode)
} catch (err: any) {
const code = (await response.text()).trim()
setCountryCode(code || null)
🤖 Prompt for AI Agents
In src/hooks/useGeoLocaion.ts around lines 21 to 23, the code reads
response.text() into a const named countryCode which both may include trailing
whitespace/newlines from ipapi and shadows the state variable; change the local
binding to a different name (e.g., rawCountryCode), call .trim() on the response
text to remove trailing/newline/whitespace, then pass the trimmed value to
setCountryCode(trimmedCountryCode).

Comment on lines +16 to +18
user?.accounts.filter(
(acc) => acc.type === AccountType.IBAN || acc.type === AccountType.US || acc.type === AccountType.CLABE
) ?? []
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix optional chaining to avoid runtime crash when user.accounts is undefined

user?.accounts.filter will throw if user exists but accounts is undefined/null. Chain the filter call too.

Apply this diff:

-        return (
-            user?.accounts.filter(
-                (acc) => acc.type === AccountType.IBAN || acc.type === AccountType.US || acc.type === AccountType.CLABE
-            ) ?? []
-        )
+        return (
+            user?.accounts?.filter(
+                (acc) => acc.type === AccountType.IBAN || acc.type === AccountType.US || acc.type === AccountType.CLABE
+            ) ?? []
+        )
📝 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
user?.accounts.filter(
(acc) => acc.type === AccountType.IBAN || acc.type === AccountType.US || acc.type === AccountType.CLABE
) ?? []
return (
user?.accounts?.filter(
(acc) =>
acc.type === AccountType.IBAN ||
acc.type === AccountType.US ||
acc.type === AccountType.CLABE
) ?? []
)
🤖 Prompt for AI Agents
In src/hooks/useSavedAccounts.tsx around lines 16 to 18, the expression uses
user?.accounts.filter(...) which will throw if user exists but accounts is
null/undefined; change the access to safely chain into accounts (e.g.
user?.accounts?.filter(...) ?? []) so the filter call only executes when
accounts is defined, or otherwise return the empty array fallback.

Comment on lines +17 to +31
export const TOKEN_DETAILS = [
{
chainId: '1',
name: 'Ethereum',
tokens: [
{
address: '0x0000000000000000000000000000000000000000',
name: 'Ether',
symbol: 'ETH',
decimals: 18,
logoURI: 'https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/color/eth.svg',
},
],
},
}
]
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Unify chainId types across mocks (string vs number).

In this mock, TOKEN_DETAILS uses chainId: '1' (string) while CHAIN_DETAILS above uses numeric IDs. Downstream utilities frequently compare chainIds by strict equality; mixing types will cause lookups like find(chain.chainId === chainId) to fail silently.

Apply this diff to standardize on a number here (or standardize the whole codebase on strings—whichever the prod SDK expects):

-        chainId: '1',
+        chainId: 1,

Follow-up: if the real SDK now exposes string chainIds, flip CHAIN_DETAILS to strings instead and adjust tests. I can help generate a migration diff once you confirm the canonical type.

Run this to see what your code expects:


🏁 Script executed:

#!/bin/bash
# Inspect usages comparing chainId strictly to string/number
rg -nP -C2 "\bchainId\s*===\s*['\"][0-9]+['\"]|\bchainId\s*===\s*\d+" --type=ts

Length of output: 472


🏁 Script executed:

#!/bin/bash
# Locate peanut-sdk.ts in the repository
echo "Finding peanut-sdk.ts..."
fd peanut-sdk.ts

# Display CHAIN_DETAILS context in the mock file
echo "Inspecting CHAIN_DETAILS in src/utils/__mocks__/peanut-sdk.ts..."
rg -n "CHAIN_DETAILS" -C5 src/utils/__mocks__/peanut-sdk.ts

Length of output: 518


🏁 Script executed:

#!/bin/bash
# Check which version of peanut-sdk is installed to infer its chainId type
rg -n "\"peanut-sdk\"" -C1 package.json || echo "peanut-sdk not found in dependencies"

Length of output: 122


Standardize chainId types across mocks and code

We’ve confirmed that in src/utils/__mocks__/peanut-sdk.ts, CHAIN_DETAILS uses a numeric chainId (e.g. chainId: 1) while TOKEN_DETAILS uses a string (chainId: '1'). Meanwhile, production code strictly compares against a string literal (from.chainId === '1' in src/services/swap.ts), so these mismatches will lead to silent failures or broken tests.

Please apply the following refactors:

• In src/utils/__mocks__/peanut-sdk.ts

  • Change all TOKEN_DETAILS[n].chainId values from strings to numbers.
  • Ensure the chainId field on each entry in CHAIN_DETAILS remains the same type.

• In src/services/swap.ts (and any other strict comparisons)

  • Update comparisons against string literals to numeric comparisons, e.g.:
    - const isUsdtInMainnet = from.chainId === '1' && …
    + const isUsdtInMainnet = from.chainId === 1 && …

• After unifying on numbers, run the full test suite to catch any remaining chainId type mismatches.

These changes will eliminate inconsistent types between mocks and production code and prevent silent lookup failures.

📝 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
export const TOKEN_DETAILS = [
{
chainId: '1',
name: 'Ethereum',
tokens: [
{
address: '0x0000000000000000000000000000000000000000',
name: 'Ether',
symbol: 'ETH',
decimals: 18,
logoURI: 'https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/color/eth.svg',
},
],
},
}
]
export const TOKEN_DETAILS = [
{
chainId: 1,
name: 'Ethereum',
tokens: [
{
address: '0x0000000000000000000000000000000000000000',
name: 'Ether',
symbol: 'ETH',
decimals: 18,
logoURI: 'https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/color/eth.svg',
},
],
},
]
🤖 Prompt for AI Agents
In src/utils/__mocks__/peanut-sdk.ts around lines 17 to 31, TOKEN_DETAILS
currently uses string chainId values; change all TOKEN_DETAILS[n].chainId
entries from strings (e.g. '1') to numbers (e.g. 1) to match CHAIN_DETAILS
numeric types and tests; then search src/services/swap.ts (and other code that
does strict comparisons like from.chainId === '1') and update those comparisons
to numeric form (e.g. from.chainId === 1) so production code and mocks use the
same numeric type; run the full test suite to detect and fix any remaining
chainId type mismatches.

Comment on lines +136 to +147
// Returns the 3-letter country code for the given country code
export const getCountryCodeForWithdraw = (country: string) => {
// If the input is already a 3-digit code and exists in the map, return it
if (countryCodeMap[country]) {
return country
}

// If the input is a 2-digit code, find the corresponding 3-digit code
const threeDigitCode = Object.keys(countryCodeMap).find((key) => countryCodeMap[key] === country)

return threeDigitCode || country
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Normalize input and fix “digits” vs “letters” in docs; honor uppercase 3-letter keys.

Per the retrieved learning, countryCodeMap keys are uppercase 3-letter codes; values are uppercase 2-letter. Uppercasing input prevents subtle misses (e.g., 'usa', 'Us') and the function should return the 3-letter code when possible.

Apply:

-// Returns the 3-letter country code for the given country code
+// Returns the 3-letter country code for the given country code (accepts 2- or 3-letter; case-insensitive)
 export const getCountryCodeForWithdraw = (country: string) => {
-    // If the input is already a 3-digit code and exists in the map, return it
-    if (countryCodeMap[country]) {
-        return country
-    }
-
-    // If the input is a 2-digit code, find the corresponding 3-digit code
-    const threeDigitCode = Object.keys(countryCodeMap).find((key) => countryCodeMap[key] === country)
-
-    return threeDigitCode || country
+    const normalized = (country || '').toUpperCase()
+    // If the input is already a 3-letter code and exists in the map, return it
+    if (countryCodeMap[normalized]) {
+        return normalized
+    }
+    // If the input is a 2-letter code, find the corresponding 3-letter code
+    const threeLetterCode = Object.keys(countryCodeMap).find((key) => countryCodeMap[key] === normalized)
+    return threeLetterCode || normalized
 }

This directly addresses the uppercase requirement noted in your prior PRs.

📝 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
// Returns the 3-letter country code for the given country code
export const getCountryCodeForWithdraw = (country: string) => {
// If the input is already a 3-digit code and exists in the map, return it
if (countryCodeMap[country]) {
return country
}
// If the input is a 2-digit code, find the corresponding 3-digit code
const threeDigitCode = Object.keys(countryCodeMap).find((key) => countryCodeMap[key] === country)
return threeDigitCode || country
}
// Returns the 3-letter country code for the given country code (accepts 2- or 3-letter; case-insensitive)
export const getCountryCodeForWithdraw = (country: string) => {
const normalized = (country || '').toUpperCase()
// If the input is already a 3-letter code and exists in the map, return it
if (countryCodeMap[normalized]) {
return normalized
}
// If the input is a 2-letter code, find the corresponding 3-letter code
const threeLetterCode = Object.keys(countryCodeMap).find((key) => countryCodeMap[key] === normalized)
return threeLetterCode || normalized
}
🤖 Prompt for AI Agents
In src/utils/withdraw.utils.ts around lines 136 to 147, the comment refers to
"digits" but country codes are letters and the function doesn't normalize input
casing; change the docs to say "3-letter/2-letter country code", uppercase the
incoming country string, treat a 3-character input by uppercasing and checking
against the uppercase 3-letter keys in countryCodeMap (return the key if
present), and for 2-character inputs uppercase and find the corresponding
3-letter key by comparing against the map values; return the found 3-letter code
(or the original input if no mapping exists).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants