From 5f03ecb4e05bc2c0263ac58dfd745e35bad9e3a9 Mon Sep 17 00:00:00 2001 From: shrinathprabhu Date: Thu, 16 Jul 2026 13:35:39 +0530 Subject: [PATCH 01/28] loading spinner faster --- public/r/nexus.json | 16 ++++---- .../components/deposit-idle-form.tsx | 8 +++- .../nexus-widget-progress-screen.tsx | 14 +++++-- .../components/pay-with-sources.tsx | 7 +++- .../components/send-idle-form.tsx | 8 +++- .../components/swap-asset-selector.tsx | 3 +- .../components/swap-intent-preview.tsx | 6 ++- .../nexus-widget/nexus-widget.tsx | 40 ++++++++++--------- registry/avail-widgets/nexus-widget/theme.ts | 5 +++ 9 files changed, 72 insertions(+), 35 deletions(-) diff --git a/public/r/nexus.json b/public/r/nexus.json index 721946eb..56a8ac7d 100644 --- a/public/r/nexus.json +++ b/public/r/nexus.json @@ -32,13 +32,13 @@ }, { "path": "registry/avail-widgets/nexus-widget/components/deposit-idle-form.tsx", - "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\nimport Decimal from \"decimal.js\";\nimport { AlertCircle, ChevronDown, Loader2 } from \"lucide-react\";\nimport React, { useRef, useState } from \"react\";\nimport { PayWithSources as SharedPayWithSources } from \"./pay-with-sources\";\nimport {\n formatSelectedTokenBalanceLabel,\n formatUsdBalanceLabel,\n type SwapTokenOption,\n} from \"./swap-asset-selector\";\n\ninterface DepositIdleFormProps {\n amount: string;\n amountMode: \"token\" | \"usd\";\n calculatingPercent?: number | null;\n fromTokens: SwapTokenOption[];\n isAmountReadOnly?: boolean;\n isBalanceLoading?: boolean;\n isCalculatingMax?: boolean;\n isQuoteRefreshing?: boolean;\n isSourcePickerDisabled?: boolean;\n isTokenPickerDisabled?: boolean;\n hideDestinationTokenDropdownIcon?: boolean;\n onAmountChange: (val: string) => void;\n onAmountModeToggle: () => void;\n onOpenSourcePicker: () => void;\n onOpenTokenPicker?: () => void;\n onSetPercent: (pct: number) => void;\n reserveSourceRows?: boolean;\n routeMessage?: string;\n routeStatus?: \"loading\" | \"insufficient\";\n showAutoBadge?: boolean;\n tokenValue: string;\n toToken?: SwapTokenOption;\n totalBalance: string;\n usdValue: string;\n}\n\nconst uiFont = '\"Geist\", system-ui, sans-serif';\nconst primary = \"#161615\";\nconst muted = \"#848483\";\nconst border = \"#E8E8E7\";\nconst brand = \"var(--foreground-brand)\";\n\nconst parseDecimal = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = String(value).replace(/[^0-9.-]/g, \"\");\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst formatToken = (value: unknown) => {\n const amount = parseDecimal(value) ?? new Decimal(0);\n return amount.toDecimalPlaces(8).toFixed();\n};\n\nconst formatUsd = (value: unknown) => {\n const amount = parseDecimal(value) ?? new Decimal(0);\n if (amount.gt(0) && amount.lt(0.01)) return \"<$0.01\";\n return `$${amount.toDecimalPlaces(2).toFixed()}`;\n};\n\nconst MAX_AMOUNT_DISPLAY_DECIMALS = 8;\nconst getTokenInputDecimals = (token?: Pick) => {\n const decimals = Number(token?.decimals);\n return Number.isFinite(decimals) && decimals >= 0 ? Math.floor(decimals) : 18;\n};\n\nconst formatAmountInputDisplay = (value: string) => {\n if (!value) return \"\";\n try {\n return new Decimal(value)\n .toDecimalPlaces(MAX_AMOUNT_DISPLAY_DECIMALS, Decimal.ROUND_DOWN)\n .toFixed();\n } catch {\n return value;\n }\n};\n\nconst sanitizeAmountInput = (raw: string, maxDecimals: number) => {\n let next = raw.replaceAll(/[^0-9.]/g, \"\");\n const parts = next.split(\".\");\n if (parts.length > 2) next = parts[0] + \".\" + parts.slice(1).join(\"\");\n const [integerPart, decimalPart] = next.split(\".\");\n if (decimalPart !== undefined) {\n next = `${integerPart}.${decimalPart.slice(0, Math.max(0, maxDecimals))}`;\n }\n if (next === \".\") next = \"0.\";\n return next;\n};\n\nfunction TokenLogo({\n src,\n label,\n size = 30,\n fontSize = 12,\n style,\n}: {\n src?: string;\n label?: string;\n size?: number;\n fontSize?: number;\n style?: React.CSSProperties;\n}) {\n const [failed, setFailed] = useState(!src);\n\n React.useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n backgroundColor: \"#FFFFFE\",\n borderRadius: \"999px\",\n height: size,\n objectFit: \"cover\",\n width: size,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {(label || \"?\").slice(0, 1).toUpperCase()}\n \n );\n}\n\nfunction SourceLogoPair({ token }: { token: SwapTokenOption }) {\n return (\n
\n \n {token.chainLogo && (\n \n )}\n
\n );\n}\n\nfunction ExactOutPercentButtons({\n visible,\n onSelect,\n}: {\n visible: boolean;\n onSelect: (pct: number) => void;\n}) {\n const [focusedPercent, setFocusedPercent] = useState(null);\n\n React.useEffect(() => {\n if (!visible) setFocusedPercent(null);\n }, [visible]);\n\n return (\n \n {[25, 50, 75, 100].map((pct) => {\n const isMax = pct === 100;\n const isFocused = focusedPercent === pct;\n return (\n {\n e.stopPropagation();\n onSelect(pct);\n }}\n onBlur={() => setFocusedPercent(null)}\n onFocus={() => setFocusedPercent(pct)}\n style={{\n alignItems: \"center\",\n backgroundColor: isFocused ? \"#E8F0FF\" : \"#F4F4F3\",\n border: \"none\",\n borderRadius: \"8px\",\n boxSizing: \"border-box\",\n color: isFocused ? brand : \"#363635\",\n cursor: \"pointer\",\n display: \"flex\",\n flex: \"1 1 0%\",\n fontFamily: '\"Geist\", system-ui, sans-serif',\n fontSize: \"12px\",\n fontWeight: 500,\n justifyContent: \"center\",\n lineHeight: \"20px\",\n minWidth: 0,\n paddingBlock: \"5px\",\n paddingInline: \"10px\",\n }}\n type=\"button\"\n >\n {isMax ? \"MAX\" : `${pct}%`}\n \n );\n })}\n \n );\n}\n\nfunction SkeletonRow() {\n return (\n \n \n \n \n );\n}\n\nfunction PayWithSources({\n fromTokens,\n onOpenSourcePicker,\n routeStatus,\n routeMessage,\n}: {\n fromTokens: SwapTokenOption[];\n onOpenSourcePicker: () => void;\n routeStatus?: \"loading\" | \"insufficient\";\n routeMessage?: string;\n}) {\n const scrollRef = useRef(null);\n const isRouteLoading = routeStatus === \"loading\";\n const shouldShowSourceSummary = !isRouteLoading && fromTokens.length > 0;\n const shouldScroll = shouldShowSourceSummary && fromTokens.length > 3;\n const autoBadge = (\n \n AUTO\n \n );\n\n return (\n \n \n \n \n Pay With\n {shouldShowSourceSummary ? ` · ${fromTokens.length} assets` : \"\"}\n \n {autoBadge}\n \n {shouldShowSourceSummary && (\n \n Edit tokens\n \n )}\n \n\n {isRouteLoading ? (\n <>\n \n \n \n Calculating best route...\n \n \n ) : shouldShowSourceSummary ? (\n
\n \n {fromTokens.map((token, index) => (\n \n \n \n \n \n {token.symbol}\n \n \n {token.isUnified\n ? \"Unified balance\"\n : `on ${token.chainName || \"Unknown chain\"}`}\n \n
\n \n \n \n {formatToken(token.userAmount || token.balance)}{\" \"}\n {token.symbol}\n \n \n {formatUsd(token.userAmountUsd || token.balanceInFiat)}\n \n \n \n ))}\n \n {shouldScroll && (\n \n scrollRef.current?.scrollBy({ behavior: \"smooth\", top: 64 })\n }\n style={{\n alignItems: \"center\",\n background: \"#FFFFFE\",\n border: `1px solid ${border}`,\n borderRadius: \"999px\",\n bottom: \"4px\",\n boxShadow: \"0 2px 8px rgba(22,22,21,0.08)\",\n cursor: \"pointer\",\n display: \"flex\",\n height: \"22px\",\n justifyContent: \"center\",\n left: \"50%\",\n padding: 0,\n position: \"absolute\",\n transform: \"translateX(-50%)\",\n width: \"22px\",\n }}\n type=\"button\"\n >\n \n \n )}\n \n ) : (\n \n Sources will be auto selected\n \n )}\n\n {routeStatus === \"insufficient\" && routeMessage && (\n \n \n {routeMessage}\n \n )}\n \n );\n}\n\nfunction BalanceSkeleton({\n height = \"16px\",\n width = \"72px\",\n}: {\n height?: string;\n width?: string;\n}) {\n return (\n \n );\n}\n\nexport function DepositIdleForm({\n amount,\n amountMode,\n onAmountChange,\n onAmountModeToggle,\n toToken,\n totalBalance,\n usdValue,\n tokenValue,\n fromTokens,\n onOpenSourcePicker,\n onSetPercent,\n routeStatus,\n routeMessage,\n isAmountReadOnly = false,\n isBalanceLoading = false,\n isCalculatingMax,\n calculatingPercent,\n isQuoteRefreshing,\n hideDestinationTokenDropdownIcon = false,\n showAutoBadge = true,\n isSourcePickerDisabled = false,\n isTokenPickerDisabled = false,\n onOpenTokenPicker,\n reserveSourceRows = false,\n}: DepositIdleFormProps) {\n const [pendingPercent, setPendingPercent] = useState(null);\n const [isAmountFocused, setIsAmountFocused] = useState(false);\n\n React.useEffect(() => {\n if (!isCalculatingMax) setPendingPercent(null);\n }, [isCalculatingMax]);\n\n const handlePercentSelect = (pct: number) => {\n setPendingPercent(pct);\n onSetPercent(pct);\n };\n\n const handleInput = (e: React.ChangeEvent) => {\n if (isAmountReadOnly) return;\n onAmountChange(\n sanitizeAmountInput(\n e.target.value,\n isUsdMode\n ? MAX_AMOUNT_DISPLAY_DECIMALS\n : getTokenInputDecimals(toToken),\n ),\n );\n };\n const isUsdMode = amountMode === \"usd\";\n const amountDisplayValue = isAmountFocused\n ? amount\n : formatAmountInputDisplay(amount);\n const showTokenPickerDropdownIcon =\n Boolean(onOpenTokenPicker) &&\n !isTokenPickerDisabled &&\n !hideDestinationTokenDropdownIcon;\n const activePendingPercent =\n calculatingPercent ?? (isCalculatingMax ? pendingPercent : null);\n const isMaxCalculating = Boolean(\n isCalculatingMax && activePendingPercent === 100,\n );\n const destinationBalanceLabel = isUsdMode\n ? formatUsdBalanceLabel(toToken?.balanceInFiat)\n : formatSelectedTokenBalanceLabel(toToken) ||\n `0 ${toToken?.symbol || \"\"}`.trim();\n\n return (\n \n \n \n \n Deposit\n \n
\n \n Total Balance:\n \n \n {isBalanceLoading ? (\n \n ) : (\n `$${totalBalance}`\n )}\n \n
\n \n\n
\n \n \n {isMaxCalculating ? (\n \n ) : (\n <>\n {isUsdMode && amount && (\n \n $\n \n )}\n setIsAmountFocused(false)}\n onChange={handleInput}\n onFocus={() => setIsAmountFocused(true)}\n placeholder=\"0\"\n style={{\n background: \"transparent\",\n border: \"none\",\n boxSizing: \"border-box\",\n color: amount ? primary : \"#C8C8C6\",\n cursor: isAmountReadOnly ? \"default\" : \"text\",\n fontFamily:\n '\"Delight-Medium\", \"Delight\", system-ui, sans-serif',\n fontSize: \"36px\",\n fontWeight: 500,\n lineHeight: \"44px\",\n minWidth: 0,\n outline: \"none\",\n padding: 0,\n width: \"100%\",\n }}\n readOnly={isAmountReadOnly}\n type=\"text\"\n value={amountDisplayValue}\n />\n \n )}\n {isCalculatingMax && !isMaxCalculating && (\n \n )}\n
\n\n \n \n \n {toToken?.chainLogo && (\n \n )}\n \n \n {toToken?.symbol || \"Token\"}\n \n {showTokenPickerDropdownIcon && (\n \n )}\n \n \n\n \n \n \n {isUsdMode\n ? `≈ ${tokenValue || \"0\"} ${toToken?.symbol || \"\"} ↕`\n : `≈ $${usdValue || \"0\"} ↕`}\n \n \n\n \n \n Balance:\n \n \n {isBalanceLoading ? (\n \n ) : (\n destinationBalanceLabel\n )}\n \n \n \n\n \n \n \n\n \n \n );\n}\n", + "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\nimport Decimal from \"decimal.js\";\nimport { AlertCircle, ChevronDown, Loader2 } from \"lucide-react\";\nimport React, { useRef, useState } from \"react\";\nimport { PayWithSources as SharedPayWithSources } from \"./pay-with-sources\";\nimport { NEXUS_WIDGET_FAST_SPINNER_STYLE } from \"../theme\";\nimport {\n formatSelectedTokenBalanceLabel,\n formatUsdBalanceLabel,\n type SwapTokenOption,\n} from \"./swap-asset-selector\";\n\ninterface DepositIdleFormProps {\n amount: string;\n amountMode: \"token\" | \"usd\";\n calculatingPercent?: number | null;\n fromTokens: SwapTokenOption[];\n isAmountReadOnly?: boolean;\n isBalanceLoading?: boolean;\n isCalculatingMax?: boolean;\n isQuoteRefreshing?: boolean;\n isSourcePickerDisabled?: boolean;\n isTokenPickerDisabled?: boolean;\n hideDestinationTokenDropdownIcon?: boolean;\n onAmountChange: (val: string) => void;\n onAmountModeToggle: () => void;\n onOpenSourcePicker: () => void;\n onOpenTokenPicker?: () => void;\n onSetPercent: (pct: number) => void;\n reserveSourceRows?: boolean;\n routeMessage?: string;\n routeStatus?: \"loading\" | \"insufficient\";\n showAutoBadge?: boolean;\n tokenValue: string;\n toToken?: SwapTokenOption;\n totalBalance: string;\n usdValue: string;\n}\n\nconst uiFont = '\"Geist\", system-ui, sans-serif';\nconst primary = \"#161615\";\nconst muted = \"#848483\";\nconst border = \"#E8E8E7\";\nconst brand = \"var(--foreground-brand)\";\n\nconst parseDecimal = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = String(value).replace(/[^0-9.-]/g, \"\");\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst formatToken = (value: unknown) => {\n const amount = parseDecimal(value) ?? new Decimal(0);\n return amount.toDecimalPlaces(8).toFixed();\n};\n\nconst formatUsd = (value: unknown) => {\n const amount = parseDecimal(value) ?? new Decimal(0);\n if (amount.gt(0) && amount.lt(0.01)) return \"<$0.01\";\n return `$${amount.toDecimalPlaces(2).toFixed()}`;\n};\n\nconst MAX_AMOUNT_DISPLAY_DECIMALS = 8;\nconst getTokenInputDecimals = (token?: Pick) => {\n const decimals = Number(token?.decimals);\n return Number.isFinite(decimals) && decimals >= 0 ? Math.floor(decimals) : 18;\n};\n\nconst formatAmountInputDisplay = (value: string) => {\n if (!value) return \"\";\n try {\n return new Decimal(value)\n .toDecimalPlaces(MAX_AMOUNT_DISPLAY_DECIMALS, Decimal.ROUND_DOWN)\n .toFixed();\n } catch {\n return value;\n }\n};\n\nconst sanitizeAmountInput = (raw: string, maxDecimals: number) => {\n let next = raw.replaceAll(/[^0-9.]/g, \"\");\n const parts = next.split(\".\");\n if (parts.length > 2) next = parts[0] + \".\" + parts.slice(1).join(\"\");\n const [integerPart, decimalPart] = next.split(\".\");\n if (decimalPart !== undefined) {\n next = `${integerPart}.${decimalPart.slice(0, Math.max(0, maxDecimals))}`;\n }\n if (next === \".\") next = \"0.\";\n return next;\n};\n\nfunction TokenLogo({\n src,\n label,\n size = 30,\n fontSize = 12,\n style,\n}: {\n src?: string;\n label?: string;\n size?: number;\n fontSize?: number;\n style?: React.CSSProperties;\n}) {\n const [failed, setFailed] = useState(!src);\n\n React.useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n backgroundColor: \"#FFFFFE\",\n borderRadius: \"999px\",\n height: size,\n objectFit: \"cover\",\n width: size,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {(label || \"?\").slice(0, 1).toUpperCase()}\n \n );\n}\n\nfunction SourceLogoPair({ token }: { token: SwapTokenOption }) {\n return (\n
\n \n {token.chainLogo && (\n \n )}\n
\n );\n}\n\nfunction ExactOutPercentButtons({\n visible,\n onSelect,\n}: {\n visible: boolean;\n onSelect: (pct: number) => void;\n}) {\n const [focusedPercent, setFocusedPercent] = useState(null);\n\n React.useEffect(() => {\n if (!visible) setFocusedPercent(null);\n }, [visible]);\n\n return (\n \n {[25, 50, 75, 100].map((pct) => {\n const isMax = pct === 100;\n const isFocused = focusedPercent === pct;\n return (\n {\n e.stopPropagation();\n onSelect(pct);\n }}\n onBlur={() => setFocusedPercent(null)}\n onFocus={() => setFocusedPercent(pct)}\n style={{\n alignItems: \"center\",\n backgroundColor: isFocused ? \"#E8F0FF\" : \"#F4F4F3\",\n border: \"none\",\n borderRadius: \"8px\",\n boxSizing: \"border-box\",\n color: isFocused ? brand : \"#363635\",\n cursor: \"pointer\",\n display: \"flex\",\n flex: \"1 1 0%\",\n fontFamily: '\"Geist\", system-ui, sans-serif',\n fontSize: \"12px\",\n fontWeight: 500,\n justifyContent: \"center\",\n lineHeight: \"20px\",\n minWidth: 0,\n paddingBlock: \"5px\",\n paddingInline: \"10px\",\n }}\n type=\"button\"\n >\n {isMax ? \"MAX\" : `${pct}%`}\n \n );\n })}\n \n );\n}\n\nfunction SkeletonRow() {\n return (\n \n \n \n \n );\n}\n\nfunction PayWithSources({\n fromTokens,\n onOpenSourcePicker,\n routeStatus,\n routeMessage,\n}: {\n fromTokens: SwapTokenOption[];\n onOpenSourcePicker: () => void;\n routeStatus?: \"loading\" | \"insufficient\";\n routeMessage?: string;\n}) {\n const scrollRef = useRef(null);\n const isRouteLoading = routeStatus === \"loading\";\n const shouldShowSourceSummary = !isRouteLoading && fromTokens.length > 0;\n const shouldScroll = shouldShowSourceSummary && fromTokens.length > 3;\n const autoBadge = (\n \n AUTO\n \n );\n\n return (\n \n \n \n \n Pay With\n {shouldShowSourceSummary ? ` · ${fromTokens.length} assets` : \"\"}\n \n {autoBadge}\n \n {shouldShowSourceSummary && (\n \n Edit tokens\n \n )}\n \n\n {isRouteLoading ? (\n <>\n \n \n \n Calculating best route...\n \n \n ) : shouldShowSourceSummary ? (\n
\n \n {fromTokens.map((token, index) => (\n \n \n \n \n \n {token.symbol}\n \n \n {token.isUnified\n ? \"Unified balance\"\n : `on ${token.chainName || \"Unknown chain\"}`}\n \n
\n \n \n \n {formatToken(token.userAmount || token.balance)}{\" \"}\n {token.symbol}\n \n \n {formatUsd(token.userAmountUsd || token.balanceInFiat)}\n \n \n \n ))}\n \n {shouldScroll && (\n \n scrollRef.current?.scrollBy({ behavior: \"smooth\", top: 64 })\n }\n style={{\n alignItems: \"center\",\n background: \"#FFFFFE\",\n border: `1px solid ${border}`,\n borderRadius: \"999px\",\n bottom: \"4px\",\n boxShadow: \"0 2px 8px rgba(22,22,21,0.08)\",\n cursor: \"pointer\",\n display: \"flex\",\n height: \"22px\",\n justifyContent: \"center\",\n left: \"50%\",\n padding: 0,\n position: \"absolute\",\n transform: \"translateX(-50%)\",\n width: \"22px\",\n }}\n type=\"button\"\n >\n \n \n )}\n \n ) : (\n \n Sources will be auto selected\n \n )}\n\n {routeStatus === \"insufficient\" && routeMessage && (\n \n \n {routeMessage}\n \n )}\n \n );\n}\n\nfunction BalanceSkeleton({\n height = \"16px\",\n width = \"72px\",\n}: {\n height?: string;\n width?: string;\n}) {\n return (\n \n );\n}\n\nexport function DepositIdleForm({\n amount,\n amountMode,\n onAmountChange,\n onAmountModeToggle,\n toToken,\n totalBalance,\n usdValue,\n tokenValue,\n fromTokens,\n onOpenSourcePicker,\n onSetPercent,\n routeStatus,\n routeMessage,\n isAmountReadOnly = false,\n isBalanceLoading = false,\n isCalculatingMax,\n calculatingPercent,\n isQuoteRefreshing,\n hideDestinationTokenDropdownIcon = false,\n showAutoBadge = true,\n isSourcePickerDisabled = false,\n isTokenPickerDisabled = false,\n onOpenTokenPicker,\n reserveSourceRows = false,\n}: DepositIdleFormProps) {\n const [pendingPercent, setPendingPercent] = useState(null);\n const [isAmountFocused, setIsAmountFocused] = useState(false);\n\n React.useEffect(() => {\n if (!isCalculatingMax) setPendingPercent(null);\n }, [isCalculatingMax]);\n\n const handlePercentSelect = (pct: number) => {\n setPendingPercent(pct);\n onSetPercent(pct);\n };\n\n const handleInput = (e: React.ChangeEvent) => {\n if (isAmountReadOnly) return;\n onAmountChange(\n sanitizeAmountInput(\n e.target.value,\n isUsdMode\n ? MAX_AMOUNT_DISPLAY_DECIMALS\n : getTokenInputDecimals(toToken),\n ),\n );\n };\n const isUsdMode = amountMode === \"usd\";\n const amountDisplayValue = isAmountFocused\n ? amount\n : formatAmountInputDisplay(amount);\n const showTokenPickerDropdownIcon =\n Boolean(onOpenTokenPicker) &&\n !isTokenPickerDisabled &&\n !hideDestinationTokenDropdownIcon;\n const activePendingPercent =\n calculatingPercent ?? (isCalculatingMax ? pendingPercent : null);\n const isMaxCalculating = Boolean(\n isCalculatingMax && activePendingPercent === 100,\n );\n const destinationBalanceLabel = isUsdMode\n ? formatUsdBalanceLabel(toToken?.balanceInFiat)\n : formatSelectedTokenBalanceLabel(toToken) ||\n `0 ${toToken?.symbol || \"\"}`.trim();\n\n return (\n \n \n \n \n Deposit\n \n
\n \n Total Balance:\n \n \n {isBalanceLoading ? (\n \n ) : (\n `$${totalBalance}`\n )}\n \n
\n \n\n
\n \n \n {isMaxCalculating ? (\n \n ) : (\n <>\n {isUsdMode && amount && (\n \n $\n \n )}\n setIsAmountFocused(false)}\n onChange={handleInput}\n onFocus={() => setIsAmountFocused(true)}\n placeholder=\"0\"\n style={{\n background: \"transparent\",\n border: \"none\",\n boxSizing: \"border-box\",\n color: amount ? primary : \"#C8C8C6\",\n cursor: isAmountReadOnly ? \"default\" : \"text\",\n fontFamily:\n '\"Delight-Medium\", \"Delight\", system-ui, sans-serif',\n fontSize: \"36px\",\n fontWeight: 500,\n lineHeight: \"44px\",\n minWidth: 0,\n outline: \"none\",\n padding: 0,\n width: \"100%\",\n }}\n readOnly={isAmountReadOnly}\n type=\"text\"\n value={amountDisplayValue}\n />\n \n )}\n {isCalculatingMax && !isMaxCalculating && (\n \n )}\n
\n\n \n \n \n {toToken?.chainLogo && (\n \n )}\n \n \n {toToken?.symbol || \"Token\"}\n \n {showTokenPickerDropdownIcon && (\n \n )}\n \n \n\n \n \n \n {isUsdMode\n ? `≈ ${tokenValue || \"0\"} ${toToken?.symbol || \"\"} ↕`\n : `≈ $${usdValue || \"0\"} ↕`}\n \n \n\n \n \n Balance:\n \n \n {isBalanceLoading ? (\n \n ) : (\n destinationBalanceLabel\n )}\n \n \n \n\n \n \n \n\n \n \n );\n}\n", "type": "registry:component", "target": "components/nexus/components/deposit-idle-form.tsx" }, { "path": "registry/avail-widgets/nexus-widget/components/nexus-widget-progress-screen.tsx", - "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\n\"use client\";\n\nimport Decimal from \"decimal.js\";\nimport { Check, ChevronDown, Loader2, X } from \"lucide-react\";\nimport React, { useEffect, useState } from \"react\";\nimport type {\n BridgeStepType,\n SwapStepType,\n} from \"../../common/types/transaction-flow\";\nimport { getShortChainName } from \"../../common/utils/constant\";\nimport {\n type NexusWidgetDepositOpportunityMetadata,\n type NexusWidgetMode,\n} from \"../types\";\nimport { type SwapTokenOption } from \"./swap-asset-selector\";\nimport { type SwapIntentData } from \"./swap-intent-preview\";\n\ntype ProgressSdkStep = SwapStepType | BridgeStepType;\n\ntype ProgressStep = {\n id: number;\n completed: boolean;\n step: ProgressSdkStep;\n};\n\nexport type NexusWidgetProgressEvent = {\n id: string;\n name: string;\n completed: boolean;\n event?: unknown;\n step?: ProgressSdkStep;\n steps?: ProgressSdkStep[];\n};\n\ninterface NexusWidgetProgressScreenProps {\n failedStep?: ProgressSdkStep | null;\n fromAmountUsd?: string;\n fromTokens?: SwapTokenOption[];\n intentData?: SwapIntentData | null;\n mode: NexusWidgetMode;\n opportunity?: NexusWidgetDepositOpportunityMetadata;\n progressEvents?: NexusWidgetProgressEvent[];\n recipientAddress?: string;\n steps?: ProgressStep[];\n toAmount?: string;\n toAmountUsd?: string;\n toToken?: SwapTokenOption;\n totalFeeUsd?: string;\n}\n\nconst fontFamily = '\"Geist\", var(--font-geist-sans), system-ui, sans-serif';\nconst primary = \"var(--foreground-primary, #161615)\";\nconst muted = \"var(--foreground-muted, #848483)\";\nconst border = \"var(--border-default, #E8E8E7)\";\nconst brand = \"var(--foreground-brand)\";\nconst danger = \"var(--foreground-negative, #E92C2C)\";\n\nconst parseDecimal = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = String(value).replace(/[^0-9.-]/g, \"\");\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst formatDecimal = (value: unknown, decimals = 2) =>\n (parseDecimal(value) ?? new Decimal(0)).toDecimalPlaces(decimals).toFixed();\n\nconst formatUsd = (value: unknown) => `$${formatDecimal(value, 2)}`;\n\nconst unique = (values: Array) =>\n Array.from(new Set(values.filter(Boolean) as string[]));\n\nconst formatSymbolSummary = (symbols: string[]) => {\n if (symbols.length <= 2) return symbols.join(\", \");\n return `${symbols.slice(0, 2).join(\", \")} and ${symbols.length - 2} others`;\n};\n\nconst getStepType = (step?: ProgressSdkStep) =>\n String((step as any)?.type ?? (step as any)?.typeID ?? \"\").toUpperCase();\n\ntype ProgressStatusId =\n | \"approveTokens\"\n | \"swapTokens\"\n | \"receiveToken\"\n | \"action\";\n\ntype ProgressStatusState =\n | \"default\"\n | \"preapproval\"\n | \"inProgress\"\n | \"completed\"\n | \"error\";\n\ntype ProgressStatusRow = {\n id: ProgressStatusId;\n label: string;\n description?: string;\n state: ProgressStatusState;\n};\n\nconst PROGRESS_EVENT_NAMES = {\n BRIDGE_PLAN_LIST: \"bridge_plan_list\",\n BRIDGE_PLAN_PROGRESS: \"bridge_plan_progress\",\n SWAP_PLAN_LIST: \"swap_plan_list\",\n SWAP_PLAN_PROGRESS: \"swap_plan_progress\",\n} as const;\n\ntype ProgressListEventName =\n | typeof PROGRESS_EVENT_NAMES.BRIDGE_PLAN_LIST\n | typeof PROGRESS_EVENT_NAMES.SWAP_PLAN_LIST;\n\nconst STATUS_ORDER: ProgressStatusId[] = [\n \"approveTokens\",\n \"swapTokens\",\n \"receiveToken\",\n \"action\",\n];\n\nconst SWAP_APPROVAL_TYPES = [\n \"SOURCE_SWAP\",\n \"CREATE_PERMIT_EOA_TO_EPHEMERAL\",\n \"CREATE_PERMIT_FOR_SOURCE_SWAP\",\n \"EOA_TO_EPHEMERAL_TRANSFER\",\n \"EOA_EXECUTE_CALL\",\n // \"BRIDGE_DEPOSIT\",\n];\n\nconst REFUND_ELIGIBLE_SWAP_TYPES = [\n \"BRIDGE_INTENT_SUBMISSION\",\n \"BRIDGE_DEPOSIT\",\n];\n\nconst DESTINATION_SWAP_TYPES = [\n \"DESTINATION_SWAP\",\n \"DESTINATION_SWAP_BATCH_TX\",\n \"DESTINATION_SWAP_HASH\",\n];\nconst BRIDGE_FILL_RECEIVE_TYPES = [\"BRIDGE_FILL\"];\n\nconst getStatusForStep = (\n step: ProgressSdkStep | undefined,\n mode: NexusWidgetMode,\n hasTransferAction = false,\n): ProgressStatusId | null => {\n const type = getStepType(step);\n\n if (\n type === \"APPROVAL\" ||\n type === \"TRANSACTION_SENT\" ||\n type === \"TRANSACTION_CONFIRMED\"\n ) {\n return mode === \"swap\" && !hasTransferAction ? null : \"action\";\n }\n\n if (type.includes(\"SWAP_START\")) {\n return \"swapTokens\";\n }\n\n if (SWAP_APPROVAL_TYPES.some((token) => type.includes(token))) {\n return \"approveTokens\";\n }\n\n if (\n type.includes(\"SOURCE_SWAP\") ||\n type.includes(\"SOURCE_BATCH\") ||\n type.includes(\"SWAP_SOURCE\") ||\n type.includes(\"BRIDGE_DEPOSIT\") ||\n type.includes(\"BRIDGE_FILL\") ||\n type.includes(\"BRIDGE_INTENT_SUBMISSION\") ||\n type.includes(\"SWAP_COMPLETE\") ||\n type.includes(\"SWAP_SKIPPED\")\n ) {\n return \"swapTokens\";\n }\n\n if (type.includes(\"DESTINATION_SWAP\") || type.includes(\"DESTINATION_BATCH\")) {\n return \"receiveToken\";\n }\n\n return null;\n};\n\nconst stepMatches = (step: ProgressSdkStep | undefined, tokens: string[]) => {\n const type = getStepType(step);\n return tokens.some((token) => type.includes(token));\n};\n\nconst hasCompletedType = (\n events: NexusWidgetProgressEvent[],\n steps: ProgressStep[],\n tokens: string[],\n) => {\n const completedEvent = events.some(\n (event) => event.completed && stepMatches(event.step, tokens),\n );\n if (completedEvent) return true;\n\n return steps.some((item) => item.completed && stepMatches(item.step, tokens));\n};\n\nconst hasStepType = (\n events: NexusWidgetProgressEvent[],\n steps: ProgressStep[],\n tokens: string[],\n) =>\n events.some(\n (event) =>\n stepMatches(event.step, tokens) ||\n (event.steps ?? []).some((step) => stepMatches(step, tokens)),\n ) || steps.some((item) => stepMatches(item.step, tokens));\n\nconst hasEventType = (events: NexusWidgetProgressEvent[], tokens: string[]) =>\n events.some(\n (event) =>\n stepMatches(event.step, tokens) ||\n (event.steps ?? []).some((step) => stepMatches(step, tokens)),\n );\n\nconst hasProgressEventType = (\n events: NexusWidgetProgressEvent[],\n tokens: string[],\n) =>\n events.some(\n (event) =>\n (event.name === PROGRESS_EVENT_NAMES.BRIDGE_PLAN_PROGRESS ||\n event.name === PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS) &&\n stepMatches(event.step, tokens),\n );\n\nconst getListedSteps = (\n events: NexusWidgetProgressEvent[],\n eventName: ProgressListEventName,\n) => {\n const listEvent = [...events]\n .reverse()\n .find(\n (event) => event.name === eventName && (event.steps?.length ?? 0) > 0,\n );\n return listEvent?.steps ?? [];\n};\n\nconst countListedSteps = (steps: ProgressSdkStep[], tokens: string[]) =>\n steps.filter((step) => stepMatches(step, tokens)).length;\n\ntype ApprovalUnit = {\n symbol?: string;\n};\n\nconst getStepSwaps = (step?: ProgressSdkStep) => {\n const swaps = (step as any)?.swaps;\n return Array.isArray(swaps) ? swaps : [];\n};\n\nconst getApprovalUnitsForStep = (step?: ProgressSdkStep): ApprovalUnit[] => {\n if (!stepMatches(step, SWAP_APPROVAL_TYPES)) return [];\n\n const swaps = getStepSwaps(step);\n if (swaps.length > 0) {\n return swaps.map((swap) => ({\n symbol:\n typeof swap?.input?.symbol === \"string\" ? swap.input.symbol : undefined,\n }));\n }\n\n const assetSymbol = (step as any)?.asset?.symbol;\n const tokenSymbol = (step as any)?.token?.symbol;\n return [\n {\n symbol:\n typeof assetSymbol === \"string\"\n ? assetSymbol\n : typeof tokenSymbol === \"string\"\n ? tokenSymbol\n : undefined,\n },\n ];\n};\n\nconst countApprovalUnits = (steps: ProgressSdkStep[]) =>\n steps.reduce((sum, step) => sum + getApprovalUnitsForStep(step).length, 0);\n\nconst countCompletedApprovalUnitsFromEvents = (\n events: NexusWidgetProgressEvent[],\n) =>\n events.reduce((sum, event) => {\n if (\n event.name !== PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS ||\n !event.completed\n ) {\n return sum;\n }\n return sum + getApprovalUnitsForStep(event.step).length;\n }, 0);\n\nconst countCompletedApprovalUnitsFromSteps = (steps: ProgressStep[]) =>\n steps.reduce(\n (sum, item) =>\n item.completed ? sum + getApprovalUnitsForStep(item.step).length : sum,\n 0,\n );\n\nconst getApprovalUnitSymbols = (steps: ProgressSdkStep[]) =>\n steps\n .flatMap((step) => getApprovalUnitsForStep(step))\n .map((unit) => unit.symbol)\n .filter(Boolean) as string[];\n\nconst getNumericEventIndex = (...values: unknown[]) => {\n for (const value of values) {\n if (typeof value === \"number\" && Number.isInteger(value) && value >= 0) {\n return value;\n }\n if (typeof value === \"string\" && /^\\d+$/.test(value)) {\n return Number(value);\n }\n }\n return undefined;\n};\n\nconst getApprovalIndexFromEvent = (event?: NexusWidgetProgressEvent) => {\n const rawEvent = event?.event as any;\n const rawStep = event?.step as any;\n return getNumericEventIndex(\n rawEvent?.approvalIndex,\n rawEvent?.swapIndex,\n rawEvent?.currentIndex,\n rawEvent?.index,\n rawEvent?.data?.approvalIndex,\n rawEvent?.data?.swapIndex,\n rawEvent?.data?.currentIndex,\n rawEvent?.data?.index,\n rawStep?.approvalIndex,\n rawStep?.swapIndex,\n rawStep?.currentIndex,\n rawStep?.index,\n rawStep?.data?.approvalIndex,\n rawStep?.data?.swapIndex,\n rawStep?.data?.currentIndex,\n rawStep?.data?.index,\n );\n};\n\nconst getActiveApprovalProgressEvent = (\n events: NexusWidgetProgressEvent[],\n completedApprovalCount = 0,\n approvalSymbols: string[] = [],\n) =>\n [...events]\n .reverse()\n .find((event) => {\n if (\n event.name !== PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS ||\n event.completed\n ) {\n return false;\n }\n\n const units = getApprovalUnitsForStep(event.step);\n if (units.length === 0) return false;\n\n const eventIndex = getApprovalIndexFromEvent(event);\n if (eventIndex !== undefined) {\n return eventIndex >= completedApprovalCount;\n }\n\n if (units.length === 1 && units[0]?.symbol) {\n const symbolIndex = approvalSymbols.findIndex(\n (symbol) => symbol === units[0]?.symbol,\n );\n if (symbolIndex >= 0) return symbolIndex >= completedApprovalCount;\n }\n\n return completedApprovalCount === 0;\n });\n\nconst getApprovalSymbolFromProgressEvent = (\n event?: NexusWidgetProgressEvent,\n) => {\n const units = getApprovalUnitsForStep(event?.step);\n if (units.length === 0) return undefined;\n if (units.length === 1) return units[0]?.symbol;\n\n const index = getApprovalIndexFromEvent(event);\n if (index === undefined || index >= units.length) return undefined;\n return units[index]?.symbol;\n};\n\nconst getApprovalTotalFromSwapStepsList = (\n events: NexusWidgetProgressEvent[],\n) =>\n countApprovalUnits(\n getListedSteps(events, PROGRESS_EVENT_NAMES.SWAP_PLAN_LIST),\n );\n\nconst hasStartedStatus = (\n events: NexusWidgetProgressEvent[],\n id: ProgressStatusId,\n mode: NexusWidgetMode,\n hasTransferAction = false,\n) =>\n events.some(\n (event) => getStatusForStep(event.step, mode, hasTransferAction) === id,\n );\n\nconst buildStatusRows = ({\n events,\n failedStep,\n mode,\n steps,\n approvalTotalCount,\n context,\n}: {\n events: NexusWidgetProgressEvent[];\n failedStep?: ProgressSdkStep | null;\n mode: NexusWidgetMode;\n steps: ProgressStep[];\n approvalTotalCount?: number | null;\n context: {\n destinationChain?: string;\n destinationSymbol?: string;\n opportunityName?: string;\n recipientAddress?: string;\n };\n}): ProgressStatusRow[] => {\n const hasTransferAction =\n mode === \"send\" || (mode === \"swap\" && Boolean(context.recipientAddress));\n const failedStatus = failedStep\n ? getStatusForStep(failedStep, mode, hasTransferAction)\n : null;\n const swapListSteps = getListedSteps(\n events,\n PROGRESS_EVENT_NAMES.SWAP_PLAN_LIST,\n );\n const fallbackSteps = steps.map((item) => item.step);\n const destinationSymbol = context.destinationSymbol || \"token\";\n const destinationChain = context.destinationChain || \"destination\";\n const opportunityName = context.opportunityName || \"app\";\n const immutableApprovalTotal =\n approvalTotalCount ??\n Math.max(\n countApprovalUnits(swapListSteps),\n countApprovalUnits(fallbackSteps),\n countCompletedApprovalUnitsFromEvents(events),\n );\n const refundEligibleFailure =\n failedStep !== null &&\n failedStep !== undefined &&\n stepMatches(failedStep, REFUND_ELIGIBLE_SWAP_TYPES);\n const approvalCompletedCount = Math.min(\n immutableApprovalTotal || Number.MAX_SAFE_INTEGER,\n Math.max(\n countCompletedApprovalUnitsFromEvents(events),\n countCompletedApprovalUnitsFromSteps(steps),\n ),\n );\n const approvalSymbols = getApprovalUnitSymbols(\n swapListSteps.length > 0 ? swapListSteps : fallbackSteps,\n );\n const activeApprovalSymbol = getApprovalSymbolFromProgressEvent(\n getActiveApprovalProgressEvent(\n events,\n approvalCompletedCount,\n approvalSymbols,\n ),\n );\n const hasSwapList =\n swapListSteps.length > 0 ||\n hasStepType(events, steps, [\n \"SWAP_START\",\n \"DETERMINING_SWAP\",\n \"SOURCE_SWAP\",\n \"DESTINATION_SWAP\",\n \"BRIDGE_DEPOSIT\",\n \"BRIDGE_FILL\",\n \"BRIDGE_INTENT_SUBMISSION\",\n \"SWAP_COMPLETE\",\n \"SWAP_SKIPPED\",\n ]);\n const hasDestinationReceiveStep =\n countListedSteps(swapListSteps, DESTINATION_SWAP_TYPES) > 0 ||\n countListedSteps(fallbackSteps, DESTINATION_SWAP_TYPES) > 0 ||\n hasProgressEventType(events, DESTINATION_SWAP_TYPES);\n const receiveTokenTypes = hasDestinationReceiveStep\n ? DESTINATION_SWAP_TYPES\n : BRIDGE_FILL_RECEIVE_TYPES;\n const hasReceiveTokenStep =\n hasDestinationReceiveStep ||\n countListedSteps(swapListSteps, receiveTokenTypes) > 0 ||\n countListedSteps(fallbackSteps, receiveTokenTypes) > 0 ||\n hasProgressEventType(events, receiveTokenTypes);\n const receiveTokenStarted = hasProgressEventType(events, receiveTokenTypes);\n const receiveTokenComplete =\n hasReceiveTokenStep && hasCompletedType(events, steps, receiveTokenTypes);\n const swapComplete = hasCompletedType(events, steps, [\n \"SWAP_COMPLETE\",\n \"SWAP_SKIPPED\",\n ]);\n const swapSkipped = hasCompletedType(events, steps, [\"SWAP_SKIPPED\"]);\n const shouldShowSwapRows =\n hasSwapList && !(swapSkipped && (mode === \"deposit\" || mode === \"send\"));\n const swapTokensComplete = hasReceiveTokenStep\n ? receiveTokenStarted\n : swapComplete;\n const transactionSent = hasCompletedType(events, steps, [\"TRANSACTION_SENT\"]);\n const transactionConfirmed = hasCompletedType(events, steps, [\n \"TRANSACTION_CONFIRMED\",\n ]);\n const rows: ProgressStatusRow[] = [];\n\n const pushRow = (row: ProgressStatusRow) => {\n if (failedStatus) {\n const failedIndex = STATUS_ORDER.indexOf(failedStatus);\n const currentIndex = STATUS_ORDER.indexOf(row.id);\n if (failedIndex >= 0 && currentIndex > failedIndex) return;\n }\n rows.push(row);\n };\n\n if (immutableApprovalTotal > 0) {\n const approvalCurrent = Math.min(\n immutableApprovalTotal,\n Math.max(1, approvalCompletedCount + 1),\n );\n const approvalSymbol =\n activeApprovalSymbol ??\n approvalSymbols[\n Math.min(approvalCompletedCount, approvalSymbols.length - 1)\n ];\n const approvalDescription = approvalSymbol\n ? `Approve ${approvalSymbol} in wallet`\n : \"Approve in wallet\";\n let state: ProgressStatusState = \"default\";\n if (failedStatus === \"approveTokens\") {\n state = \"error\";\n } else if (approvalCompletedCount >= immutableApprovalTotal) {\n state = \"completed\";\n } else if (\n hasStartedStatus(events, \"approveTokens\", mode, hasTransferAction) ||\n events.length === 0\n ) {\n state = \"preapproval\";\n }\n\n const approvalSteps = (\n swapListSteps.length > 0 ? swapListSteps : fallbackSteps\n ).filter((step) => stepMatches(step, SWAP_APPROVAL_TYPES));\n const currentApprovalStep = approvalSteps[approvalCompletedCount];\n const activeSymbol = currentApprovalStep\n ? (currentApprovalStep as any).symbol\n : undefined;\n\n pushRow({\n id: \"approveTokens\",\n state,\n description: state === \"preapproval\" ? approvalDescription : undefined,\n label:\n state === \"completed\"\n ? `Approved Swaps (${immutableApprovalTotal} of ${immutableApprovalTotal})`\n : state === \"error\"\n ? \"Collection failed\"\n : `Approve Swaps (${approvalCurrent} of ${immutableApprovalTotal})`,\n });\n }\n\n if (shouldShowSwapRows) {\n const approvalsSatisfied =\n immutableApprovalTotal === 0 ||\n approvalCompletedCount >= immutableApprovalTotal;\n let state: ProgressStatusState = \"default\";\n if (failedStatus === \"swapTokens\") {\n state = \"error\";\n } else if (swapTokensComplete) {\n state = \"completed\";\n } else if (\n approvalsSatisfied &&\n (hasStartedStatus(events, \"swapTokens\", mode, hasTransferAction) ||\n hasEventType(events, [\n \"DESTINATION_SWAP_BATCH_TX\",\n \"BRIDGE_DEPOSIT\",\n \"SOURCE_SWAP_BATCH_TX\",\n \"SOURCE_SWAP_HASH\",\n \"SWAP_START\",\n ]))\n ) {\n state = \"inProgress\";\n }\n\n pushRow({\n id: \"swapTokens\",\n state,\n label:\n state === \"completed\"\n ? \"Swaps completed\"\n : state === \"error\"\n ? refundEligibleFailure\n ? \"Swap failed. Refund initiated\"\n : \"Swap failed\"\n : state === \"inProgress\"\n ? \"Swaps in progress\"\n : \"Swap tokens\",\n });\n\n if (hasReceiveTokenStep) {\n pushRow({\n id: \"receiveToken\",\n state:\n failedStatus === \"receiveToken\"\n ? \"error\"\n : receiveTokenComplete\n ? \"completed\"\n : receiveTokenStarted\n ? \"inProgress\"\n : \"default\",\n label:\n failedStatus === \"receiveToken\"\n ? refundEligibleFailure\n ? \"Destination swap failed. Refund initiated.\"\n : \"Destination swap failed.\"\n : receiveTokenComplete\n ? `Received ${destinationSymbol} on ${destinationChain}`\n : receiveTokenStarted\n ? `Receiving ${destinationSymbol} on ${destinationChain}`\n : `Receive ${destinationSymbol} on ${destinationChain}`,\n });\n }\n }\n\n if (mode === \"deposit\" || hasTransferAction) {\n const receiveComplete =\n swapSkipped ||\n !shouldShowSwapRows ||\n (hasReceiveTokenStep ? receiveTokenComplete : swapTokensComplete);\n const isDeposit = mode === \"deposit\";\n let state: ProgressStatusState = \"default\";\n if (failedStatus === \"action\") {\n state = \"error\";\n } else if (transactionConfirmed) {\n state = \"completed\";\n } else if (transactionSent) {\n state = \"inProgress\";\n } else if (receiveComplete) {\n state = \"preapproval\";\n }\n\n pushRow({\n id: \"action\",\n state,\n description: state === \"preapproval\" ? \"Approve in wallet\" : undefined,\n label: isDeposit\n ? state === \"completed\"\n ? `${destinationSymbol} deposited to ${opportunityName}`\n : state === \"inProgress\"\n ? `Depositing ${destinationSymbol} to ${opportunityName}`\n : state === \"error\"\n ? \"Deposit failed. Funds are in your wallet.\"\n : state === \"preapproval\"\n ? `Approve Deposit of ${destinationSymbol} to ${opportunityName}`\n : `Deposit ${destinationSymbol} to ${opportunityName}`\n : state === \"completed\"\n ? `${destinationSymbol} sent`\n : state === \"inProgress\"\n ? `Sending ${destinationSymbol}`\n : state === \"error\"\n ? \"Send failed. Funds are in your wallet.\"\n : state === \"preapproval\"\n ? `Approve ${destinationSymbol} transfer`\n : `Send ${destinationSymbol}`,\n });\n }\n\n const orderedRows = rows.sort(\n (a, b) => STATUS_ORDER.indexOf(a.id) - STATUS_ORDER.indexOf(b.id),\n );\n\n if (\n orderedRows.some(\n (row) => row.state === \"preapproval\" || row.state === \"inProgress\",\n ) ||\n orderedRows.some((row) => row.state === \"error\")\n ) {\n return orderedRows;\n }\n\n const nextActiveIndex = orderedRows.findIndex(\n (row) => row.state === \"default\",\n );\n if (nextActiveIndex === -1) return orderedRows;\n\n return orderedRows.map((row, index) => {\n if (index !== nextActiveIndex) return row;\n const nextState =\n row.id === \"approveTokens\" || row.id === \"action\"\n ? \"preapproval\"\n : \"inProgress\";\n return {\n ...row,\n description:\n nextState === \"preapproval\" ? \"Approve in wallet\" : undefined,\n label:\n row.id === \"swapTokens\"\n ? \"Swaps in progress\"\n : row.id === \"receiveToken\"\n ? `Receiving ${destinationSymbol} on ${destinationChain}`\n : row.label,\n state: nextState,\n };\n });\n};\n\nfunction MiniLogo({\n src,\n label,\n size,\n style,\n}: {\n src?: string;\n label?: string;\n size: number;\n style?: React.CSSProperties;\n}) {\n const [failed, setFailed] = useState(!src);\n\n useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n background: \"#FFFFFE\",\n borderRadius: \"999px\",\n height: size,\n objectFit: \"cover\",\n width: size,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {(label || \"?\").trim().slice(0, 1).toUpperCase()}\n \n );\n}\n\nfunction TokenLogoPair({\n tokenLogo,\n chainLogo,\n tokenSymbol,\n chainName,\n}: {\n tokenLogo?: string;\n chainLogo?: string;\n tokenSymbol?: string;\n chainName?: string;\n}) {\n return (\n \n \n {chainLogo && (\n \n )}\n \n );\n}\n\nexport function NexusWidgetProgressScreen({\n fromTokens = [],\n toToken,\n fromAmountUsd,\n toAmount,\n toAmountUsd,\n totalFeeUsd,\n intentData,\n mode,\n opportunity,\n steps,\n progressEvents = [],\n failedStep,\n recipientAddress,\n}: NexusWidgetProgressScreenProps) {\n const intentSources = intentData?.sources ?? [];\n const intentDestination = intentData?.destination;\n const destinationSourceToken = fromTokens.find((token) => {\n const destinationChainId = intentDestination?.chain.id ?? toToken?.chainId;\n const destinationTokenAddress = (\n intentDestination?.token.contractAddress ??\n toToken?.contractAddress ??\n \"\"\n ).toLowerCase();\n const tokenAmount =\n parseDecimal(token.userAmount) ?? parseDecimal(token.balance);\n\n return (\n destinationChainId !== undefined &&\n destinationTokenAddress !== \"\" &&\n token.chainId === destinationChainId &&\n token.contractAddress.toLowerCase() === destinationTokenAddress &&\n Boolean(tokenAmount && tokenAmount.gt(0))\n );\n });\n const sourceSymbols = unique([\n ...(destinationSourceToken ? [destinationSourceToken.symbol] : []),\n ...(intentSources.length > 0\n ? intentSources.map((source) => source.token.symbol)\n : fromTokens.map((token) => token.symbol)),\n ]);\n const intentSourceUsd =\n intentSources.length > 0\n ? intentSources.reduce(\n (sum, source) => sum.plus(parseDecimal(source.value) ?? 0),\n new Decimal(0),\n )\n : parseDecimal(fromAmountUsd);\n const requestedDestinationAmount = parseDecimal(toAmount);\n const quotedDestinationAmount = parseDecimal(intentDestination?.amount);\n const destinationBalanceAmount = parseDecimal(toToken?.balance);\n const requestedDestinationUsd = parseDecimal(toAmountUsd);\n const destinationUsdRate =\n requestedDestinationAmount &&\n requestedDestinationAmount.gt(0) &&\n requestedDestinationUsd &&\n requestedDestinationUsd.gt(0)\n ? requestedDestinationUsd.div(requestedDestinationAmount)\n : quotedDestinationAmount &&\n quotedDestinationAmount.gt(0) &&\n intentDestination?.value\n ? (parseDecimal(intentDestination.value) ?? new Decimal(0)).div(\n quotedDestinationAmount,\n )\n : undefined;\n const destinationCoverageUsd =\n (mode === \"deposit\" || mode === \"send\") &&\n requestedDestinationAmount &&\n requestedDestinationAmount.gt(0) &&\n quotedDestinationAmount &&\n requestedDestinationAmount.gt(quotedDestinationAmount) &&\n destinationBalanceAmount &&\n destinationBalanceAmount.gt(0) &&\n destinationUsdRate &&\n destinationUsdRate.gt(0)\n ? Decimal.min(\n requestedDestinationAmount.minus(quotedDestinationAmount),\n destinationBalanceAmount,\n ).mul(destinationUsdRate)\n : undefined;\n const quotedDestinationUsd = parseDecimal(intentDestination?.value);\n const feeUsd = parseDecimal(totalFeeUsd);\n const sourceUsd =\n mode === \"deposit\" || mode === \"send\"\n ? [\n destinationCoverageUsd !== undefined\n ? (intentSourceUsd ?? new Decimal(0)).plus(destinationCoverageUsd)\n : intentSourceUsd,\n requestedDestinationUsd,\n requestedDestinationUsd &&\n requestedDestinationUsd.gt(0) &&\n intentSourceUsd &&\n intentSourceUsd.gt(0) &&\n quotedDestinationUsd &&\n quotedDestinationUsd.gt(0)\n ? requestedDestinationUsd.plus(\n Decimal.max(intentSourceUsd.minus(quotedDestinationUsd), 0),\n )\n : undefined,\n requestedDestinationUsd &&\n requestedDestinationUsd.gt(0) &&\n feeUsd &&\n feeUsd.gt(0)\n ? requestedDestinationUsd.plus(feeUsd)\n : undefined,\n ]\n .filter((value): value is Decimal => Boolean(value && value.gt(0)))\n .reduce(\n (max, value) => (!max || value.gt(max) ? value : max),\n undefined,\n )\n : intentSourceUsd;\n const destinationAmount =\n (mode === \"deposit\" || mode === \"send\") && toAmount\n ? toAmount\n : (intentDestination?.amount ?? toAmount ?? \"0\");\n const destinationSymbol =\n intentDestination?.token.symbol ||\n toToken?.symbol ||\n opportunity?.tokenSymbol ||\n \"\";\n const destinationChainName = getShortChainName(\n intentDestination?.chain.id ?? toToken?.chainId,\n intentDestination?.chain.name || toToken?.chainName || \"\",\n );\n const destinationChain =\n mode === \"deposit\"\n ? opportunity?.title || opportunity?.protocol || destinationChainName\n : destinationChainName;\n const seededApprovalTotal = countApprovalUnits(\n (steps ?? []).map((item) => item.step),\n );\n const completedApprovalEventTotal =\n countCompletedApprovalUnitsFromEvents(progressEvents);\n const computedApprovalTotal = Math.max(\n getApprovalTotalFromSwapStepsList(progressEvents),\n seededApprovalTotal,\n completedApprovalEventTotal,\n );\n const [lockedApprovalTotal, setLockedApprovalTotal] = useState(\n null,\n );\n const approvalTotalCount =\n lockedApprovalTotal ??\n (computedApprovalTotal > 0 ? computedApprovalTotal : null);\n\n useEffect(() => {\n if (progressEvents.length === 0) {\n setLockedApprovalTotal(null);\n return;\n }\n if (lockedApprovalTotal !== null || computedApprovalTotal <= 0) return;\n setLockedApprovalTotal(computedApprovalTotal);\n }, [computedApprovalTotal, lockedApprovalTotal, progressEvents.length]);\n\n const statusRows = buildStatusRows({\n events: progressEvents,\n failedStep,\n mode,\n steps: steps ?? [],\n approvalTotalCount,\n context: {\n destinationChain: destinationChainName || destinationChain,\n destinationSymbol,\n opportunityName: opportunity?.title || opportunity?.protocol,\n recipientAddress,\n },\n });\n const [stepsExpanded, setStepsExpanded] = useState(true);\n const activeRow =\n statusRows.find(\n (row) => row.state === \"preapproval\" || row.state === \"inProgress\",\n ) ??\n statusRows.find((row) => row.state === \"error\") ??\n statusRows.find((row) => row.state === \"default\") ??\n statusRows[statusRows.length - 1];\n const visibleRows = stepsExpanded ? statusRows : activeRow ? [activeRow] : [];\n const canExpand = statusRows.length > 1;\n const getRowHeight = (row: ProgressStatusRow) => (row.description ? 47 : 40);\n const collapsedStatusHeight = activeRow ? getRowHeight(activeRow) : 40;\n const expandedStatusHeight = statusRows.reduce(\n (sum, row) => sum + getRowHeight(row),\n 0,\n );\n\n return (\n \n \n \n {formatSymbolSummary(sourceSymbols)}\n \n \n {formatUsd(sourceUsd)}\n \n\n \n\n \n \n \n {formatDecimal(destinationAmount, 8)}\n \n {destinationSymbol}\n \n \n {destinationChain && (\n \n on {destinationChain}\n \n )}\n \n \n\n \n \n \n {visibleRows.map((row, index) => {\n const isCompleted = row.state === \"completed\";\n const isError = row.state === \"error\";\n const isDefault = row.state === \"default\";\n const isLoading =\n row.state === \"preapproval\" || row.state === \"inProgress\";\n const hasDescription = Boolean(row.description);\n const rowColor = isDefault ? muted : isError ? danger : primary;\n\n return (\n {\n if (canExpand) setStepsExpanded((current) => !current);\n }}\n style={{\n alignItems: hasDescription ? \"flex-start\" : \"center\",\n appearance: \"none\",\n background: \"transparent\",\n border: \"0\",\n borderTop:\n index > 0 && stepsExpanded ? `1px solid ${border}` : \"0\",\n boxSizing: \"border-box\",\n color: rowColor,\n cursor: canExpand ? \"pointer\" : \"default\",\n display: \"flex\",\n fontFamily,\n fontSize: \"11px\",\n fontWeight: 400,\n gap: \"7px\",\n minHeight: `${getRowHeight(row)}px`,\n padding: \"8px 11px\",\n textAlign: \"left\",\n transition:\n \"color 220ms ease, min-height 220ms ease, opacity 220ms ease\",\n width: \"100%\",\n }}\n type=\"button\"\n >\n {isCompleted || isError ? (\n \n {isError ? (\n \n ) : (\n \n )}\n \n ) : isDefault ? (\n \n ) : (\n \n )}\n \n \n {row.label}\n \n {row.description && (\n \n {row.description}\n \n )}\n \n {canExpand && index === 0 && (\n \n )}\n \n );\n })}\n \n \n \n \n );\n}\n", + "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\n\"use client\";\n\nimport Decimal from \"decimal.js\";\nimport { Check, ChevronDown, Loader2, X } from \"lucide-react\";\nimport React, { useEffect, useState } from \"react\";\nimport type {\n BridgeStepType,\n SwapStepType,\n} from \"../../common/types/transaction-flow\";\nimport { getShortChainName } from \"../../common/utils/constant\";\nimport {\n type NexusWidgetDepositOpportunityMetadata,\n type NexusWidgetMode,\n} from \"../types\";\nimport { NEXUS_WIDGET_FAST_SPINNER_STYLE } from \"../theme\";\nimport { type SwapTokenOption } from \"./swap-asset-selector\";\nimport { type SwapIntentData } from \"./swap-intent-preview\";\n\ntype ProgressSdkStep = SwapStepType | BridgeStepType;\n\ntype ProgressStep = {\n id: number;\n completed: boolean;\n step: ProgressSdkStep;\n};\n\nexport type NexusWidgetProgressEvent = {\n id: string;\n name: string;\n completed: boolean;\n event?: unknown;\n step?: ProgressSdkStep;\n steps?: ProgressSdkStep[];\n};\n\ninterface NexusWidgetProgressScreenProps {\n failedStep?: ProgressSdkStep | null;\n fromAmountUsd?: string;\n fromTokens?: SwapTokenOption[];\n intentData?: SwapIntentData | null;\n mode: NexusWidgetMode;\n opportunity?: NexusWidgetDepositOpportunityMetadata;\n progressEvents?: NexusWidgetProgressEvent[];\n recipientAddress?: string;\n steps?: ProgressStep[];\n toAmount?: string;\n toAmountUsd?: string;\n toToken?: SwapTokenOption;\n totalFeeUsd?: string;\n}\n\nconst fontFamily = '\"Geist\", var(--font-geist-sans), system-ui, sans-serif';\nconst primary = \"var(--foreground-primary, #161615)\";\nconst muted = \"var(--foreground-muted, #848483)\";\nconst border = \"var(--border-default, #E8E8E7)\";\nconst brand = \"var(--foreground-brand)\";\nconst danger = \"var(--foreground-negative, #E92C2C)\";\n\nconst parseDecimal = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = String(value).replace(/[^0-9.-]/g, \"\");\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst formatDecimal = (value: unknown, decimals = 2) =>\n (parseDecimal(value) ?? new Decimal(0)).toDecimalPlaces(decimals).toFixed();\n\nconst formatUsd = (value: unknown) => `$${formatDecimal(value, 2)}`;\n\nconst unique = (values: Array) =>\n Array.from(new Set(values.filter(Boolean) as string[]));\n\nconst formatSymbolSummary = (symbols: string[]) => {\n if (symbols.length <= 2) return symbols.join(\", \");\n return `${symbols.slice(0, 2).join(\", \")} and ${symbols.length - 2} others`;\n};\n\nconst getStepType = (step?: ProgressSdkStep) =>\n String((step as any)?.type ?? (step as any)?.typeID ?? \"\").toUpperCase();\n\ntype ProgressStatusId =\n | \"approveTokens\"\n | \"swapTokens\"\n | \"receiveToken\"\n | \"action\";\n\ntype ProgressStatusState =\n | \"default\"\n | \"preapproval\"\n | \"inProgress\"\n | \"completed\"\n | \"error\";\n\ntype ProgressStatusRow = {\n id: ProgressStatusId;\n label: string;\n description?: string;\n state: ProgressStatusState;\n};\n\nconst PROGRESS_EVENT_NAMES = {\n BRIDGE_PLAN_LIST: \"bridge_plan_list\",\n BRIDGE_PLAN_PROGRESS: \"bridge_plan_progress\",\n SWAP_PLAN_LIST: \"swap_plan_list\",\n SWAP_PLAN_PROGRESS: \"swap_plan_progress\",\n} as const;\n\ntype ProgressListEventName =\n | typeof PROGRESS_EVENT_NAMES.BRIDGE_PLAN_LIST\n | typeof PROGRESS_EVENT_NAMES.SWAP_PLAN_LIST;\n\nconst STATUS_ORDER: ProgressStatusId[] = [\n \"approveTokens\",\n \"swapTokens\",\n \"receiveToken\",\n \"action\",\n];\n\nconst SWAP_APPROVAL_TYPES = [\n \"SOURCE_SWAP\",\n \"CREATE_PERMIT_EOA_TO_EPHEMERAL\",\n \"CREATE_PERMIT_FOR_SOURCE_SWAP\",\n \"EOA_TO_EPHEMERAL_TRANSFER\",\n \"EOA_EXECUTE_CALL\",\n // \"BRIDGE_DEPOSIT\",\n];\n\nconst REFUND_ELIGIBLE_SWAP_TYPES = [\n \"BRIDGE_INTENT_SUBMISSION\",\n \"BRIDGE_DEPOSIT\",\n];\n\nconst DESTINATION_SWAP_TYPES = [\n \"DESTINATION_SWAP\",\n \"DESTINATION_SWAP_BATCH_TX\",\n \"DESTINATION_SWAP_HASH\",\n];\nconst BRIDGE_FILL_RECEIVE_TYPES = [\"BRIDGE_FILL\"];\n\nconst getStatusForStep = (\n step: ProgressSdkStep | undefined,\n mode: NexusWidgetMode,\n hasTransferAction = false,\n): ProgressStatusId | null => {\n const type = getStepType(step);\n\n if (\n type === \"APPROVAL\" ||\n type === \"TRANSACTION_SENT\" ||\n type === \"TRANSACTION_CONFIRMED\"\n ) {\n return mode === \"swap\" && !hasTransferAction ? null : \"action\";\n }\n\n if (type.includes(\"SWAP_START\")) {\n return \"swapTokens\";\n }\n\n if (SWAP_APPROVAL_TYPES.some((token) => type.includes(token))) {\n return \"approveTokens\";\n }\n\n if (\n type.includes(\"SOURCE_SWAP\") ||\n type.includes(\"SOURCE_BATCH\") ||\n type.includes(\"SWAP_SOURCE\") ||\n type.includes(\"BRIDGE_DEPOSIT\") ||\n type.includes(\"BRIDGE_FILL\") ||\n type.includes(\"BRIDGE_INTENT_SUBMISSION\") ||\n type.includes(\"SWAP_COMPLETE\") ||\n type.includes(\"SWAP_SKIPPED\")\n ) {\n return \"swapTokens\";\n }\n\n if (type.includes(\"DESTINATION_SWAP\") || type.includes(\"DESTINATION_BATCH\")) {\n return \"receiveToken\";\n }\n\n return null;\n};\n\nconst stepMatches = (step: ProgressSdkStep | undefined, tokens: string[]) => {\n const type = getStepType(step);\n return tokens.some((token) => type.includes(token));\n};\n\nconst hasCompletedType = (\n events: NexusWidgetProgressEvent[],\n steps: ProgressStep[],\n tokens: string[],\n) => {\n const completedEvent = events.some(\n (event) => event.completed && stepMatches(event.step, tokens),\n );\n if (completedEvent) return true;\n\n return steps.some((item) => item.completed && stepMatches(item.step, tokens));\n};\n\nconst hasStepType = (\n events: NexusWidgetProgressEvent[],\n steps: ProgressStep[],\n tokens: string[],\n) =>\n events.some(\n (event) =>\n stepMatches(event.step, tokens) ||\n (event.steps ?? []).some((step) => stepMatches(step, tokens)),\n ) || steps.some((item) => stepMatches(item.step, tokens));\n\nconst hasEventType = (events: NexusWidgetProgressEvent[], tokens: string[]) =>\n events.some(\n (event) =>\n stepMatches(event.step, tokens) ||\n (event.steps ?? []).some((step) => stepMatches(step, tokens)),\n );\n\nconst hasProgressEventType = (\n events: NexusWidgetProgressEvent[],\n tokens: string[],\n) =>\n events.some(\n (event) =>\n (event.name === PROGRESS_EVENT_NAMES.BRIDGE_PLAN_PROGRESS ||\n event.name === PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS) &&\n stepMatches(event.step, tokens),\n );\n\nconst getListedSteps = (\n events: NexusWidgetProgressEvent[],\n eventName: ProgressListEventName,\n) => {\n const listEvent = [...events]\n .reverse()\n .find(\n (event) => event.name === eventName && (event.steps?.length ?? 0) > 0,\n );\n return listEvent?.steps ?? [];\n};\n\nconst countListedSteps = (steps: ProgressSdkStep[], tokens: string[]) =>\n steps.filter((step) => stepMatches(step, tokens)).length;\n\ntype ApprovalUnit = {\n symbol?: string;\n};\n\nconst getStepSwaps = (step?: ProgressSdkStep) => {\n const swaps = (step as any)?.swaps;\n return Array.isArray(swaps) ? swaps : [];\n};\n\nconst getApprovalUnitsForStep = (step?: ProgressSdkStep): ApprovalUnit[] => {\n if (!stepMatches(step, SWAP_APPROVAL_TYPES)) return [];\n\n const swaps = getStepSwaps(step);\n if (swaps.length > 0) {\n return swaps.map((swap) => ({\n symbol:\n typeof swap?.input?.symbol === \"string\" ? swap.input.symbol : undefined,\n }));\n }\n\n const assetSymbol = (step as any)?.asset?.symbol;\n const tokenSymbol = (step as any)?.token?.symbol;\n return [\n {\n symbol:\n typeof assetSymbol === \"string\"\n ? assetSymbol\n : typeof tokenSymbol === \"string\"\n ? tokenSymbol\n : undefined,\n },\n ];\n};\n\nconst countApprovalUnits = (steps: ProgressSdkStep[]) =>\n steps.reduce((sum, step) => sum + getApprovalUnitsForStep(step).length, 0);\n\nconst countCompletedApprovalUnitsFromEvents = (\n events: NexusWidgetProgressEvent[],\n) =>\n events.reduce((sum, event) => {\n if (\n event.name !== PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS ||\n !event.completed\n ) {\n return sum;\n }\n return sum + getApprovalUnitsForStep(event.step).length;\n }, 0);\n\nconst countCompletedApprovalUnitsFromSteps = (steps: ProgressStep[]) =>\n steps.reduce(\n (sum, item) =>\n item.completed ? sum + getApprovalUnitsForStep(item.step).length : sum,\n 0,\n );\n\nconst getApprovalUnitSymbols = (steps: ProgressSdkStep[]) =>\n steps\n .flatMap((step) => getApprovalUnitsForStep(step))\n .map((unit) => unit.symbol)\n .filter(Boolean) as string[];\n\nconst getNumericEventIndex = (...values: unknown[]) => {\n for (const value of values) {\n if (typeof value === \"number\" && Number.isInteger(value) && value >= 0) {\n return value;\n }\n if (typeof value === \"string\" && /^\\d+$/.test(value)) {\n return Number(value);\n }\n }\n return undefined;\n};\n\nconst getApprovalIndexFromEvent = (event?: NexusWidgetProgressEvent) => {\n const rawEvent = event?.event as any;\n const rawStep = event?.step as any;\n return getNumericEventIndex(\n rawEvent?.approvalIndex,\n rawEvent?.swapIndex,\n rawEvent?.currentIndex,\n rawEvent?.index,\n rawEvent?.data?.approvalIndex,\n rawEvent?.data?.swapIndex,\n rawEvent?.data?.currentIndex,\n rawEvent?.data?.index,\n rawStep?.approvalIndex,\n rawStep?.swapIndex,\n rawStep?.currentIndex,\n rawStep?.index,\n rawStep?.data?.approvalIndex,\n rawStep?.data?.swapIndex,\n rawStep?.data?.currentIndex,\n rawStep?.data?.index,\n );\n};\n\nconst getActiveApprovalProgressEvent = (\n events: NexusWidgetProgressEvent[],\n completedApprovalCount = 0,\n approvalSymbols: string[] = [],\n) =>\n [...events]\n .reverse()\n .find((event) => {\n if (\n event.name !== PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS ||\n event.completed\n ) {\n return false;\n }\n\n const units = getApprovalUnitsForStep(event.step);\n if (units.length === 0) return false;\n\n const eventIndex = getApprovalIndexFromEvent(event);\n if (eventIndex !== undefined) {\n return eventIndex >= completedApprovalCount;\n }\n\n if (units.length === 1 && units[0]?.symbol) {\n const symbolIndex = approvalSymbols.findIndex(\n (symbol) => symbol === units[0]?.symbol,\n );\n if (symbolIndex >= 0) return symbolIndex >= completedApprovalCount;\n }\n\n return completedApprovalCount === 0;\n });\n\nconst getApprovalSymbolFromProgressEvent = (\n event?: NexusWidgetProgressEvent,\n) => {\n const units = getApprovalUnitsForStep(event?.step);\n if (units.length === 0) return undefined;\n if (units.length === 1) return units[0]?.symbol;\n\n const index = getApprovalIndexFromEvent(event);\n if (index === undefined || index >= units.length) return undefined;\n return units[index]?.symbol;\n};\n\nconst getApprovalTotalFromSwapStepsList = (\n events: NexusWidgetProgressEvent[],\n) =>\n countApprovalUnits(\n getListedSteps(events, PROGRESS_EVENT_NAMES.SWAP_PLAN_LIST),\n );\n\nconst hasStartedStatus = (\n events: NexusWidgetProgressEvent[],\n id: ProgressStatusId,\n mode: NexusWidgetMode,\n hasTransferAction = false,\n) =>\n events.some(\n (event) => getStatusForStep(event.step, mode, hasTransferAction) === id,\n );\n\nconst buildStatusRows = ({\n events,\n failedStep,\n mode,\n steps,\n approvalTotalCount,\n context,\n}: {\n events: NexusWidgetProgressEvent[];\n failedStep?: ProgressSdkStep | null;\n mode: NexusWidgetMode;\n steps: ProgressStep[];\n approvalTotalCount?: number | null;\n context: {\n destinationChain?: string;\n destinationSymbol?: string;\n opportunityName?: string;\n recipientAddress?: string;\n };\n}): ProgressStatusRow[] => {\n const hasTransferAction =\n mode === \"send\" || (mode === \"swap\" && Boolean(context.recipientAddress));\n const failedStatus = failedStep\n ? getStatusForStep(failedStep, mode, hasTransferAction)\n : null;\n const swapListSteps = getListedSteps(\n events,\n PROGRESS_EVENT_NAMES.SWAP_PLAN_LIST,\n );\n const fallbackSteps = steps.map((item) => item.step);\n const destinationSymbol = context.destinationSymbol || \"token\";\n const destinationChain = context.destinationChain || \"destination\";\n const opportunityName = context.opportunityName || \"app\";\n const immutableApprovalTotal =\n approvalTotalCount ??\n Math.max(\n countApprovalUnits(swapListSteps),\n countApprovalUnits(fallbackSteps),\n countCompletedApprovalUnitsFromEvents(events),\n );\n const refundEligibleFailure =\n failedStep !== null &&\n failedStep !== undefined &&\n stepMatches(failedStep, REFUND_ELIGIBLE_SWAP_TYPES);\n const approvalCompletedCount = Math.min(\n immutableApprovalTotal || Number.MAX_SAFE_INTEGER,\n Math.max(\n countCompletedApprovalUnitsFromEvents(events),\n countCompletedApprovalUnitsFromSteps(steps),\n ),\n );\n const approvalSymbols = getApprovalUnitSymbols(\n swapListSteps.length > 0 ? swapListSteps : fallbackSteps,\n );\n const activeApprovalSymbol = getApprovalSymbolFromProgressEvent(\n getActiveApprovalProgressEvent(\n events,\n approvalCompletedCount,\n approvalSymbols,\n ),\n );\n const hasSwapList =\n swapListSteps.length > 0 ||\n hasStepType(events, steps, [\n \"SWAP_START\",\n \"DETERMINING_SWAP\",\n \"SOURCE_SWAP\",\n \"DESTINATION_SWAP\",\n \"BRIDGE_DEPOSIT\",\n \"BRIDGE_FILL\",\n \"BRIDGE_INTENT_SUBMISSION\",\n \"SWAP_COMPLETE\",\n \"SWAP_SKIPPED\",\n ]);\n const hasDestinationReceiveStep =\n countListedSteps(swapListSteps, DESTINATION_SWAP_TYPES) > 0 ||\n countListedSteps(fallbackSteps, DESTINATION_SWAP_TYPES) > 0 ||\n hasProgressEventType(events, DESTINATION_SWAP_TYPES);\n const receiveTokenTypes = hasDestinationReceiveStep\n ? DESTINATION_SWAP_TYPES\n : BRIDGE_FILL_RECEIVE_TYPES;\n const hasReceiveTokenStep =\n hasDestinationReceiveStep ||\n countListedSteps(swapListSteps, receiveTokenTypes) > 0 ||\n countListedSteps(fallbackSteps, receiveTokenTypes) > 0 ||\n hasProgressEventType(events, receiveTokenTypes);\n const receiveTokenStarted = hasProgressEventType(events, receiveTokenTypes);\n const receiveTokenComplete =\n hasReceiveTokenStep && hasCompletedType(events, steps, receiveTokenTypes);\n const swapComplete = hasCompletedType(events, steps, [\n \"SWAP_COMPLETE\",\n \"SWAP_SKIPPED\",\n ]);\n const swapSkipped = hasCompletedType(events, steps, [\"SWAP_SKIPPED\"]);\n const shouldShowSwapRows =\n hasSwapList && !(swapSkipped && (mode === \"deposit\" || mode === \"send\"));\n const swapTokensComplete = hasReceiveTokenStep\n ? receiveTokenStarted\n : swapComplete;\n const transactionSent = hasCompletedType(events, steps, [\"TRANSACTION_SENT\"]);\n const transactionConfirmed = hasCompletedType(events, steps, [\n \"TRANSACTION_CONFIRMED\",\n ]);\n const rows: ProgressStatusRow[] = [];\n\n const pushRow = (row: ProgressStatusRow) => {\n if (failedStatus) {\n const failedIndex = STATUS_ORDER.indexOf(failedStatus);\n const currentIndex = STATUS_ORDER.indexOf(row.id);\n if (failedIndex >= 0 && currentIndex > failedIndex) return;\n }\n rows.push(row);\n };\n\n if (immutableApprovalTotal > 0) {\n const approvalCurrent = Math.min(\n immutableApprovalTotal,\n Math.max(1, approvalCompletedCount + 1),\n );\n const approvalSymbol =\n activeApprovalSymbol ??\n approvalSymbols[\n Math.min(approvalCompletedCount, approvalSymbols.length - 1)\n ];\n const approvalDescription = approvalSymbol\n ? `Approve ${approvalSymbol} in wallet`\n : \"Approve in wallet\";\n let state: ProgressStatusState = \"default\";\n if (failedStatus === \"approveTokens\") {\n state = \"error\";\n } else if (approvalCompletedCount >= immutableApprovalTotal) {\n state = \"completed\";\n } else if (\n hasStartedStatus(events, \"approveTokens\", mode, hasTransferAction) ||\n events.length === 0\n ) {\n state = \"preapproval\";\n }\n\n const approvalSteps = (\n swapListSteps.length > 0 ? swapListSteps : fallbackSteps\n ).filter((step) => stepMatches(step, SWAP_APPROVAL_TYPES));\n const currentApprovalStep = approvalSteps[approvalCompletedCount];\n const activeSymbol = currentApprovalStep\n ? (currentApprovalStep as any).symbol\n : undefined;\n\n pushRow({\n id: \"approveTokens\",\n state,\n description: state === \"preapproval\" ? approvalDescription : undefined,\n label:\n state === \"completed\"\n ? `Approved Swaps (${immutableApprovalTotal} of ${immutableApprovalTotal})`\n : state === \"error\"\n ? \"Collection failed\"\n : `Approve Swaps (${approvalCurrent} of ${immutableApprovalTotal})`,\n });\n }\n\n if (shouldShowSwapRows) {\n const approvalsSatisfied =\n immutableApprovalTotal === 0 ||\n approvalCompletedCount >= immutableApprovalTotal;\n let state: ProgressStatusState = \"default\";\n if (failedStatus === \"swapTokens\") {\n state = \"error\";\n } else if (swapTokensComplete) {\n state = \"completed\";\n } else if (\n approvalsSatisfied &&\n (hasStartedStatus(events, \"swapTokens\", mode, hasTransferAction) ||\n hasEventType(events, [\n \"DESTINATION_SWAP_BATCH_TX\",\n \"BRIDGE_DEPOSIT\",\n \"SOURCE_SWAP_BATCH_TX\",\n \"SOURCE_SWAP_HASH\",\n \"SWAP_START\",\n ]))\n ) {\n state = \"inProgress\";\n }\n\n pushRow({\n id: \"swapTokens\",\n state,\n label:\n state === \"completed\"\n ? \"Swaps completed\"\n : state === \"error\"\n ? refundEligibleFailure\n ? \"Swap failed. Refund initiated\"\n : \"Swap failed\"\n : state === \"inProgress\"\n ? \"Swaps in progress\"\n : \"Swap tokens\",\n });\n\n if (hasReceiveTokenStep) {\n pushRow({\n id: \"receiveToken\",\n state:\n failedStatus === \"receiveToken\"\n ? \"error\"\n : receiveTokenComplete\n ? \"completed\"\n : receiveTokenStarted\n ? \"inProgress\"\n : \"default\",\n label:\n failedStatus === \"receiveToken\"\n ? refundEligibleFailure\n ? \"Destination swap failed. Refund initiated.\"\n : \"Destination swap failed.\"\n : receiveTokenComplete\n ? `Received ${destinationSymbol} on ${destinationChain}`\n : receiveTokenStarted\n ? `Receiving ${destinationSymbol} on ${destinationChain}`\n : `Receive ${destinationSymbol} on ${destinationChain}`,\n });\n }\n }\n\n if (mode === \"deposit\" || hasTransferAction) {\n const receiveComplete =\n swapSkipped ||\n !shouldShowSwapRows ||\n (hasReceiveTokenStep ? receiveTokenComplete : swapTokensComplete);\n const isDeposit = mode === \"deposit\";\n let state: ProgressStatusState = \"default\";\n if (failedStatus === \"action\") {\n state = \"error\";\n } else if (transactionConfirmed) {\n state = \"completed\";\n } else if (transactionSent) {\n state = \"inProgress\";\n } else if (receiveComplete) {\n state = \"preapproval\";\n }\n\n pushRow({\n id: \"action\",\n state,\n description: state === \"preapproval\" ? \"Approve in wallet\" : undefined,\n label: isDeposit\n ? state === \"completed\"\n ? `${destinationSymbol} deposited to ${opportunityName}`\n : state === \"inProgress\"\n ? `Depositing ${destinationSymbol} to ${opportunityName}`\n : state === \"error\"\n ? \"Deposit failed. Funds are in your wallet.\"\n : state === \"preapproval\"\n ? `Approve Deposit of ${destinationSymbol} to ${opportunityName}`\n : `Deposit ${destinationSymbol} to ${opportunityName}`\n : state === \"completed\"\n ? `${destinationSymbol} sent`\n : state === \"inProgress\"\n ? `Sending ${destinationSymbol}`\n : state === \"error\"\n ? \"Send failed. Funds are in your wallet.\"\n : state === \"preapproval\"\n ? `Approve ${destinationSymbol} transfer`\n : `Send ${destinationSymbol}`,\n });\n }\n\n const orderedRows = rows.sort(\n (a, b) => STATUS_ORDER.indexOf(a.id) - STATUS_ORDER.indexOf(b.id),\n );\n\n if (\n orderedRows.some(\n (row) => row.state === \"preapproval\" || row.state === \"inProgress\",\n ) ||\n orderedRows.some((row) => row.state === \"error\")\n ) {\n return orderedRows;\n }\n\n const nextActiveIndex = orderedRows.findIndex(\n (row) => row.state === \"default\",\n );\n if (nextActiveIndex === -1) return orderedRows;\n\n return orderedRows.map((row, index) => {\n if (index !== nextActiveIndex) return row;\n const nextState =\n row.id === \"approveTokens\" || row.id === \"action\"\n ? \"preapproval\"\n : \"inProgress\";\n return {\n ...row,\n description:\n nextState === \"preapproval\" ? \"Approve in wallet\" : undefined,\n label:\n row.id === \"swapTokens\"\n ? \"Swaps in progress\"\n : row.id === \"receiveToken\"\n ? `Receiving ${destinationSymbol} on ${destinationChain}`\n : row.label,\n state: nextState,\n };\n });\n};\n\nfunction MiniLogo({\n src,\n label,\n size,\n style,\n}: {\n src?: string;\n label?: string;\n size: number;\n style?: React.CSSProperties;\n}) {\n const [failed, setFailed] = useState(!src);\n\n useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n background: \"#FFFFFE\",\n borderRadius: \"999px\",\n height: size,\n objectFit: \"cover\",\n width: size,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {(label || \"?\").trim().slice(0, 1).toUpperCase()}\n \n );\n}\n\nfunction TokenLogoPair({\n tokenLogo,\n chainLogo,\n tokenSymbol,\n chainName,\n}: {\n tokenLogo?: string;\n chainLogo?: string;\n tokenSymbol?: string;\n chainName?: string;\n}) {\n return (\n \n \n {chainLogo && (\n \n )}\n \n );\n}\n\nexport function NexusWidgetProgressScreen({\n fromTokens = [],\n toToken,\n fromAmountUsd,\n toAmount,\n toAmountUsd,\n totalFeeUsd,\n intentData,\n mode,\n opportunity,\n steps,\n progressEvents = [],\n failedStep,\n recipientAddress,\n}: NexusWidgetProgressScreenProps) {\n const intentSources = intentData?.sources ?? [];\n const intentDestination = intentData?.destination;\n const destinationSourceToken = fromTokens.find((token) => {\n const destinationChainId = intentDestination?.chain.id ?? toToken?.chainId;\n const destinationTokenAddress = (\n intentDestination?.token.contractAddress ??\n toToken?.contractAddress ??\n \"\"\n ).toLowerCase();\n const tokenAmount =\n parseDecimal(token.userAmount) ?? parseDecimal(token.balance);\n\n return (\n destinationChainId !== undefined &&\n destinationTokenAddress !== \"\" &&\n token.chainId === destinationChainId &&\n token.contractAddress.toLowerCase() === destinationTokenAddress &&\n Boolean(tokenAmount && tokenAmount.gt(0))\n );\n });\n const sourceSymbols = unique([\n ...(destinationSourceToken ? [destinationSourceToken.symbol] : []),\n ...(intentSources.length > 0\n ? intentSources.map((source) => source.token.symbol)\n : fromTokens.map((token) => token.symbol)),\n ]);\n const intentSourceUsd =\n intentSources.length > 0\n ? intentSources.reduce(\n (sum, source) => sum.plus(parseDecimal(source.value) ?? 0),\n new Decimal(0),\n )\n : parseDecimal(fromAmountUsd);\n const requestedDestinationAmount = parseDecimal(toAmount);\n const quotedDestinationAmount = parseDecimal(intentDestination?.amount);\n const destinationBalanceAmount = parseDecimal(toToken?.balance);\n const requestedDestinationUsd = parseDecimal(toAmountUsd);\n const destinationUsdRate =\n requestedDestinationAmount &&\n requestedDestinationAmount.gt(0) &&\n requestedDestinationUsd &&\n requestedDestinationUsd.gt(0)\n ? requestedDestinationUsd.div(requestedDestinationAmount)\n : quotedDestinationAmount &&\n quotedDestinationAmount.gt(0) &&\n intentDestination?.value\n ? (parseDecimal(intentDestination.value) ?? new Decimal(0)).div(\n quotedDestinationAmount,\n )\n : undefined;\n const destinationCoverageUsd =\n (mode === \"deposit\" || mode === \"send\") &&\n requestedDestinationAmount &&\n requestedDestinationAmount.gt(0) &&\n quotedDestinationAmount &&\n requestedDestinationAmount.gt(quotedDestinationAmount) &&\n destinationBalanceAmount &&\n destinationBalanceAmount.gt(0) &&\n destinationUsdRate &&\n destinationUsdRate.gt(0)\n ? Decimal.min(\n requestedDestinationAmount.minus(quotedDestinationAmount),\n destinationBalanceAmount,\n ).mul(destinationUsdRate)\n : undefined;\n const quotedDestinationUsd = parseDecimal(intentDestination?.value);\n const feeUsd = parseDecimal(totalFeeUsd);\n const sourceUsd =\n mode === \"deposit\" || mode === \"send\"\n ? [\n destinationCoverageUsd !== undefined\n ? (intentSourceUsd ?? new Decimal(0)).plus(destinationCoverageUsd)\n : intentSourceUsd,\n requestedDestinationUsd,\n requestedDestinationUsd &&\n requestedDestinationUsd.gt(0) &&\n intentSourceUsd &&\n intentSourceUsd.gt(0) &&\n quotedDestinationUsd &&\n quotedDestinationUsd.gt(0)\n ? requestedDestinationUsd.plus(\n Decimal.max(intentSourceUsd.minus(quotedDestinationUsd), 0),\n )\n : undefined,\n requestedDestinationUsd &&\n requestedDestinationUsd.gt(0) &&\n feeUsd &&\n feeUsd.gt(0)\n ? requestedDestinationUsd.plus(feeUsd)\n : undefined,\n ]\n .filter((value): value is Decimal => Boolean(value && value.gt(0)))\n .reduce(\n (max, value) => (!max || value.gt(max) ? value : max),\n undefined,\n )\n : intentSourceUsd;\n const destinationAmount =\n (mode === \"deposit\" || mode === \"send\") && toAmount\n ? toAmount\n : (intentDestination?.amount ?? toAmount ?? \"0\");\n const destinationSymbol =\n intentDestination?.token.symbol ||\n toToken?.symbol ||\n opportunity?.tokenSymbol ||\n \"\";\n const destinationChainName = getShortChainName(\n intentDestination?.chain.id ?? toToken?.chainId,\n intentDestination?.chain.name || toToken?.chainName || \"\",\n );\n const destinationChain =\n mode === \"deposit\"\n ? opportunity?.title || opportunity?.protocol || destinationChainName\n : destinationChainName;\n const seededApprovalTotal = countApprovalUnits(\n (steps ?? []).map((item) => item.step),\n );\n const completedApprovalEventTotal =\n countCompletedApprovalUnitsFromEvents(progressEvents);\n const computedApprovalTotal = Math.max(\n getApprovalTotalFromSwapStepsList(progressEvents),\n seededApprovalTotal,\n completedApprovalEventTotal,\n );\n const [lockedApprovalTotal, setLockedApprovalTotal] = useState(\n null,\n );\n const approvalTotalCount =\n lockedApprovalTotal ??\n (computedApprovalTotal > 0 ? computedApprovalTotal : null);\n\n useEffect(() => {\n if (progressEvents.length === 0) {\n setLockedApprovalTotal(null);\n return;\n }\n if (lockedApprovalTotal !== null || computedApprovalTotal <= 0) return;\n setLockedApprovalTotal(computedApprovalTotal);\n }, [computedApprovalTotal, lockedApprovalTotal, progressEvents.length]);\n\n const statusRows = buildStatusRows({\n events: progressEvents,\n failedStep,\n mode,\n steps: steps ?? [],\n approvalTotalCount,\n context: {\n destinationChain: destinationChainName || destinationChain,\n destinationSymbol,\n opportunityName: opportunity?.title || opportunity?.protocol,\n recipientAddress,\n },\n });\n const [stepsExpanded, setStepsExpanded] = useState(true);\n const activeRow =\n statusRows.find(\n (row) => row.state === \"preapproval\" || row.state === \"inProgress\",\n ) ??\n statusRows.find((row) => row.state === \"error\") ??\n statusRows.find((row) => row.state === \"default\") ??\n statusRows[statusRows.length - 1];\n const visibleRows = stepsExpanded ? statusRows : activeRow ? [activeRow] : [];\n const canExpand = statusRows.length > 1;\n const getRowHeight = (row: ProgressStatusRow) => (row.description ? 47 : 40);\n const collapsedStatusHeight = activeRow ? getRowHeight(activeRow) : 40;\n const expandedStatusHeight = statusRows.reduce(\n (sum, row) => sum + getRowHeight(row),\n 0,\n );\n\n return (\n \n \n \n {formatSymbolSummary(sourceSymbols)}\n \n \n {formatUsd(sourceUsd)}\n \n\n \n\n \n \n \n {formatDecimal(destinationAmount, 8)}\n \n {destinationSymbol}\n \n \n {destinationChain && (\n \n on {destinationChain}\n \n )}\n \n \n\n \n \n \n {visibleRows.map((row, index) => {\n const isCompleted = row.state === \"completed\";\n const isError = row.state === \"error\";\n const isDefault = row.state === \"default\";\n const isLoading =\n row.state === \"preapproval\" || row.state === \"inProgress\";\n const hasDescription = Boolean(row.description);\n const rowColor = isDefault ? muted : isError ? danger : primary;\n\n return (\n {\n if (canExpand) setStepsExpanded((current) => !current);\n }}\n style={{\n alignItems: hasDescription ? \"flex-start\" : \"center\",\n appearance: \"none\",\n background: \"transparent\",\n border: \"0\",\n borderTop:\n index > 0 && stepsExpanded ? `1px solid ${border}` : \"0\",\n boxSizing: \"border-box\",\n color: rowColor,\n cursor: canExpand ? \"pointer\" : \"default\",\n display: \"flex\",\n fontFamily,\n fontSize: \"11px\",\n fontWeight: 400,\n gap: \"7px\",\n minHeight: `${getRowHeight(row)}px`,\n padding: \"8px 11px\",\n textAlign: \"left\",\n transition:\n \"color 220ms ease, min-height 220ms ease, opacity 220ms ease\",\n width: \"100%\",\n }}\n type=\"button\"\n >\n {isCompleted || isError ? (\n \n {isError ? (\n \n ) : (\n \n )}\n \n ) : isDefault ? (\n \n ) : (\n\t \n )}\n \n \n {row.label}\n \n {row.description && (\n \n {row.description}\n \n )}\n \n {canExpand && index === 0 && (\n \n )}\n \n );\n })}\n \n \n \n \n );\n}\n", "type": "registry:component", "target": "components/nexus/components/nexus-widget-progress-screen.tsx" }, @@ -50,7 +50,7 @@ }, { "path": "registry/avail-widgets/nexus-widget/components/pay-with-sources.tsx", - "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\nimport Decimal from \"decimal.js\";\nimport { AlertCircle, ChevronDown, Loader2 } from \"lucide-react\";\nimport React, { useRef, useState } from \"react\";\nimport { type SwapTokenOption } from \"./swap-asset-selector\";\n\nconst uiFont = '\"Geist\", system-ui, sans-serif';\nconst primary = \"#161615\";\nconst muted = \"#848483\";\nconst border = \"#E8E8E7\";\nconst brand = \"var(--foreground-brand)\";\n\nconst parseDecimal = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = String(value).replace(/[^0-9.-]/g, \"\");\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst formatToken = (value: unknown) => {\n const amount = parseDecimal(value) ?? new Decimal(0);\n return amount.toDecimalPlaces(8).toFixed();\n};\n\nconst formatUsd = (value: unknown) => {\n const amount = parseDecimal(value) ?? new Decimal(0);\n if (amount.gt(0) && amount.lt(0.01)) return \"<$0.01\";\n return `$${amount.toDecimalPlaces(2).toFixed()}`;\n};\n\nconst getDisplayedUsdValue = (token: SwapTokenOption) =>\n parseDecimal(token.userAmountUsd || token.balanceInFiat) ?? new Decimal(0);\n\nconst sortSourceTokensByDisplayedUsdDesc = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const usdDiff = getDisplayedUsdValue(b).cmp(getDisplayedUsdValue(a));\n if (usdDiff !== 0) return usdDiff;\n return `${a.symbol ?? \"\"} ${a.chainName ?? \"\"}`.localeCompare(\n `${b.symbol ?? \"\"} ${b.chainName ?? \"\"}`,\n );\n });\n\nfunction TokenLogo({\n src,\n label,\n size = 30,\n fontSize = 12,\n style,\n}: {\n src?: string;\n label?: string;\n size?: number;\n fontSize?: number;\n style?: React.CSSProperties;\n}) {\n const [failed, setFailed] = useState(!src);\n\n React.useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n backgroundColor: \"#FFFFFE\",\n borderRadius: \"999px\",\n height: size,\n objectFit: \"cover\",\n width: size,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {(label || \"?\").slice(0, 1).toUpperCase()}\n \n );\n}\n\nfunction SourceLogoPair({ token }: { token: SwapTokenOption }) {\n return (\n
\n \n {token.chainLogo && (\n \n )}\n
\n );\n}\n\nfunction SkeletonRow() {\n return (\n \n \n \n \n );\n}\n\nexport function PayWithSources({\n fromTokens,\n onOpenSourcePicker,\n routeStatus,\n routeMessage,\n showAutoBadge = true,\n isSourcePickerDisabled = false,\n reserveSourceRows = false,\n}: {\n fromTokens: SwapTokenOption[];\n onOpenSourcePicker: () => void;\n routeStatus?: \"loading\" | \"insufficient\";\n routeMessage?: string;\n showAutoBadge?: boolean;\n isSourcePickerDisabled?: boolean;\n reserveSourceRows?: boolean;\n}) {\n const scrollRef = useRef(null);\n const isRouteLoading = routeStatus === \"loading\";\n const displayTokens = React.useMemo(\n () => sortSourceTokensByDisplayedUsdDesc(fromTokens),\n [fromTokens],\n );\n const shouldShowSourceSummary = !isRouteLoading && displayTokens.length > 0;\n const shouldScroll = shouldShowSourceSummary && displayTokens.length > 3;\n const sourceCountLabel =\n displayTokens.length === 1 ? \"1 asset\" : `${displayTokens.length} assets`;\n const autoBadge = (\n \n AUTO\n \n );\n\n return (\n \n \n \n \n Pay With\n {shouldShowSourceSummary ? ` · ${sourceCountLabel}` : \"\"}\n \n {showAutoBadge ? autoBadge : null}\n \n {shouldShowSourceSummary && (\n \n Edit tokens\n \n )}\n \n\n {isRouteLoading ? (\n <>\n \n \n \n Calculating best route...\n \n \n ) : shouldShowSourceSummary ? (\n
\n \n {displayTokens.map((token, index) => (\n \n \n \n \n \n \n {token.symbol}\n \n \n {token.isUnified\n ? \"Unified balance\"\n : `on ${token.chainName || \"Unknown chain\"}`}\n \n
\n \n \n \n \n {formatToken(token.userAmount || token.balance)}{\" \"}\n {token.symbol}\n \n \n {formatUsd(token.userAmountUsd || token.balanceInFiat)}\n \n \n \n ))}\n \n {shouldScroll && (\n \n scrollRef.current?.scrollBy({ behavior: \"smooth\", top: 64 })\n }\n style={{\n alignItems: \"center\",\n background: \"#FFFFFE\",\n border: `1px solid ${border}`,\n borderRadius: \"999px\",\n bottom: \"4px\",\n boxShadow: \"0 2px 8px rgba(22,22,21,0.08)\",\n cursor: \"pointer\",\n display: \"flex\",\n height: \"22px\",\n justifyContent: \"center\",\n left: \"50%\",\n padding: 0,\n position: \"absolute\",\n transform: \"translateX(-50%)\",\n width: \"22px\",\n }}\n type=\"button\"\n >\n \n \n )}\n \n ) : (\n \n Sources will be auto selected\n \n )}\n\n {routeStatus === \"insufficient\" && routeMessage && (\n \n \n {routeMessage}\n \n )}\n \n );\n}\n", + "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\nimport Decimal from \"decimal.js\";\nimport { AlertCircle, ChevronDown, Loader2 } from \"lucide-react\";\nimport React, { useRef, useState } from \"react\";\nimport { NEXUS_WIDGET_FAST_SPINNER_STYLE } from \"../theme\";\nimport { type SwapTokenOption } from \"./swap-asset-selector\";\n\nconst uiFont = '\"Geist\", system-ui, sans-serif';\nconst primary = \"#161615\";\nconst muted = \"#848483\";\nconst border = \"#E8E8E7\";\nconst brand = \"var(--foreground-brand)\";\n\nconst parseDecimal = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = String(value).replace(/[^0-9.-]/g, \"\");\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst formatToken = (value: unknown) => {\n const amount = parseDecimal(value) ?? new Decimal(0);\n return amount.toDecimalPlaces(8).toFixed();\n};\n\nconst formatUsd = (value: unknown) => {\n const amount = parseDecimal(value) ?? new Decimal(0);\n if (amount.gt(0) && amount.lt(0.01)) return \"<$0.01\";\n return `$${amount.toDecimalPlaces(2).toFixed()}`;\n};\n\nconst getDisplayedUsdValue = (token: SwapTokenOption) =>\n parseDecimal(token.userAmountUsd || token.balanceInFiat) ?? new Decimal(0);\n\nconst sortSourceTokensByDisplayedUsdDesc = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const usdDiff = getDisplayedUsdValue(b).cmp(getDisplayedUsdValue(a));\n if (usdDiff !== 0) return usdDiff;\n return `${a.symbol ?? \"\"} ${a.chainName ?? \"\"}`.localeCompare(\n `${b.symbol ?? \"\"} ${b.chainName ?? \"\"}`,\n );\n });\n\nfunction TokenLogo({\n src,\n label,\n size = 30,\n fontSize = 12,\n style,\n}: {\n src?: string;\n label?: string;\n size?: number;\n fontSize?: number;\n style?: React.CSSProperties;\n}) {\n const [failed, setFailed] = useState(!src);\n\n React.useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n backgroundColor: \"#FFFFFE\",\n borderRadius: \"999px\",\n height: size,\n objectFit: \"cover\",\n width: size,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {(label || \"?\").slice(0, 1).toUpperCase()}\n \n );\n}\n\nfunction SourceLogoPair({ token }: { token: SwapTokenOption }) {\n return (\n
\n \n {token.chainLogo && (\n \n )}\n
\n );\n}\n\nfunction SkeletonRow() {\n return (\n \n \n \n \n );\n}\n\nexport function PayWithSources({\n fromTokens,\n onOpenSourcePicker,\n routeStatus,\n routeMessage,\n showAutoBadge = true,\n isSourcePickerDisabled = false,\n reserveSourceRows = false,\n}: {\n fromTokens: SwapTokenOption[];\n onOpenSourcePicker: () => void;\n routeStatus?: \"loading\" | \"insufficient\";\n routeMessage?: string;\n showAutoBadge?: boolean;\n isSourcePickerDisabled?: boolean;\n reserveSourceRows?: boolean;\n}) {\n const scrollRef = useRef(null);\n const isRouteLoading = routeStatus === \"loading\";\n const displayTokens = React.useMemo(\n () => sortSourceTokensByDisplayedUsdDesc(fromTokens),\n [fromTokens],\n );\n const shouldShowSourceSummary = !isRouteLoading && displayTokens.length > 0;\n const shouldScroll = shouldShowSourceSummary && displayTokens.length > 3;\n const sourceCountLabel =\n displayTokens.length === 1 ? \"1 asset\" : `${displayTokens.length} assets`;\n const autoBadge = (\n \n AUTO\n \n );\n\n return (\n \n \n \n \n Pay With\n {shouldShowSourceSummary ? ` · ${sourceCountLabel}` : \"\"}\n \n {showAutoBadge ? autoBadge : null}\n \n {shouldShowSourceSummary && (\n \n Edit tokens\n \n )}\n \n\n {isRouteLoading ? (\n <>\n \n \n \n Calculating best route...\n \n \n ) : shouldShowSourceSummary ? (\n
\n \n {displayTokens.map((token, index) => (\n \n \n \n \n \n \n {token.symbol}\n \n \n {token.isUnified\n ? \"Unified balance\"\n : `on ${token.chainName || \"Unknown chain\"}`}\n \n
\n \n \n \n \n {formatToken(token.userAmount || token.balance)}{\" \"}\n {token.symbol}\n \n \n {formatUsd(token.userAmountUsd || token.balanceInFiat)}\n \n \n \n ))}\n \n {shouldScroll && (\n \n scrollRef.current?.scrollBy({ behavior: \"smooth\", top: 64 })\n }\n style={{\n alignItems: \"center\",\n background: \"#FFFFFE\",\n border: `1px solid ${border}`,\n borderRadius: \"999px\",\n bottom: \"4px\",\n boxShadow: \"0 2px 8px rgba(22,22,21,0.08)\",\n cursor: \"pointer\",\n display: \"flex\",\n height: \"22px\",\n justifyContent: \"center\",\n left: \"50%\",\n padding: 0,\n position: \"absolute\",\n transform: \"translateX(-50%)\",\n width: \"22px\",\n }}\n type=\"button\"\n >\n \n \n )}\n \n ) : (\n \n Sources will be auto selected\n \n )}\n\n {routeStatus === \"insufficient\" && routeMessage && (\n \n \n {routeMessage}\n \n )}\n \n );\n}\n", "type": "registry:component", "target": "components/nexus/components/pay-with-sources.tsx" }, @@ -68,7 +68,7 @@ }, { "path": "registry/avail-widgets/nexus-widget/components/send-idle-form.tsx", - "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\nimport Decimal from \"decimal.js\";\nimport { AlertCircle, ChevronDown, Loader2 } from \"lucide-react\";\nimport React, { useRef, useState } from \"react\";\nimport { PayWithSources as SharedPayWithSources } from \"./pay-with-sources\";\nimport {\n formatSelectedTokenBalanceLabel,\n type SwapTokenOption,\n} from \"./swap-asset-selector\";\n\ninterface SendIdleFormProps {\n amount: string;\n calculatingPercent?: number | null;\n fromTokens: SwapTokenOption[];\n isAmountReadOnly?: boolean;\n isAssetPickerDisabled?: boolean;\n isBalanceLoading?: boolean;\n isCalculatingMax?: boolean;\n isQuoteRefreshing?: boolean;\n isRecipientLocked?: boolean;\n hideDestinationTokenDropdownIcon?: boolean;\n isSourcePickerDisabled?: boolean;\n onAmountChange: (val: string) => void;\n onOpenAssetPicker: () => void;\n onOpenRecipientPicker: () => void;\n onOpenSourcePicker: () => void;\n onSetPercent: (pct: number) => void;\n recipientAddress: string;\n reserveSourceRows?: boolean;\n routeMessage?: string;\n routeStatus?: \"loading\" | \"insufficient\";\n showAutoBadge?: boolean;\n toToken?: SwapTokenOption;\n totalBalance: string;\n usdValue: string;\n}\n\nconst uiFont = '\"Geist\", system-ui, sans-serif';\nconst primary = \"#161615\";\nconst muted = \"#848483\";\nconst border = \"#E8E8E7\";\nconst brand = \"var(--foreground-brand)\";\n\nconst parseDecimal = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = String(value).replace(/[^0-9.-]/g, \"\");\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst formatToken = (value: unknown) => {\n const amount = parseDecimal(value) ?? new Decimal(0);\n return amount.toDecimalPlaces(8).toFixed();\n};\n\nconst formatUsd = (value: unknown) => {\n const amount = parseDecimal(value) ?? new Decimal(0);\n if (amount.gt(0) && amount.lt(0.01)) return \"<$0.01\";\n return `$${amount.toDecimalPlaces(2).toFixed()}`;\n};\n\nconst MAX_AMOUNT_DISPLAY_DECIMALS = 8;\nconst getTokenInputDecimals = (token?: Pick) => {\n const decimals = Number(token?.decimals);\n return Number.isFinite(decimals) && decimals >= 0 ? Math.floor(decimals) : 18;\n};\n\nconst formatAmountInputDisplay = (value: string) => {\n if (!value) return \"\";\n try {\n return new Decimal(value)\n .toDecimalPlaces(MAX_AMOUNT_DISPLAY_DECIMALS, Decimal.ROUND_DOWN)\n .toFixed();\n } catch {\n return value;\n }\n};\n\nconst sanitizeAmountInput = (raw: string, maxDecimals: number) => {\n let next = raw.replaceAll(/[^0-9.]/g, \"\");\n const parts = next.split(\".\");\n if (parts.length > 2) next = parts[0] + \".\" + parts.slice(1).join(\"\");\n const [integerPart, decimalPart] = next.split(\".\");\n if (decimalPart !== undefined) {\n next = `${integerPart}.${decimalPart.slice(0, Math.max(0, maxDecimals))}`;\n }\n if (next === \".\") next = \"0.\";\n return next;\n};\n\nfunction TokenLogo({\n src,\n label,\n size = 30,\n fontSize = 12,\n style,\n}: {\n src?: string;\n label?: string;\n size?: number;\n fontSize?: number;\n style?: React.CSSProperties;\n}) {\n const [failed, setFailed] = useState(!src);\n\n React.useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n backgroundColor: \"#FFFFFE\",\n borderRadius: \"999px\",\n height: size,\n objectFit: \"cover\",\n width: size,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {(label || \"?\").slice(0, 1).toUpperCase()}\n \n );\n}\n\nfunction SourceLogoPair({ token }: { token: SwapTokenOption }) {\n return (\n
\n \n {token.chainLogo && (\n \n )}\n
\n );\n}\n\nfunction ExactOutPercentButtons({\n visible,\n onSelect,\n}: {\n visible: boolean;\n onSelect: (pct: number) => void;\n}) {\n const [focusedPercent, setFocusedPercent] = useState(null);\n\n React.useEffect(() => {\n if (!visible) setFocusedPercent(null);\n }, [visible]);\n\n return (\n \n {[25, 50, 75, 100].map((pct) => {\n const isMax = pct === 100;\n const isFocused = focusedPercent === pct;\n return (\n {\n e.stopPropagation();\n onSelect(pct);\n }}\n onBlur={() => setFocusedPercent(null)}\n onFocus={() => setFocusedPercent(pct)}\n style={{\n alignItems: \"center\",\n backgroundColor: isFocused ? \"#E8F0FF\" : \"#F4F4F3\",\n border: \"none\",\n borderRadius: \"8px\",\n boxSizing: \"border-box\",\n color: isFocused ? brand : \"#363635\",\n cursor: \"pointer\",\n display: \"flex\",\n flex: \"1 1 0%\",\n fontFamily: '\"Geist\", system-ui, sans-serif',\n fontSize: \"12px\",\n fontWeight: 500,\n justifyContent: \"center\",\n lineHeight: \"20px\",\n minWidth: 0,\n paddingBlock: \"5px\",\n paddingInline: \"10px\",\n }}\n type=\"button\"\n >\n {isMax ? \"MAX\" : `${pct}%`}\n \n );\n })}\n \n );\n}\n\nfunction SkeletonRow() {\n return (\n \n \n \n \n );\n}\n\nfunction PayWithSources({\n fromTokens,\n onOpenSourcePicker,\n routeStatus,\n routeMessage,\n}: {\n fromTokens: SwapTokenOption[];\n onOpenSourcePicker: () => void;\n routeStatus?: \"loading\" | \"insufficient\";\n routeMessage?: string;\n}) {\n const scrollRef = useRef(null);\n const isRouteLoading = routeStatus === \"loading\";\n const shouldShowSourceSummary = !isRouteLoading && fromTokens.length > 0;\n const shouldScroll = shouldShowSourceSummary && fromTokens.length > 3;\n const autoBadge = (\n \n AUTO\n \n );\n\n return (\n \n \n \n \n Pay With\n {shouldShowSourceSummary ? ` · ${fromTokens.length} assets` : \"\"}\n \n {autoBadge}\n \n {shouldShowSourceSummary && (\n \n Edit tokens\n \n )}\n \n\n {isRouteLoading ? (\n <>\n \n \n \n Calculating best route...\n \n \n ) : shouldShowSourceSummary ? (\n
\n \n {fromTokens.map((token, index) => (\n \n \n \n \n \n {token.symbol}\n \n \n {token.isUnified\n ? \"Unified balance\"\n : `on ${token.chainName || \"Unknown chain\"}`}\n \n
\n \n \n \n {formatToken(token.userAmount || token.balance)}{\" \"}\n {token.symbol}\n \n \n {formatUsd(token.userAmountUsd || token.balanceInFiat)}\n \n \n \n ))}\n \n {shouldScroll && (\n \n scrollRef.current?.scrollBy({ behavior: \"smooth\", top: 64 })\n }\n style={{\n alignItems: \"center\",\n background: \"#FFFFFE\",\n border: `1px solid ${border}`,\n borderRadius: \"999px\",\n bottom: \"4px\",\n boxShadow: \"0 2px 8px rgba(22,22,21,0.08)\",\n cursor: \"pointer\",\n display: \"flex\",\n height: \"22px\",\n justifyContent: \"center\",\n left: \"50%\",\n padding: 0,\n position: \"absolute\",\n transform: \"translateX(-50%)\",\n width: \"22px\",\n }}\n type=\"button\"\n >\n \n \n )}\n \n ) : (\n \n Sources will be auto selected\n \n )}\n\n {routeStatus === \"insufficient\" && routeMessage && (\n \n \n {routeMessage}\n \n )}\n \n );\n}\n\nfunction BalanceSkeleton({\n height = \"16px\",\n width = \"72px\",\n}: {\n height?: string;\n width?: string;\n}) {\n return (\n \n );\n}\n\nexport function SendIdleForm({\n amount,\n onAmountChange,\n toToken,\n fromTokens,\n totalBalance,\n usdValue,\n onOpenAssetPicker,\n onOpenSourcePicker,\n onOpenRecipientPicker,\n recipientAddress,\n onSetPercent,\n routeStatus,\n routeMessage,\n isAmountReadOnly = false,\n isAssetPickerDisabled = false,\n isBalanceLoading = false,\n isCalculatingMax,\n calculatingPercent,\n isQuoteRefreshing,\n isRecipientLocked = false,\n hideDestinationTokenDropdownIcon = false,\n showAutoBadge = true,\n isSourcePickerDisabled = false,\n reserveSourceRows = false,\n}: SendIdleFormProps) {\n const [pendingPercent, setPendingPercent] = useState(null);\n const [isAmountFocused, setIsAmountFocused] = useState(false);\n\n React.useEffect(() => {\n if (!isCalculatingMax) setPendingPercent(null);\n }, [isCalculatingMax]);\n\n const handlePercentSelect = (pct: number) => {\n setPendingPercent(pct);\n onSetPercent(pct);\n };\n\n const handleInput = (e: React.ChangeEvent) => {\n if (isAmountReadOnly) return;\n onAmountChange(\n sanitizeAmountInput(e.target.value, getTokenInputDecimals(toToken)),\n );\n };\n const amountDisplayValue = isAmountFocused\n ? amount\n : formatAmountInputDisplay(amount);\n const activePendingPercent =\n calculatingPercent ?? (isCalculatingMax ? pendingPercent : null);\n const isMaxCalculating = Boolean(\n isCalculatingMax && activePendingPercent === 100,\n );\n\n const destinationBalanceLabel =\n formatSelectedTokenBalanceLabel(toToken) ||\n `0 ${toToken?.symbol || \"\"}`.trim();\n\n return (\n \n \n \n \n Recipient\n \n \n \n {recipientAddress\n ? `${recipientAddress.slice(0, 6)}…${recipientAddress.slice(-4)}`\n : \"Select recipient\"}\n \n \n \n {isRecipientLocked ? \"Locked\" : \"Edit\"}\n \n \n \n \n \n\n \n \n \n Amount\n \n
\n \n Total Balance:\n \n \n {isBalanceLoading ? (\n \n ) : (\n `$${totalBalance}`\n )}\n \n
\n \n\n
\n \n {isMaxCalculating ? (\n \n ) : (\n setIsAmountFocused(false)}\n onChange={handleInput}\n onFocus={() => setIsAmountFocused(true)}\n placeholder=\"0\"\n style={{\n background: \"transparent\",\n border: \"none\",\n boxSizing: \"border-box\",\n color: amount ? primary : \"#C8C8C6\",\n cursor: isAmountReadOnly ? \"default\" : \"text\",\n flex: \"1 1 0%\",\n fontFamily:\n '\"Delight-Medium\", \"Delight\", system-ui, sans-serif',\n fontSize: \"36px\",\n fontWeight: 500,\n lineHeight: \"44px\",\n minWidth: 0,\n outline: \"none\",\n padding: 0,\n }}\n readOnly={isAmountReadOnly}\n type=\"text\"\n value={amountDisplayValue}\n />\n )}\n {isCalculatingMax && !isMaxCalculating && (\n \n )}\n\n \n {toToken ? (\n \n \n {toToken.chainLogo && (\n \n )}\n
\n ) : (\n \n )}\n \n {toToken ? toToken.symbol : \"Assets\"}\n \n {!hideDestinationTokenDropdownIcon && (\n \n )}\n \n \n\n \n \n \n ≈ ${usdValue || \"0\"}\n \n \n\n \n {toToken && (\n <>\n \n Balance:\n \n \n {isBalanceLoading ? (\n \n ) : (\n destinationBalanceLabel\n )}\n \n \n )}\n \n \n\n \n \n \n\n \n \n );\n}\n", + "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\nimport Decimal from \"decimal.js\";\nimport { AlertCircle, ChevronDown, Loader2 } from \"lucide-react\";\nimport React, { useRef, useState } from \"react\";\nimport { PayWithSources as SharedPayWithSources } from \"./pay-with-sources\";\nimport { NEXUS_WIDGET_FAST_SPINNER_STYLE } from \"../theme\";\nimport {\n formatSelectedTokenBalanceLabel,\n type SwapTokenOption,\n} from \"./swap-asset-selector\";\n\ninterface SendIdleFormProps {\n amount: string;\n calculatingPercent?: number | null;\n fromTokens: SwapTokenOption[];\n isAmountReadOnly?: boolean;\n isAssetPickerDisabled?: boolean;\n isBalanceLoading?: boolean;\n isCalculatingMax?: boolean;\n isQuoteRefreshing?: boolean;\n isRecipientLocked?: boolean;\n hideDestinationTokenDropdownIcon?: boolean;\n isSourcePickerDisabled?: boolean;\n onAmountChange: (val: string) => void;\n onOpenAssetPicker: () => void;\n onOpenRecipientPicker: () => void;\n onOpenSourcePicker: () => void;\n onSetPercent: (pct: number) => void;\n recipientAddress: string;\n reserveSourceRows?: boolean;\n routeMessage?: string;\n routeStatus?: \"loading\" | \"insufficient\";\n showAutoBadge?: boolean;\n toToken?: SwapTokenOption;\n totalBalance: string;\n usdValue: string;\n}\n\nconst uiFont = '\"Geist\", system-ui, sans-serif';\nconst primary = \"#161615\";\nconst muted = \"#848483\";\nconst border = \"#E8E8E7\";\nconst brand = \"var(--foreground-brand)\";\n\nconst parseDecimal = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = String(value).replace(/[^0-9.-]/g, \"\");\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst formatToken = (value: unknown) => {\n const amount = parseDecimal(value) ?? new Decimal(0);\n return amount.toDecimalPlaces(8).toFixed();\n};\n\nconst formatUsd = (value: unknown) => {\n const amount = parseDecimal(value) ?? new Decimal(0);\n if (amount.gt(0) && amount.lt(0.01)) return \"<$0.01\";\n return `$${amount.toDecimalPlaces(2).toFixed()}`;\n};\n\nconst MAX_AMOUNT_DISPLAY_DECIMALS = 8;\nconst getTokenInputDecimals = (token?: Pick) => {\n const decimals = Number(token?.decimals);\n return Number.isFinite(decimals) && decimals >= 0 ? Math.floor(decimals) : 18;\n};\n\nconst formatAmountInputDisplay = (value: string) => {\n if (!value) return \"\";\n try {\n return new Decimal(value)\n .toDecimalPlaces(MAX_AMOUNT_DISPLAY_DECIMALS, Decimal.ROUND_DOWN)\n .toFixed();\n } catch {\n return value;\n }\n};\n\nconst sanitizeAmountInput = (raw: string, maxDecimals: number) => {\n let next = raw.replaceAll(/[^0-9.]/g, \"\");\n const parts = next.split(\".\");\n if (parts.length > 2) next = parts[0] + \".\" + parts.slice(1).join(\"\");\n const [integerPart, decimalPart] = next.split(\".\");\n if (decimalPart !== undefined) {\n next = `${integerPart}.${decimalPart.slice(0, Math.max(0, maxDecimals))}`;\n }\n if (next === \".\") next = \"0.\";\n return next;\n};\n\nfunction TokenLogo({\n src,\n label,\n size = 30,\n fontSize = 12,\n style,\n}: {\n src?: string;\n label?: string;\n size?: number;\n fontSize?: number;\n style?: React.CSSProperties;\n}) {\n const [failed, setFailed] = useState(!src);\n\n React.useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n backgroundColor: \"#FFFFFE\",\n borderRadius: \"999px\",\n height: size,\n objectFit: \"cover\",\n width: size,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {(label || \"?\").slice(0, 1).toUpperCase()}\n \n );\n}\n\nfunction SourceLogoPair({ token }: { token: SwapTokenOption }) {\n return (\n
\n \n {token.chainLogo && (\n \n )}\n
\n );\n}\n\nfunction ExactOutPercentButtons({\n visible,\n onSelect,\n}: {\n visible: boolean;\n onSelect: (pct: number) => void;\n}) {\n const [focusedPercent, setFocusedPercent] = useState(null);\n\n React.useEffect(() => {\n if (!visible) setFocusedPercent(null);\n }, [visible]);\n\n return (\n \n {[25, 50, 75, 100].map((pct) => {\n const isMax = pct === 100;\n const isFocused = focusedPercent === pct;\n return (\n {\n e.stopPropagation();\n onSelect(pct);\n }}\n onBlur={() => setFocusedPercent(null)}\n onFocus={() => setFocusedPercent(pct)}\n style={{\n alignItems: \"center\",\n backgroundColor: isFocused ? \"#E8F0FF\" : \"#F4F4F3\",\n border: \"none\",\n borderRadius: \"8px\",\n boxSizing: \"border-box\",\n color: isFocused ? brand : \"#363635\",\n cursor: \"pointer\",\n display: \"flex\",\n flex: \"1 1 0%\",\n fontFamily: '\"Geist\", system-ui, sans-serif',\n fontSize: \"12px\",\n fontWeight: 500,\n justifyContent: \"center\",\n lineHeight: \"20px\",\n minWidth: 0,\n paddingBlock: \"5px\",\n paddingInline: \"10px\",\n }}\n type=\"button\"\n >\n {isMax ? \"MAX\" : `${pct}%`}\n \n );\n })}\n \n );\n}\n\nfunction SkeletonRow() {\n return (\n \n \n \n \n );\n}\n\nfunction PayWithSources({\n fromTokens,\n onOpenSourcePicker,\n routeStatus,\n routeMessage,\n}: {\n fromTokens: SwapTokenOption[];\n onOpenSourcePicker: () => void;\n routeStatus?: \"loading\" | \"insufficient\";\n routeMessage?: string;\n}) {\n const scrollRef = useRef(null);\n const isRouteLoading = routeStatus === \"loading\";\n const shouldShowSourceSummary = !isRouteLoading && fromTokens.length > 0;\n const shouldScroll = shouldShowSourceSummary && fromTokens.length > 3;\n const autoBadge = (\n \n AUTO\n \n );\n\n return (\n \n \n \n \n Pay With\n {shouldShowSourceSummary ? ` · ${fromTokens.length} assets` : \"\"}\n \n {autoBadge}\n \n {shouldShowSourceSummary && (\n \n Edit tokens\n \n )}\n \n\n {isRouteLoading ? (\n <>\n \n \n \n Calculating best route...\n \n \n ) : shouldShowSourceSummary ? (\n
\n \n {fromTokens.map((token, index) => (\n \n \n \n \n \n {token.symbol}\n \n \n {token.isUnified\n ? \"Unified balance\"\n : `on ${token.chainName || \"Unknown chain\"}`}\n \n
\n \n \n \n {formatToken(token.userAmount || token.balance)}{\" \"}\n {token.symbol}\n \n \n {formatUsd(token.userAmountUsd || token.balanceInFiat)}\n \n \n \n ))}\n \n {shouldScroll && (\n \n scrollRef.current?.scrollBy({ behavior: \"smooth\", top: 64 })\n }\n style={{\n alignItems: \"center\",\n background: \"#FFFFFE\",\n border: `1px solid ${border}`,\n borderRadius: \"999px\",\n bottom: \"4px\",\n boxShadow: \"0 2px 8px rgba(22,22,21,0.08)\",\n cursor: \"pointer\",\n display: \"flex\",\n height: \"22px\",\n justifyContent: \"center\",\n left: \"50%\",\n padding: 0,\n position: \"absolute\",\n transform: \"translateX(-50%)\",\n width: \"22px\",\n }}\n type=\"button\"\n >\n \n \n )}\n \n ) : (\n \n Sources will be auto selected\n \n )}\n\n {routeStatus === \"insufficient\" && routeMessage && (\n \n \n {routeMessage}\n \n )}\n \n );\n}\n\nfunction BalanceSkeleton({\n height = \"16px\",\n width = \"72px\",\n}: {\n height?: string;\n width?: string;\n}) {\n return (\n \n );\n}\n\nexport function SendIdleForm({\n amount,\n onAmountChange,\n toToken,\n fromTokens,\n totalBalance,\n usdValue,\n onOpenAssetPicker,\n onOpenSourcePicker,\n onOpenRecipientPicker,\n recipientAddress,\n onSetPercent,\n routeStatus,\n routeMessage,\n isAmountReadOnly = false,\n isAssetPickerDisabled = false,\n isBalanceLoading = false,\n isCalculatingMax,\n calculatingPercent,\n isQuoteRefreshing,\n isRecipientLocked = false,\n hideDestinationTokenDropdownIcon = false,\n showAutoBadge = true,\n isSourcePickerDisabled = false,\n reserveSourceRows = false,\n}: SendIdleFormProps) {\n const [pendingPercent, setPendingPercent] = useState(null);\n const [isAmountFocused, setIsAmountFocused] = useState(false);\n\n React.useEffect(() => {\n if (!isCalculatingMax) setPendingPercent(null);\n }, [isCalculatingMax]);\n\n const handlePercentSelect = (pct: number) => {\n setPendingPercent(pct);\n onSetPercent(pct);\n };\n\n const handleInput = (e: React.ChangeEvent) => {\n if (isAmountReadOnly) return;\n onAmountChange(\n sanitizeAmountInput(e.target.value, getTokenInputDecimals(toToken)),\n );\n };\n const amountDisplayValue = isAmountFocused\n ? amount\n : formatAmountInputDisplay(amount);\n const activePendingPercent =\n calculatingPercent ?? (isCalculatingMax ? pendingPercent : null);\n const isMaxCalculating = Boolean(\n isCalculatingMax && activePendingPercent === 100,\n );\n\n const destinationBalanceLabel =\n formatSelectedTokenBalanceLabel(toToken) ||\n `0 ${toToken?.symbol || \"\"}`.trim();\n\n return (\n \n \n \n \n Recipient\n \n \n \n {recipientAddress\n ? `${recipientAddress.slice(0, 6)}…${recipientAddress.slice(-4)}`\n : \"Select recipient\"}\n \n \n \n {isRecipientLocked ? \"Locked\" : \"Edit\"}\n \n \n \n \n \n\n \n \n \n Amount\n \n
\n \n Total Balance:\n \n \n {isBalanceLoading ? (\n \n ) : (\n `$${totalBalance}`\n )}\n \n
\n \n\n
\n \n {isMaxCalculating ? (\n \n ) : (\n setIsAmountFocused(false)}\n onChange={handleInput}\n onFocus={() => setIsAmountFocused(true)}\n placeholder=\"0\"\n style={{\n background: \"transparent\",\n border: \"none\",\n boxSizing: \"border-box\",\n color: amount ? primary : \"#C8C8C6\",\n cursor: isAmountReadOnly ? \"default\" : \"text\",\n flex: \"1 1 0%\",\n fontFamily:\n '\"Delight-Medium\", \"Delight\", system-ui, sans-serif',\n fontSize: \"36px\",\n fontWeight: 500,\n lineHeight: \"44px\",\n minWidth: 0,\n outline: \"none\",\n padding: 0,\n }}\n readOnly={isAmountReadOnly}\n type=\"text\"\n value={amountDisplayValue}\n />\n )}\n {isCalculatingMax && !isMaxCalculating && (\n \n )}\n\n \n {toToken ? (\n \n \n {toToken.chainLogo && (\n \n )}\n
\n ) : (\n \n )}\n \n {toToken ? toToken.symbol : \"Assets\"}\n \n {!hideDestinationTokenDropdownIcon && (\n \n )}\n \n \n\n \n \n \n ≈ ${usdValue || \"0\"}\n \n \n\n \n {toToken && (\n <>\n \n Balance:\n \n \n {isBalanceLoading ? (\n \n ) : (\n destinationBalanceLabel\n )}\n \n \n )}\n \n \n\n \n \n \n\n \n \n );\n}\n", "type": "registry:component", "target": "components/nexus/components/send-idle-form.tsx" }, @@ -80,7 +80,7 @@ }, { "path": "registry/avail-widgets/nexus-widget/components/swap-asset-selector.tsx", - "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\n\"use client\";\nimport type { SupportedChainsAndTokensResult } from \"@avail-project/nexus-core\";\nimport { formatTokenBalance } from \"@avail-project/nexus-core/utils\";\nimport Decimal from \"decimal.js\";\nimport {\n Check,\n ChevronDown,\n ChevronUp,\n Globe,\n Info,\n Loader2,\n Minus,\n Search,\n X,\n} from \"lucide-react\";\nimport React, {\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n CHAIN_METADATA,\n getSdkSwapSupportedChainIds,\n getShortChainName,\n isSwapSupportedBySdkChainList,\n} from \"../../common/utils/constant\";\nimport type { UserAsset } from \"../../nexus/NexusProvider\";\n\nconst tabularNums: React.CSSProperties = {\n fontFeatureSettings: '\"tnum\"',\n fontVariantNumeric: \"tabular-nums\",\n};\n\nconst brand = \"var(--foreground-brand)\";\n\nexport interface SwapTokenOption {\n balance: string;\n balanceInFiat: string;\n chainId?: number;\n chainLogo?: string;\n chainName?: string;\n contractAddress: string;\n decimals: number;\n isUnified?: boolean;\n logo?: string;\n name: string;\n priceUSD?: number | string;\n sourceTokens?: SwapTokenOption[];\n symbol: string;\n unifiedSymbol?: \"USDC\" | \"USDT\" | \"ETH\";\n userAmount?: string;\n userAmountMode?: \"token\" | \"usd\";\n userAmountUsd?: string;\n}\n\ninterface SwapAssetSelectorProps {\n allowSelectedTokenRemoval?: boolean;\n allowUnified?: boolean;\n autoSelectFilterTabs?: boolean;\n editingAssetIndex?: number | null;\n filterTabBehavior?: FilterTabBehavior;\n hideCustomTab?: boolean;\n initialFilterTab?: FilterTab;\n isMulti?: boolean;\n lockedTokens?: SwapTokenOption[];\n onBack: () => void;\n onClearSelection?: () => void;\n onDone?: (tokens?: SwapTokenOption[]) => void;\n onFilterTabSelect?: (tab: Exclude) => void;\n onSelect: (token: SwapTokenOption) => void;\n onSelectionChange?: (tokens: SwapTokenOption[]) => void;\n onToggle?: (token: SwapTokenOption) => void;\n preserveSelectedBelowMinimum?: boolean;\n requiredUsd?: string;\n selectedTokens?: SwapTokenOption[];\n showBelowMinimumInline?: boolean;\n staticOptions?: SwapTokenOption[];\n swapBalance: UserAsset[] | null;\n swapSupportedChains?: SupportedChainsAndTokensResult | null;\n title: string;\n}\n\nexport function deriveTokenOptions(\n swapBalance: UserAsset[],\n swapSupportedChains?: SupportedChainsAndTokensResult | null,\n): SwapTokenOption[] {\n const tokens: SwapTokenOption[] = [];\n for (const asset of swapBalance) {\n for (const bd of asset.breakdown ?? []) {\n if (!isSwapSupportedBySdkChainList(bd.chain?.id, swapSupportedChains)) {\n continue;\n }\n if (Number.parseFloat(bd.balance ?? \"0\") <= 0) continue;\n const chainMeta = bd.chain?.id ? CHAIN_METADATA[bd.chain.id] : undefined;\n tokens.push({\n contractAddress: bd.contractAddress,\n symbol: bd.symbol ?? asset.symbol,\n name: bd.symbol ?? asset.symbol,\n logo: asset.logo ?? \"\",\n decimals: bd.decimals ?? asset.decimals ?? 18,\n balance: bd.balance,\n balanceInFiat:\n bd.balanceInFiat != null\n ? `$${Number(bd.balanceInFiat).toFixed(2)}`\n : \"$0.00\",\n chainId: bd.chain?.id,\n chainName: getShortChainName(\n bd.chain?.id,\n chainMeta?.name ?? bd.chain?.name,\n ),\n chainLogo: chainMeta?.logo ?? bd.chain?.logo,\n });\n }\n }\n const seen = new Map();\n for (const t of tokens) {\n seen.set(`${t.contractAddress.toLowerCase()}-${t.chainId}`, t);\n }\n return Array.from(seen.values());\n}\n\n/* ── Radio dot (circular) ── */\nexport const RadioDot = ({ selected }: { selected: boolean }) => (\n \n {selected && (\n \n )}\n \n);\n\nconst SelectionControl = ({\n selected,\n indeterminate = false,\n multi,\n}: {\n selected: boolean;\n indeterminate?: boolean;\n multi: boolean;\n}) => {\n if (!multi) return ;\n\n const isActive = selected || indeterminate;\n\n return (\n \n {selected && (\n \n )}\n {!selected && indeterminate && (\n \n )}\n \n );\n};\n\nfunction TokenLogo({\n src,\n label,\n size,\n fontSize,\n style,\n fallbackBackground = brand,\n fallbackColor = \"#FFFFFE\",\n}: {\n src?: string;\n label?: string;\n size: number;\n fontSize: number;\n style?: React.CSSProperties;\n fallbackBackground?: string;\n fallbackColor?: string;\n}) {\n const normalizedSrc = src?.trim();\n const [failed, setFailed] = useState(!normalizedSrc);\n\n useEffect(() => {\n setFailed(!normalizedSrc);\n }, [normalizedSrc]);\n\n if (!failed && normalizedSrc) {\n return (\n setFailed(true)}\n src={normalizedSrc}\n style={{\n borderRadius: \"999px\",\n height: size,\n objectFit: \"cover\",\n width: size,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {(label || \"?\").slice(0, 2).toUpperCase()}\n \n );\n}\n\nconst getChainLogo = (\n token?: Pick | null,\n) => {\n if (!token) return undefined;\n return token.chainId\n ? (CHAIN_METADATA[token.chainId]?.logo ?? token.chainLogo)\n : token.chainLogo;\n};\n\n/* ── Chain logo cluster ── */\nconst ChainLogos = ({ tokens }: { tokens: SwapTokenOption[] }) => {\n const clusterRef = useRef(null);\n const closeTimerRef = useRef | null>(null);\n const [showTooltip, setShowTooltip] = useState(false);\n const [tooltipRect, setTooltipRect] = useState(null);\n const uniqueChains = useMemo(() => {\n const seen = new Set();\n const out: {\n id: number;\n logo?: string;\n name?: string;\n balance?: string;\n balanceInFiat?: string;\n balanceValue: number;\n fiatValue: number;\n }[] = [];\n for (const t of tokens) {\n if (t.chainId && !seen.has(t.chainId)) {\n const balanceValue = Number(\n String(t.balance ?? \"\").replace(/[^0-9.]/g, \"\") || 0,\n );\n seen.add(t.chainId);\n out.push({\n id: t.chainId,\n logo: getChainLogo(t),\n name: getShortChainName(t.chainId, t.chainName),\n balance: t.balance,\n balanceInFiat: t.balanceInFiat,\n balanceValue:\n Number.isNaN(balanceValue) || !Number.isFinite(balanceValue)\n ? 0\n : balanceValue,\n fiatValue: getTokenFiatValue(t),\n });\n }\n }\n return out.sort((a, b) => {\n if (a.fiatValue !== b.fiatValue) return b.fiatValue - a.fiatValue;\n if (a.balanceValue !== b.balanceValue) {\n return b.balanceValue - a.balanceValue;\n }\n return (a.name ?? \"\").localeCompare(b.name ?? \"\");\n });\n }, [tokens]);\n\n const maxShow = 3;\n const shown = uniqueChains.slice(0, maxShow);\n const openTooltip = () => {\n if (closeTimerRef.current) {\n clearTimeout(closeTimerRef.current);\n closeTimerRef.current = null;\n }\n setTooltipRect(clusterRef.current?.getBoundingClientRect() ?? null);\n setShowTooltip(true);\n };\n const closeTooltip = () => {\n if (closeTimerRef.current) clearTimeout(closeTimerRef.current);\n closeTimerRef.current = setTimeout(() => {\n setShowTooltip(false);\n closeTimerRef.current = null;\n }, 120);\n };\n useEffect(() => {\n return () => {\n if (closeTimerRef.current) clearTimeout(closeTimerRef.current);\n };\n }, []);\n const showTooltipAbove = tooltipRect ? tooltipRect.top > 240 : true;\n const tooltip =\n showTooltip && uniqueChains.length > 1 && tooltipRect\n ? createPortal(\n \n \n UNIFIED · {uniqueChains.length} CHAINS\n \n {tokens\n .reduce((sum, token) => sum + getTokenFiatValue(token), 0)\n .toLocaleString(undefined, {\n currency: \"USD\",\n maximumFractionDigits: 2,\n style: \"currency\",\n })}\n \n \n {uniqueChains.map((chain) => (\n \n \n \n \n {chain.name || \"Unknown chain\"}\n \n \n \n {chain.balance\n ? formatTokenAmountDisplay(\n String(chain.balance).replace(/\\s+[^\\s]+$/, \"\"),\n )\n : chain.balanceInFiat || \"0\"}\n \n \n ))}\n ,\n document.body,\n )\n : null;\n\n return (\n \n {tooltip}\n {shown.map((c) => (\n \n ))}\n \n {uniqueChains.length} chain{uniqueChains.length !== 1 ? \"s\" : \"\"}\n \n \n );\n};\n\n/* ── Filter tabs ── */\ntype FilterTab = \"all\" | \"native\" | \"stables\" | \"custom\";\ntype FilterTabBehavior = \"select-all\" | \"source-pool\";\nconst FILTER_TABS: { key: FilterTab; label: string }[] = [\n { key: \"all\", label: \"All\" },\n { key: \"native\", label: \"Native\" },\n { key: \"stables\", label: \"Stables\" },\n { key: \"custom\", label: \"Custom\" },\n];\nconst STABLE_SYMBOLS = new Set([\n \"AUSD\",\n \"BDO\",\n \"BRZ\",\n \"BTCUSD\",\n \"BUSD\",\n \"bnbUSD\",\n \"crvUSD\",\n \"CUSD\",\n \"DAI\",\n \"DAI.e\",\n \"dEURO\",\n \"DUSD\",\n \"EURA\",\n \"EURC\",\n \"EURCV\",\n \"EURe\",\n \"EURS\",\n \"EURT\",\n \"eUSD\",\n \"frxUSD\",\n \"GHO\",\n \"GUSD\",\n \"JUST\",\n \"jEUR\",\n \"JUSD\",\n \"LUSD\",\n \"rUSD\",\n \"RLUSD\",\n \"sUSD\",\n \"svJUSD\",\n \"TUSD\",\n \"USDC\",\n \"USDC.e\",\n \"USDS\",\n \"USDT\",\n \"USDT0\",\n \"USDT.e\",\n \"ctUSD\",\n \"PYUSD\",\n \"USDe\",\n \"xDAI\",\n \"USD0\",\n \"USDM\",\n]);\nconst normalizeTokenGroupSymbol = (symbol: string) =>\n symbol\n .trim()\n .toUpperCase()\n .replaceAll(\"₮\", \"T\")\n .replaceAll(/[^A-Z0-9]/g, \"\");\nconst STABLE_SYMBOL_KEYS = new Set(\n Array.from(STABLE_SYMBOLS, normalizeTokenGroupSymbol),\n);\n\nconst isStableToken = (token: SwapTokenOption) =>\n STABLE_SYMBOL_KEYS.has(normalizeTokenGroupSymbol(token.symbol));\n\nfunction isNativeToken(t: SwapTokenOption) {\n if (isNativeLikeAddress(t.contractAddress)) return true;\n\n const sym = t.symbol.toUpperCase();\n const chain = (t.chainName || \"\").toLowerCase();\n\n if (sym === \"ETH\")\n return (\n !chain.includes(\"bnb\") &&\n !chain.includes(\"bsc\") &&\n !chain.includes(\"polygon\") &&\n !chain.includes(\"monad\") &&\n !chain.includes(\"hyperevm\")\n );\n if (sym === \"POL\" || sym === \"MATIC\") return chain.includes(\"polygon\");\n if (sym === \"HYPE\") return chain.includes(\"hyperevm\");\n if (sym === \"MON\") return chain.includes(\"monad\");\n if (sym === \"BNB\") return chain.includes(\"bnb\") || chain.includes(\"bsc\");\n if (sym === \"AVAX\") return chain.includes(\"avalanche\");\n if (sym === \"FTM\") return chain.includes(\"fantom\");\n if (sym === \"CELO\") return chain.includes(\"celo\");\n if (sym === \"SUI\") return chain.includes(\"sui\");\n if (sym === \"APT\") return chain.includes(\"aptos\");\n if (sym === \"SOL\") return chain.includes(\"solana\");\n return false;\n}\n\nconst MIN_FIAT_THRESHOLD = 1;\nconst CHAIN_SELECTOR_CLOSE_MS = 220;\nconst MODAL_HEIGHT_TRANSITION_MS = 260;\nconst modalHeightTransitionStyle = {\n interpolateSize: \"allow-keywords\",\n} as React.CSSProperties;\nconst modalHeightTransition = `height ${MODAL_HEIGHT_TRANSITION_MS}ms ease, max-height ${MODAL_HEIGHT_TRANSITION_MS}ms ease`;\nexport const SWAP_CHAIN_DISPLAY_ORDER = [\n 1, // Ethereum\n 42161, // Arbitrum\n 8453, // Base\n 137, // Polygon\n 10, // OP\n 999, // HyperEVM\n 56, // BSC\n // 43114, // Avalanche\n 143, // Monad\n 4326, // MegaETH\n 4114, // Citrea\n 8217, // Kaia\n 534352, // Scroll\n] as const;\nconst SWAP_CHAIN_DISPLAY_ORDER_RANK = new Map(\n SWAP_CHAIN_DISPLAY_ORDER.map((chainId, index) => [chainId, index]),\n);\nexport const SWAP_CHAIN_DISPLAY_ORDER_SET = new Set(\n SWAP_CHAIN_DISPLAY_ORDER,\n);\nexport const sortChainIdsBySwapDisplayOrder = (chainIds: number[]) =>\n [...chainIds].sort((a, b) => {\n const aRank =\n SWAP_CHAIN_DISPLAY_ORDER_RANK.get(a) ?? Number.MAX_SAFE_INTEGER;\n const bRank =\n SWAP_CHAIN_DISPLAY_ORDER_RANK.get(b) ?? Number.MAX_SAFE_INTEGER;\n if (aRank !== bRank) return aRank - bRank;\n\n const aName = CHAIN_METADATA[a]?.name ?? String(a);\n const bName = CHAIN_METADATA[b]?.name ?? String(b);\n return aName.localeCompare(bName);\n });\nexport const compareChainsBySwapDisplayOrder = <\n T extends { chainId?: number; chainName?: string },\n>(\n a: T,\n b: T,\n) => {\n const aRank =\n SWAP_CHAIN_DISPLAY_ORDER_RANK.get(a.chainId ?? -1) ??\n Number.MAX_SAFE_INTEGER;\n const bRank =\n SWAP_CHAIN_DISPLAY_ORDER_RANK.get(b.chainId ?? -1) ??\n Number.MAX_SAFE_INTEGER;\n if (aRank !== bRank) return aRank - bRank;\n return (a.chainName ?? \"\").localeCompare(b.chainName ?? \"\");\n};\nconst UNIFIED_MAINNET_CHAIN_IDS = new Set([\n 1, 10, 56, 137, 143, 999, 4114, 8217, 8453, 42161, 43114, 534352, 4326,\n]);\nconst UNIFIED_USDC_SYMBOL_KEYS = new Set([\"USDC\", \"USDCE\", \"USDM\"]);\nconst UNIFIED_USDT_SYMBOL_KEYS = new Set([\"USDT\", \"USDT0\", \"USDTE\"]);\n\nconst escapeRegExp = (value: string) =>\n value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\nconst getTokenFiatValue = (token: Pick) => {\n const parsed = Number(\n String(token.balanceInFiat ?? \"\").replace(/[^0-9.]/g, \"\") || 0,\n );\n return isNaN(parsed) || !isFinite(parsed) ? 0 : parsed;\n};\n\nconst formatBalanceWithSymbol = (\n token: Pick,\n) => {\n const symbol = token.symbol?.trim() || \"\";\n const balanceStr = String(token.balance ?? \"\").trim();\n let cleanBalance = balanceStr;\n if (symbol) {\n cleanBalance = balanceStr.replace(\n new RegExp(`\\\\s*${escapeRegExp(symbol)}$`, \"i\"),\n \"\",\n );\n }\n const formatted = formatTokenAmountDisplay(cleanBalance);\n return symbol ? `${formatted} ${symbol}` : formatted;\n};\n\nconst parseTokenAmount = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = String(value).replace(/[^0-9.-]/g, \"\");\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst compareTokensByUsdBalance = (a: SwapTokenOption, b: SwapTokenOption) => {\n const fiatDelta = getTokenFiatValue(b) - getTokenFiatValue(a);\n if (fiatDelta !== 0) return fiatDelta;\n\n const aBalance = parseTokenAmount(a.balance) ?? new Decimal(0);\n const bBalance = parseTokenAmount(b.balance) ?? new Decimal(0);\n const balanceDelta = bBalance.cmp(aBalance);\n if (balanceDelta !== 0) return balanceDelta;\n\n const chainDelta = compareChainsBySwapDisplayOrder(a, b);\n if (chainDelta !== 0) return chainDelta;\n\n return `${a.symbol} ${a.chainName}`.localeCompare(\n `${b.symbol} ${b.chainName}`,\n );\n};\n\nconst sortTokensByUsdBalance = (tokens: SwapTokenOption[]) =>\n [...tokens].sort(compareTokensByUsdBalance);\n\nexport const formatTokenAmountDisplay = (value: unknown) => {\n const amount = parseTokenAmount(value) ?? new Decimal(0);\n const abs = amount.abs();\n\n if (amount.isZero()) return \"0\";\n\n const compactUnits = [\n { suffix: \"T\", value: new Decimal(1_000_000_000_000) },\n { suffix: \"B\", value: new Decimal(1_000_000_000) },\n { suffix: \"M\", value: new Decimal(1_000_000) },\n ];\n\n for (const unit of compactUnits) {\n if (abs.gte(unit.value)) {\n return `${amount\n .div(unit.value)\n .toDecimalPlaces(4, Decimal.ROUND_DOWN)\n .toFixed()}${unit.suffix}`;\n }\n }\n\n const minDisplay = new Decimal(\"0.00000001\");\n if (amount.gt(0) && amount.lt(minDisplay)) {\n return `>${minDisplay.toFixed()}`;\n }\n\n return amount.toDecimalPlaces(8, Decimal.ROUND_DOWN).toFixed();\n};\n\nconst addThousandsSeparators = (value: string) => {\n const [integerPart, decimalPart] = value.split(\".\");\n const withSeparators = integerPart.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n return decimalPart === undefined\n ? withSeparators\n : `${withSeparators}.${decimalPart}`;\n};\n\nexport const formatUsdBalanceLabel = (value: unknown) => {\n const amount = parseTokenAmount(value) ?? new Decimal(0);\n const abs = amount.abs();\n\n if (amount.isZero()) return \"$0.00\";\n if (amount.gt(0) && amount.lt(0.01)) return \"<$0.01\";\n\n const compactUnits = [\n { suffix: \"T\", value: new Decimal(1_000_000_000_000) },\n { suffix: \"B\", value: new Decimal(1_000_000_000) },\n { suffix: \"M\", value: new Decimal(1_000_000) },\n ];\n\n for (const unit of compactUnits) {\n if (abs.gte(unit.value)) {\n return `$${amount\n .div(unit.value)\n .toDecimalPlaces(4, Decimal.ROUND_DOWN)\n .toFixed()}${unit.suffix}`;\n }\n }\n\n return `$${addThousandsSeparators(amount.toDecimalPlaces(2).toFixed(2))}`;\n};\n\nexport const formatSelectedTokenBalanceLabel = (\n token?: Pick,\n) => {\n if (!token) return \"\";\n const symbol = token.symbol || \"\";\n const formatted = formatTokenAmountDisplay(token.balance);\n return symbol ? `${formatted} ${symbol}` : formatted;\n};\n\nconst getSearchTerms = (query: string) =>\n query.toLowerCase().trim().split(/\\s+/).filter(Boolean);\n\nconst includesTerm = (value: string | undefined, term: string) =>\n (value ?? \"\").toLowerCase().includes(term);\n\nconst startsWithTerm = (value: string | undefined, term: string) =>\n (value ?? \"\").toLowerCase().startsWith(term);\n\nconst equalsTerm = (value: string | undefined, term: string) =>\n (value ?? \"\").toLowerCase() === term;\n\nexport const getTokenSearchRank = (\n token: Pick<\n SwapTokenOption,\n \"symbol\" | \"name\" | \"chainName\" | \"contractAddress\"\n >,\n query: string,\n) => {\n const terms = getSearchTerms(query);\n if (terms.length === 0) return null;\n\n let matchedTerms = 0;\n let symbolExactTerms = 0;\n let symbolPrefixTerms = 0;\n let symbolIncludeTerms = 0;\n let namePrefixTerms = 0;\n let tokenExactTerms = 0;\n let tokenPrefixTerms = 0;\n let tokenIncludeTerms = 0;\n let chainExactTerms = 0;\n let chainPrefixTerms = 0;\n let chainIncludeTerms = 0;\n let addressTerms = 0;\n\n for (const term of terms) {\n const symbolExact = equalsTerm(token.symbol, term);\n const symbolPrefix = symbolExact || startsWithTerm(token.symbol, term);\n const symbolInclude = symbolPrefix || includesTerm(token.symbol, term);\n const nameExact = equalsTerm(token.name, term);\n const namePrefix = nameExact || startsWithTerm(token.name, term);\n const nameInclude = namePrefix || includesTerm(token.name, term);\n const tokenExact = symbolExact || nameExact;\n const tokenPrefix = tokenExact || symbolPrefix || namePrefix;\n const tokenInclude = tokenPrefix || symbolInclude || nameInclude;\n const chainExact = equalsTerm(token.chainName, term);\n const chainPrefix = chainExact || startsWithTerm(token.chainName, term);\n const chainInclude = chainPrefix || includesTerm(token.chainName, term);\n const addressMatch = includesTerm(token.contractAddress, term);\n\n if (tokenInclude || chainInclude || addressMatch) matchedTerms += 1;\n if (symbolExact) symbolExactTerms += 1;\n if (symbolPrefix) symbolPrefixTerms += 1;\n if (symbolInclude) symbolIncludeTerms += 1;\n if (namePrefix) namePrefixTerms += 1;\n if (tokenExact) tokenExactTerms += 1;\n if (tokenPrefix) tokenPrefixTerms += 1;\n if (tokenInclude) tokenIncludeTerms += 1;\n if (chainExact) chainExactTerms += 1;\n if (chainPrefix) chainPrefixTerms += 1;\n if (chainInclude) chainIncludeTerms += 1;\n if (addressMatch) addressTerms += 1;\n }\n\n const allTermsMatched = matchedTerms === terms.length;\n if (!allTermsMatched) return null;\n\n if (\n terms.length > 1 &&\n chainIncludeTerms > 0 &&\n symbolIncludeTerms === 0 &&\n addressTerms === 0 &&\n namePrefixTerms === 0\n ) {\n return null;\n }\n\n const hasTokenMatch = tokenIncludeTerms > 0;\n const hasChainMatch = chainIncludeTerms > 0;\n const isTokenChainMatch = allTermsMatched && hasTokenMatch && hasChainMatch;\n\n let score = 20;\n if (isTokenChainMatch) {\n if (symbolExactTerms > 0 && chainExactTerms > 0) score = 0;\n else if (symbolExactTerms > 0 && chainPrefixTerms > 0) score = 1;\n else if (symbolExactTerms > 0 && chainIncludeTerms > 0) score = 2;\n else if (symbolPrefixTerms > 0 && chainIncludeTerms > 0) score = 3;\n else if (symbolIncludeTerms > 0 && chainIncludeTerms > 0) score = 4;\n else if (namePrefixTerms > 0 && chainIncludeTerms > 0) score = 5;\n else score = 6;\n } else if (symbolExactTerms > 0) score = 7;\n else if (symbolPrefixTerms > 0) score = 8;\n else if (symbolIncludeTerms > 0) score = 9;\n else if (tokenExactTerms > 0) score = 10;\n else if (tokenPrefixTerms > 0) score = 11;\n else if (tokenIncludeTerms > 0) score = 12;\n else if (chainExactTerms > 0) score = 13;\n else if (chainPrefixTerms > 0) score = 14;\n else if (chainIncludeTerms > 0) score = 15;\n else if (addressTerms > 0) score = 16;\n\n return {\n allTermsMatched,\n isTokenChainMatch,\n matchedTerms,\n score,\n tokenExactTerms,\n tokenIncludeTerms,\n };\n};\n\nconst isPrioritySearchMatch = (token: SwapTokenOption, query: string) => {\n const rank = getTokenSearchRank(token, query);\n return Boolean(\n rank &&\n (rank.isTokenChainMatch ||\n rank.tokenExactTerms > 0 ||\n rank.allTermsMatched),\n );\n};\n\nconst compareTokensBySearch = (\n a: SwapTokenOption,\n b: SwapTokenOption,\n query: string,\n) => {\n const aRank = getTokenSearchRank(a, query);\n const bRank = getTokenSearchRank(b, query);\n const aScore = aRank?.score ?? Number.MAX_SAFE_INTEGER;\n const bScore = bRank?.score ?? Number.MAX_SAFE_INTEGER;\n if (aScore !== bScore) return aScore - bScore;\n\n const aMatched = aRank?.matchedTerms ?? 0;\n const bMatched = bRank?.matchedTerms ?? 0;\n if (aMatched !== bMatched) return bMatched - aMatched;\n\n const aFiat = getTokenFiatValue(a);\n const bFiat = getTokenFiatValue(b);\n if (aFiat !== bFiat) return bFiat - aFiat;\n\n return `${a.symbol} ${a.chainName}`.localeCompare(\n `${b.symbol} ${b.chainName}`,\n );\n};\n\nfunction getUnifiedSymbol(token: Pick) {\n if (token.chainId && !UNIFIED_MAINNET_CHAIN_IDS.has(token.chainId)) {\n return null;\n }\n\n const symbol = normalizeTokenGroupSymbol(token.symbol);\n if (UNIFIED_USDC_SYMBOL_KEYS.has(symbol)) return \"USDC\" as const;\n if (UNIFIED_USDT_SYMBOL_KEYS.has(symbol)) return \"USDT\" as const;\n if (symbol === \"ETH\") return \"ETH\" as const;\n return null;\n}\n\nfunction sameTokenOption(a?: SwapTokenOption, b?: SwapTokenOption) {\n if (!a || !b) return false;\n if (a.isUnified || b.isUnified) {\n return Boolean(\n a.isUnified && b.isUnified && a.unifiedSymbol === b.unifiedSymbol,\n );\n }\n return (\n sameContractAddress(a.contractAddress, b.contractAddress) &&\n a.chainId === b.chainId\n );\n}\n\nfunction isSameTokenList(a: SwapTokenOption[], b: SwapTokenOption[]) {\n if (a.length !== b.length) return false;\n return a.every((tokenA) =>\n b.some((tokenB) => sameTokenOption(tokenA, tokenB)),\n );\n}\n\nfunction dedupeTokenOptions(tokens: SwapTokenOption[]) {\n return tokens.reduce((acc, token) => {\n if (!acc.some((item) => sameTokenOption(item, token))) {\n acc.push(token);\n }\n return acc;\n }, []);\n}\n\nfunction mergeTokenOptions(\n base: SwapTokenOption[],\n additions: SwapTokenOption[],\n) {\n return dedupeTokenOptions([...base, ...additions]);\n}\n\nfunction removeTokenOptions(\n base: SwapTokenOption[],\n removals: SwapTokenOption[],\n) {\n return base.filter(\n (token) => !removals.some((removal) => sameTokenOption(token, removal)),\n );\n}\n\nfunction isNativeLikeAddress(address?: string) {\n const normalized = (address ?? \"\").toLowerCase();\n return (\n normalized === \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\" ||\n normalized === \"0x0000000000000000000000000000000000000000\"\n );\n}\n\nfunction addressTail(address?: string) {\n const normalized = (address ?? \"\").toLowerCase();\n if (!normalized.startsWith(\"0x\")) return normalized;\n return normalized.slice(-40);\n}\n\nfunction sameContractAddress(a?: string, b?: string) {\n const normalizedA = (a ?? \"\").toLowerCase();\n const normalizedB = (b ?? \"\").toLowerCase();\n if (!normalizedA || !normalizedB) return normalizedA === normalizedB;\n if (normalizedA === normalizedB) return true;\n if (isNativeLikeAddress(normalizedA) && isNativeLikeAddress(normalizedB)) {\n return true;\n }\n return addressTail(normalizedA) === addressTail(normalizedB);\n}\n\nexport function SwapAssetSelector({\n title,\n swapBalance,\n swapSupportedChains,\n staticOptions,\n onSelect,\n onBack,\n isMulti,\n selectedTokens = [],\n editingAssetIndex = null,\n onToggle,\n onClearSelection,\n onDone,\n allowUnified = false,\n preserveSelectedBelowMinimum = false,\n showBelowMinimumInline = false,\n allowSelectedTokenRemoval = false,\n hideCustomTab = false,\n autoSelectFilterTabs = false,\n initialFilterTab = \"all\",\n filterTabBehavior = \"select-all\",\n onFilterTabSelect,\n lockedTokens = [],\n onSelectionChange,\n requiredUsd,\n}: SwapAssetSelectorProps) {\n const sdkSwapSupportedChainIds = useMemo(\n () => getSdkSwapSupportedChainIds(swapSupportedChains),\n [swapSupportedChains],\n );\n const selectorRef = useRef(null);\n const listRef = useRef(null);\n const chainCloseTimerRef = useRef | null>(null);\n const [portalRoot, setPortalRoot] = useState(null);\n const [query, setQuery] = useState(\"\");\n const normalizedInitialFilterTab =\n hideCustomTab && initialFilterTab === \"custom\" ? \"all\" : initialFilterTab;\n const [activeTab, setActiveTab] = useState(\n normalizedInitialFilterTab,\n );\n const [showBelowMin, setShowBelowMin] = useState(false);\n const [isSearchFocused, setIsSearchFocused] = useState(false);\n const [showChainSelector, setShowChainSelector] = useState(false);\n const [isChainSelectorClosing, setIsChainSelectorClosing] = useState(false);\n const [chainQuery, setChainQuery] = useState(\"\");\n const [selectedChainFilter, setSelectedChainFilter] = useState(\n null,\n );\n const [isChainSearchFocused, setIsChainSearchFocused] = useState(false);\n const stableListHeightRef = useRef(0);\n const pendingSelectionScrollTopRef = useRef(null);\n const skipNextSelectionScrollResetRef = useRef(false);\n const [stableListHeight, setStableListHeight] = useState(null);\n const lockedSelectedTokens = useMemo(\n () => dedupeTokenOptions(lockedTokens),\n [lockedTokens],\n );\n const isLockedToken = useCallback(\n (token: SwapTokenOption) =>\n lockedSelectedTokens.some((locked) => sameTokenOption(locked, token)),\n [lockedSelectedTokens],\n );\n const [draftSelectedTokens, setDraftSelectedTokens] = useState<\n SwapTokenOption[]\n >(() => mergeTokenOptions(selectedTokens, lockedSelectedTokens));\n useEffect(() => {\n if (!isMulti) return;\n setDraftSelectedTokens(\n mergeTokenOptions(selectedTokens, lockedSelectedTokens),\n );\n }, [isMulti, lockedSelectedTokens, selectedTokens]);\n const activeSelectedTokens = isMulti ? draftSelectedTokens : selectedTokens;\n const emitSelectionChange = useCallback(\n (tokens: SwapTokenOption[]) => {\n const next = mergeTokenOptions(tokens, lockedSelectedTokens);\n if (isMulti) {\n setDraftSelectedTokens(next);\n }\n onSelectionChange?.(next);\n },\n [isMulti, lockedSelectedTokens, onSelectionChange],\n );\n const visibleFilterTabs = useMemo(\n () =>\n hideCustomTab\n ? FILTER_TABS.filter((tab) => tab.key !== \"custom\")\n : FILTER_TABS,\n [hideCustomTab],\n );\n\n useEffect(() => {\n if (hideCustomTab && activeTab === \"custom\") {\n setActiveTab(\"all\");\n }\n }, [activeTab, hideCustomTab]);\n\n useEffect(() => {\n setActiveTab((current) =>\n current === normalizedInitialFilterTab\n ? current\n : normalizedInitialFilterTab,\n );\n }, [normalizedInitialFilterTab]);\n\n useEffect(() => {\n if (skipNextSelectionScrollResetRef.current) {\n skipNextSelectionScrollResetRef.current = false;\n return;\n }\n if (listRef.current) {\n listRef.current.scrollTop = 0;\n }\n }, [query, activeTab, selectedChainFilter]);\n\n useLayoutEffect(() => {\n const pendingScrollTop = pendingSelectionScrollTopRef.current;\n if (pendingScrollTop === null) return;\n\n if (listRef.current) {\n listRef.current.scrollTop = pendingScrollTop;\n }\n pendingSelectionScrollTopRef.current = null;\n }, [draftSelectedTokens]);\n\n const allTokens = useMemo(() => {\n const isSwapSupportedToken = (token: SwapTokenOption) =>\n isSwapSupportedBySdkChainList(token.chainId, swapSupportedChains);\n\n const baseTokens = staticOptions\n ? staticOptions.filter(isSwapSupportedToken)\n : swapBalance\n ? deriveTokenOptions(swapBalance, swapSupportedChains)\n : [];\n\n if (!preserveSelectedBelowMinimum && lockedSelectedTokens.length === 0) {\n return sortTokensByUsdBalance(baseTokens);\n }\n\n const merged = [...baseTokens];\n const selectedSourceTokens = [\n ...activeSelectedTokens,\n ...lockedSelectedTokens,\n ].flatMap((token) =>\n token.isUnified && token.sourceTokens?.length\n ? token.sourceTokens\n : [token],\n );\n\n for (const selectedToken of selectedSourceTokens) {\n const alreadyPresent = merged.some((token) =>\n sameTokenOption(token, selectedToken),\n );\n if (!alreadyPresent) {\n merged.push(selectedToken);\n }\n }\n\n return sortTokensByUsdBalance(merged.filter(isSwapSupportedToken));\n }, [\n lockedSelectedTokens,\n preserveSelectedBelowMinimum,\n activeSelectedTokens,\n swapBalance,\n swapSupportedChains,\n staticOptions,\n ]);\n\n const getFilterTabTokens = useCallback(\n (tab: FilterTab) => {\n let result = allTokens;\n if (selectedChainFilter !== null) {\n result = result.filter(\n (token) => token.chainId === selectedChainFilter,\n );\n }\n if (tab === \"native\") result = result.filter(isNativeToken);\n else if (tab === \"stables\") {\n result = result.filter(isStableToken);\n }\n\n if (showBelowMinimumInline) {\n return mergeTokenOptions(result, lockedSelectedTokens);\n }\n\n return mergeTokenOptions(\n result.filter(\n (token) => getTokenFiatValue(token) >= MIN_FIAT_THRESHOLD,\n ),\n lockedSelectedTokens,\n );\n },\n [allTokens, lockedSelectedTokens, selectedChainFilter],\n );\n\n const selectionMatchesFilterTab = useCallback(\n (tab: FilterTab) => {\n if (tab === \"custom\") return true;\n const expected = getFilterTabTokens(tab);\n const selected = mergeTokenOptions(\n activeSelectedTokens,\n lockedSelectedTokens,\n );\n return (\n selected.length === expected.length &&\n selected.every((token) =>\n expected.some((expectedToken) =>\n sameTokenOption(expectedToken, token),\n ),\n )\n );\n },\n [activeSelectedTokens, getFilterTabTokens, lockedSelectedTokens],\n );\n\n useEffect(() => {\n if (\n !autoSelectFilterTabs ||\n filterTabBehavior === \"source-pool\" ||\n !isMulti ||\n activeTab === \"custom\"\n )\n return;\n if (activeSelectedTokens.length === 0 && lockedSelectedTokens.length === 0)\n return;\n if (!selectionMatchesFilterTab(activeTab)) {\n setActiveTab(\"custom\");\n }\n }, [\n activeTab,\n activeSelectedTokens.length,\n autoSelectFilterTabs,\n filterTabBehavior,\n isMulti,\n lockedSelectedTokens.length,\n selectionMatchesFilterTab,\n ]);\n\n /* Search + tab + chain filter */\n const filtered = useMemo(() => {\n let result = allTokens;\n if (selectedChainFilter !== null) {\n result = result.filter((t) => t.chainId === selectedChainFilter);\n }\n if (query.trim()) {\n result = result\n .filter((t) => getTokenSearchRank(t, query) !== null)\n .sort((a, b) => compareTokensBySearch(a, b, query));\n }\n if (activeTab === \"native\") result = result.filter(isNativeToken);\n else if (activeTab === \"stables\") result = result.filter(isStableToken);\n else if (activeTab === \"custom\" && !autoSelectFilterTabs)\n result = result.filter((t) => !isNativeToken(t) && !isStableToken(t));\n return result;\n }, [activeTab, allTokens, autoSelectFilterTabs, query, selectedChainFilter]);\n\n const isTokenSelectedForVisibility = useCallback(\n (token: SwapTokenOption) => {\n if (isLockedToken(token)) return true;\n if (!preserveSelectedBelowMinimum) return false;\n\n return activeSelectedTokens.some(\n (selected) =>\n sameTokenOption(selected, token) ||\n Boolean(\n selected.isUnified &&\n selected.sourceTokens?.some((source) =>\n sameTokenOption(source, token),\n ),\n ),\n );\n },\n [activeSelectedTokens, isLockedToken, preserveSelectedBelowMinimum],\n );\n\n const isUnifiedSelectedForVisibility = useCallback(\n (symbol: string) =>\n preserveSelectedBelowMinimum &&\n activeSelectedTokens.some(\n (selected) => selected.isUnified && selected.unifiedSymbol === symbol,\n ),\n [activeSelectedTokens, preserveSelectedBelowMinimum],\n );\n\n /* Split into above/below minimum */\n const { aboveMin, belowMin } = useMemo(() => {\n const above: SwapTokenOption[] = [];\n const below: SwapTokenOption[] = [];\n for (const t of filtered) {\n const fiat = getTokenFiatValue(t);\n if (\n showBelowMinimumInline ||\n fiat >= MIN_FIAT_THRESHOLD ||\n isTokenSelectedForVisibility(t) ||\n isPrioritySearchMatch(t, query)\n )\n above.push(t);\n else below.push(t);\n }\n return { aboveMin: above, belowMin: below };\n }, [filtered, isTokenSelectedForVisibility, query, showBelowMinimumInline]);\n\n /* Group by symbol */\n const groupedFiltered = useMemo(() => {\n const groups: Record = {};\n for (const token of filtered) {\n const unifiedSym = allowUnified ? getUnifiedSymbol(token) : null;\n if (\n !showBelowMinimumInline &&\n unifiedSym &&\n getTokenFiatValue(token) < MIN_FIAT_THRESHOLD &&\n !isTokenSelectedForVisibility(token)\n ) {\n continue;\n }\n const key = unifiedSym ?? `${token.contractAddress}-${token.chainId}`;\n if (!groups[key]) groups[key] = [];\n groups[key].push(token);\n }\n return Object.values(groups)\n .map((group) => {\n const sortedGroup = sortTokensByUsdBalance(group);\n let totalFiatVal = 0;\n let totalBalVal = 0;\n for (const t of sortedGroup) {\n const fiatVal = getTokenFiatValue(t);\n totalFiatVal += isNaN(fiatVal) || !isFinite(fiatVal) ? 0 : fiatVal;\n const balStr = String(t.balance ?? \"\").replace(/[^0-9.]/g, \"\");\n const balVal = Number(balStr || 0);\n totalBalVal += isNaN(balVal) || !isFinite(balVal) ? 0 : balVal;\n }\n const unifiedSym = allowUnified\n ? getUnifiedSymbol(sortedGroup[0])\n : null;\n const symbol = unifiedSym ?? sortedGroup[0].symbol;\n const isStable = [\"USDC\", \"USDT\", \"DAI\", \"USDM\", \"CTUSD\"].includes(\n symbol.toUpperCase(),\n );\n const maxDigits = isStable ? 2 : 6;\n const formattedBal = totalBalVal.toLocaleString(undefined, {\n minimumFractionDigits: 0,\n maximumFractionDigits: maxDigits,\n });\n\n return {\n symbol,\n logo: sortedGroup[0].logo,\n totalFiat: totalFiatVal,\n totalFiatStr: `$${totalFiatVal.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 })}`,\n totalBalStr: `${formattedBal} ${symbol}`,\n totalBalRaw: totalBalVal,\n tokens: sortedGroup,\n isUnifiedCandidate: Boolean(unifiedSym && group.length > 1),\n };\n })\n .filter((group) => {\n const hasSelectedToken = group.tokens.some(\n isTokenSelectedForVisibility,\n );\n const hasSelectedUnified = isUnifiedSelectedForVisibility(group.symbol);\n const hasPrioritySearchMatch = group.tokens.some((token) =>\n isPrioritySearchMatch(token, query),\n );\n if (showBelowMinimumInline) {\n return true;\n }\n if (group.isUnifiedCandidate) {\n return (\n group.totalFiat >= MIN_FIAT_THRESHOLD ||\n hasSelectedToken ||\n hasSelectedUnified ||\n hasPrioritySearchMatch\n );\n }\n return group.tokens.some(\n (token) =>\n getTokenFiatValue(token) >= MIN_FIAT_THRESHOLD ||\n isTokenSelectedForVisibility(token) ||\n isPrioritySearchMatch(token, query),\n );\n })\n .sort((a, b) => {\n if (query.trim()) {\n const aScore = Math.min(\n ...a.tokens.map(\n (token) =>\n getTokenSearchRank(token, query)?.score ??\n Number.MAX_SAFE_INTEGER,\n ),\n );\n const bScore = Math.min(\n ...b.tokens.map(\n (token) =>\n getTokenSearchRank(token, query)?.score ??\n Number.MAX_SAFE_INTEGER,\n ),\n );\n if (aScore !== bScore) return aScore - bScore;\n }\n return b.totalFiat - a.totalFiat;\n });\n }, [\n filtered,\n allowUnified,\n isTokenSelectedForVisibility,\n isUnifiedSelectedForVisibility,\n query,\n ]);\n\n const [expandedGroups, setExpandedGroups] = useState>(new Set());\n\n const toggleGroup = (symbol: string, e: React.MouseEvent) => {\n e.stopPropagation();\n setExpandedGroups((prev) => {\n const next = new Set(prev);\n if (next.has(symbol)) next.delete(symbol);\n else next.add(symbol);\n return next;\n });\n };\n\n const isTokenSelectedInOtherSlot = (token: SwapTokenOption) =>\n !allowSelectedTokenRemoval &&\n activeSelectedTokens.some(\n (st, idx) => idx !== editingAssetIndex && sameTokenOption(st, token),\n );\n\n const isTokenSelectedInCurrentSlot = (token: SwapTokenOption) => {\n if (isMulti) {\n return activeSelectedTokens.some(\n (st) =>\n sameTokenOption(st, token) ||\n Boolean(\n st.isUnified &&\n st.sourceTokens?.some((source) => sameTokenOption(source, token)),\n ),\n );\n }\n if (allowSelectedTokenRemoval) {\n return activeSelectedTokens.some(\n (st) =>\n sameTokenOption(st, token) ||\n Boolean(\n st.isUnified &&\n st.sourceTokens?.some((source) => sameTokenOption(source, token)),\n ),\n );\n }\n if (editingAssetIndex === null) return false;\n const st = activeSelectedTokens[editingAssetIndex];\n return sameTokenOption(st, token);\n };\n\n const isGroupUnifiedSelectedInOtherSlot = (\n group: (typeof groupedFiltered)[0],\n ) => {\n if (allowSelectedTokenRemoval) return false;\n const relevantTokens = isMulti\n ? activeSelectedTokens\n : activeSelectedTokens.filter((_, idx) => idx !== editingAssetIndex);\n return relevantTokens.some(\n (st) => st.isUnified && st.unifiedSymbol === group.symbol,\n );\n };\n\n const isGroupUnifiedSelectedInCurrentSlot = (\n group: (typeof groupedFiltered)[0],\n ) => {\n if (isMulti) {\n return activeSelectedTokens.some(\n (st) => st.isUnified && st.unifiedSymbol === group.symbol,\n );\n }\n if (editingAssetIndex === null) return false;\n const st = activeSelectedTokens[editingAssetIndex];\n return Boolean(st?.isUnified && st.unifiedSymbol === group.symbol);\n };\n\n const isAnyTokenInGroupSelectedInOtherSlot = (\n group: (typeof groupedFiltered)[0],\n ) => {\n if (allowSelectedTokenRemoval) return false;\n const relevantTokens = isMulti\n ? activeSelectedTokens\n : activeSelectedTokens.filter((_, idx) => idx !== editingAssetIndex);\n return relevantTokens.some(\n (st) =>\n group.tokens.some((gt) => sameTokenOption(gt, st)) ||\n (st.isUnified && st.unifiedSymbol === group.symbol),\n );\n };\n\n const selectorRows = useMemo(() => {\n if (isMulti) {\n return groupedFiltered.map((group) => ({\n fiat: group.totalFiat,\n group,\n key: `group:${group.symbol}`,\n kind: \"group\" as const,\n label: group.symbol,\n searchScore: query.trim()\n ? Math.min(\n ...group.tokens.map(\n (token) =>\n getTokenSearchRank(token, query)?.score ??\n Number.MAX_SAFE_INTEGER,\n ),\n )\n : 0,\n }));\n }\n\n const rows: Array<\n | {\n fiat: number;\n group: (typeof groupedFiltered)[number];\n key: string;\n kind: \"group\";\n label: string;\n searchScore: number;\n }\n | {\n fiat: number;\n key: string;\n kind: \"token\";\n label: string;\n searchScore: number;\n token: SwapTokenOption;\n }\n > = [];\n const hasQuery = Boolean(query.trim());\n const getSearchScore = (token: SwapTokenOption) =>\n hasQuery\n ? (getTokenSearchRank(token, query)?.score ?? Number.MAX_SAFE_INTEGER)\n : 0;\n const isVisibleTokenRow = (token: SwapTokenOption) =>\n showBelowMinimumInline ||\n getTokenFiatValue(token) >= MIN_FIAT_THRESHOLD ||\n isTokenSelectedForVisibility(token) ||\n isPrioritySearchMatch(token, query);\n\n for (const group of groupedFiltered) {\n if (!group.isUnifiedCandidate) {\n for (const token of group.tokens) {\n if (!isVisibleTokenRow(token)) continue;\n rows.push({\n fiat: getTokenFiatValue(token),\n key: `token:${token.chainId}:${token.contractAddress}`,\n kind: \"token\",\n label: `${token.symbol} ${token.chainName}`,\n searchScore: getSearchScore(token),\n token,\n });\n }\n continue;\n }\n\n const unifiedSelectedInOther = isGroupUnifiedSelectedInOtherSlot(group);\n const unifiedSelectedInCurrent =\n isGroupUnifiedSelectedInCurrentSlot(group);\n const anyIndividualSelectedInOther =\n isAnyTokenInGroupSelectedInOtherSlot(group);\n const anyIndividualSelectedInCurrent = group.tokens.some(\n isTokenSelectedInCurrentSlot,\n );\n const shouldHideUnifiedRow =\n anyIndividualSelectedInOther ||\n anyIndividualSelectedInCurrent ||\n (group.totalFiat < MIN_FIAT_THRESHOLD &&\n !isUnifiedSelectedForVisibility(group.symbol));\n const shouldHideIndividualRows =\n unifiedSelectedInOther || unifiedSelectedInCurrent;\n\n if (!unifiedSelectedInOther && !shouldHideUnifiedRow) {\n rows.push({\n fiat: group.totalFiat,\n group,\n key: `group:${group.symbol}`,\n kind: \"group\",\n label: group.symbol,\n searchScore: hasQuery\n ? Math.min(\n ...group.tokens.map(\n (token) =>\n getTokenSearchRank(token, query)?.score ??\n Number.MAX_SAFE_INTEGER,\n ),\n )\n : 0,\n });\n }\n\n if (!shouldHideIndividualRows) {\n for (const token of group.tokens) {\n if (!isVisibleTokenRow(token)) continue;\n rows.push({\n fiat: getTokenFiatValue(token),\n key: `token:${token.chainId}:${token.contractAddress}`,\n kind: \"token\",\n label: `${token.symbol} ${token.chainName}`,\n searchScore: getSearchScore(token),\n token,\n });\n }\n }\n }\n\n return rows.sort((a, b) => {\n if (hasQuery && a.searchScore !== b.searchScore) {\n return a.searchScore - b.searchScore;\n }\n if (a.fiat !== b.fiat) return b.fiat - a.fiat;\n if (a.kind !== b.kind) return a.kind === \"group\" ? -1 : 1;\n return a.label.localeCompare(b.label);\n });\n }, [\n groupedFiltered,\n isMulti,\n isGroupUnifiedSelectedInCurrentSlot,\n isGroupUnifiedSelectedInOtherSlot,\n isAnyTokenInGroupSelectedInOtherSlot,\n isTokenSelectedInCurrentSlot,\n isTokenSelectedForVisibility,\n isUnifiedSelectedForVisibility,\n query,\n showBelowMinimumInline,\n ]);\n\n const handleFilterTabClick = (tab: FilterTab) => {\n setActiveTab(tab);\n if (autoSelectFilterTabs && isMulti && tab !== \"custom\") {\n if (tab === \"all\" && onFilterTabSelect) {\n onFilterTabSelect(tab);\n return;\n }\n if (filterTabBehavior === \"source-pool\") {\n onFilterTabSelect?.(tab);\n return;\n }\n if (!onSelectionChange) return;\n emitSelectionChange(getFilterTabTokens(tab));\n }\n };\n\n const handleClearSelection = () => {\n if (isMulti && onSelectionChange) {\n setActiveTab(\"custom\");\n emitSelectionChange([]);\n return;\n }\n onClearSelection?.();\n };\n\n const handleMultiTokenToggle = (token: SwapTokenOption) => {\n if (!autoSelectFilterTabs || !isMulti || !onSelectionChange) {\n onToggle?.(token);\n return;\n }\n\n pendingSelectionScrollTopRef.current = listRef.current?.scrollTop ?? null;\n skipNextSelectionScrollResetRef.current = true;\n setActiveTab(\"custom\");\n const current = mergeTokenOptions(\n activeSelectedTokens,\n lockedSelectedTokens,\n );\n const targets =\n token.isUnified && token.sourceTokens?.length\n ? token.sourceTokens\n : [token];\n const unlockedTargets = targets.filter((target) => !isLockedToken(target));\n if (unlockedTargets.length === 0) return;\n\n const allTargetsSelected = unlockedTargets.every((target) =>\n current.some((item) => sameTokenOption(item, target)),\n );\n const next = allTargetsSelected\n ? removeTokenOptions(current, unlockedTargets)\n : mergeTokenOptions(current, unlockedTargets);\n emitSelectionChange(next);\n };\n\n /* ── Render a single-chain token row ── */\n const renderTokenRow = (\n token: SwapTokenOption,\n indent = false,\n isDisabledByUnified = false,\n ) => {\n const selectedInOther = !isMulti && isTokenSelectedInOtherSlot(token);\n if (selectedInOther) return null;\n\n const selectedInCurrent = isTokenSelectedInCurrentSlot(token);\n const locked = isLockedToken(token);\n const disabled = isDisabledByUnified || locked;\n return (\n {\n if (disabled) return;\n if (isMulti) {\n handleMultiTokenToggle(token);\n } else if (\n allowSelectedTokenRemoval &&\n selectedInCurrent &&\n onToggle\n ) {\n onToggle(token);\n } else onSelect(token);\n }}\n style={{\n width: \"100%\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"space-between\",\n padding: \"10px 14px\",\n paddingLeft: indent ? \"36px\" : \"14px\",\n backgroundColor: \"transparent\",\n border: \"none\",\n cursor: disabled ? \"not-allowed\" : \"pointer\",\n borderBottom: \"1px solid #F0F0EF\",\n boxSizing: \"border-box\",\n opacity: isDisabledByUnified ? 0.5 : 1,\n }}\n >\n
\n \n
\n \n
\n \n \n {token.symbol}\n \n {token.chainName && (\n
\n \n \n {token.chainName}\n \n
\n )}\n
\n \n \n \n {formatBalanceWithSymbol(token)}\n \n \n ≈ {token.balanceInFiat}\n \n \n \n );\n };\n\n /* ── Render a unified (multi-chain) group row ── */\n const renderGroupRow = (\n group: (typeof groupedFiltered)[0],\n includeIndividualRows = true,\n ) => {\n if (!group.isUnifiedCandidate) {\n return group.tokens\n .filter(\n (token) =>\n getTokenFiatValue(token) >= MIN_FIAT_THRESHOLD ||\n isTokenSelectedForVisibility(token) ||\n isPrioritySearchMatch(token, query),\n )\n .map((token) => renderTokenRow(token));\n }\n\n const unifiedSelectedInOther =\n !isMulti && isGroupUnifiedSelectedInOtherSlot(group);\n if (unifiedSelectedInOther) return null;\n\n const individualTokens = group.tokens.filter(\n (token) =>\n getTokenFiatValue(token) >= MIN_FIAT_THRESHOLD ||\n isTokenSelectedForVisibility(token) ||\n isPrioritySearchMatch(token, query),\n );\n const hasVisibleUnifiedRow =\n (group.totalFiat >= MIN_FIAT_THRESHOLD ||\n isUnifiedSelectedForVisibility(group.symbol)) &&\n !unifiedSelectedInOther;\n const visibleTokensCount = individualTokens.filter(\n (t) => !isTokenSelectedInOtherSlot(t),\n ).length;\n if (!hasVisibleUnifiedRow && visibleTokensCount === 0) return null;\n\n const isExpanded = expandedGroups.has(group.symbol);\n const unifiedSelectedInCurrent = isGroupUnifiedSelectedInCurrentSlot(group);\n const anyIndividualSelectedInOther =\n isAnyTokenInGroupSelectedInOtherSlot(group);\n const anyIndividualSelectedInCurrent = group.tokens.some(\n isTokenSelectedInCurrentSlot,\n );\n const selectedChildCount = group.tokens.filter(\n isTokenSelectedInCurrentSlot,\n ).length;\n const areAllChildrenSelected =\n group.tokens.length > 0 && selectedChildCount === group.tokens.length;\n const isPartiallySelected =\n selectedChildCount > 0 && selectedChildCount < group.tokens.length;\n const shouldHideUnifiedRow =\n !isMulti &&\n (anyIndividualSelectedInOther ||\n anyIndividualSelectedInCurrent ||\n (group.totalFiat < MIN_FIAT_THRESHOLD &&\n !isUnifiedSelectedForVisibility(group.symbol)));\n const shouldHideIndividualRows =\n !isMulti && (unifiedSelectedInOther || unifiedSelectedInCurrent);\n const unifiedToken: SwapTokenOption = {\n ...group.tokens[0],\n balance: String(group.totalBalRaw),\n balanceInFiat: group.totalFiatStr,\n chainId: undefined,\n chainName: \"All Chains\",\n chainLogo: \"/nexus-widget/all-chains.png\",\n contractAddress: `${group.symbol}-UNIFIED`,\n isUnified: true,\n name: group.symbol,\n sourceTokens: group.tokens,\n symbol: group.symbol,\n unifiedSymbol: group.symbol as \"USDC\" | \"USDT\" | \"ETH\",\n };\n\n return (\n \n {!shouldHideUnifiedRow && (\n {\n if (isMulti) {\n toggleGroup(group.symbol, e);\n return;\n }\n onSelect(unifiedToken);\n }}\n style={{\n width: \"100%\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"space-between\",\n padding: \"10px 14px\",\n backgroundColor: \"transparent\",\n border: \"none\",\n cursor: \"pointer\",\n borderBottom: \"1px solid #F0F0EF\",\n boxSizing: \"border-box\",\n }}\n >\n
\n {\n e.stopPropagation();\n if (isMulti) handleMultiTokenToggle(unifiedToken);\n else onSelect(unifiedToken);\n }}\n style={{ cursor: \"pointer\" }}\n >\n \n
\n \n \n \n \n
\n \n {group.symbol}\n \n \n UNIFIED\n \n
\n \n \n \n
\n \n \n {group.totalBalStr}\n \n \n ≈ {group.totalFiatStr}\n \n
\n \n \n )}\n {isMulti ? (\n \n
\n {group.tokens.map((token) => renderTokenRow(token, true, false))}\n
\n \n ) : (\n includeIndividualRows &&\n !shouldHideIndividualRows &&\n individualTokens.map((token) => renderTokenRow(token))\n )}\n \n );\n };\n\n const isLoading = !staticOptions && swapBalance === null;\n const selectedAssetCount = activeSelectedTokens.length;\n const requiredUsdAmount = parseTokenAmount(requiredUsd);\n const shouldCountSelectedUsd = (token: SwapTokenOption, value: Decimal) =>\n value.gt(0) && (isLockedToken(token) || value.gte(MIN_FIAT_THRESHOLD));\n const selectedUsdAmount = activeSelectedTokens.reduce((sum, token) => {\n if (token.isUnified && token.sourceTokens?.length) {\n return sum.plus(\n token.sourceTokens.reduce((sourceSum, source) => {\n const value =\n parseTokenAmount(source.balanceInFiat) ?? new Decimal(0);\n return shouldCountSelectedUsd(source, value)\n ? sourceSum.plus(value)\n : sourceSum;\n }, new Decimal(0)),\n );\n }\n const value = parseTokenAmount(token.balanceInFiat) ?? new Decimal(0);\n return shouldCountSelectedUsd(token, value) ? sum.plus(value) : sum;\n }, new Decimal(0));\n const selectionDeficitUsdAmount =\n requiredUsdAmount && selectedUsdAmount.lt(requiredUsdAmount)\n ? requiredUsdAmount.minus(selectedUsdAmount)\n : new Decimal(0);\n const hasSelectionShortfall = Boolean(\n requiredUsdAmount &&\n requiredUsdAmount.gt(0) &&\n selectionDeficitUsdAmount.gt(0),\n );\n const shouldShowSelectionProgress = Boolean(\n isMulti && hasSelectionShortfall,\n );\n const selectionProgressPercent =\n shouldShowSelectionProgress && requiredUsdAmount\n ? Decimal.min(\n 100,\n selectedUsdAmount.div(requiredUsdAmount).mul(100),\n ).toNumber()\n : 0;\n const subtitle = isMulti\n ? `${selectedAssetCount} asset${selectedAssetCount === 1 ? \"\" : \"s\"} selected`\n : \"\";\n\n useEffect(() => {\n setPortalRoot(\n selectorRef.current?.closest(\n \"[data-nexus-widget-root]\",\n ) as HTMLElement | null,\n );\n }, []);\n\n useEffect(() => {\n return () => {\n if (chainCloseTimerRef.current) {\n clearTimeout(chainCloseTimerRef.current);\n }\n };\n }, []);\n\n const openChainSelector = () => {\n if (chainCloseTimerRef.current) {\n clearTimeout(chainCloseTimerRef.current);\n chainCloseTimerRef.current = null;\n }\n setChainQuery(\"\");\n setIsChainSelectorClosing(false);\n setShowChainSelector(true);\n };\n\n const closeChainSelector = () => {\n if (chainCloseTimerRef.current) {\n clearTimeout(chainCloseTimerRef.current);\n }\n setIsChainSelectorClosing(true);\n chainCloseTimerRef.current = setTimeout(() => {\n setShowChainSelector(false);\n setIsChainSelectorClosing(false);\n chainCloseTimerRef.current = null;\n }, CHAIN_SELECTOR_CLOSE_MS);\n };\n\n const chainOptions = useMemo(() => {\n const options = new Map();\n\n for (const chain of swapSupportedChains ?? []) {\n if (\n !sdkSwapSupportedChainIds &&\n !SWAP_CHAIN_DISPLAY_ORDER_SET.has(chain.id)\n ) {\n continue;\n }\n if (!isSwapSupportedBySdkChainList(chain.id, swapSupportedChains)) {\n continue;\n }\n options.set(chain.id, {\n contractAddress: \"\",\n symbol: \"\",\n name: getShortChainName(chain.id, chain.name),\n decimals: 18,\n balance: \"0\",\n balanceInFiat: \"$0.00\",\n chainId: chain.id,\n chainName: getShortChainName(chain.id, chain.name),\n chainLogo: CHAIN_METADATA[chain.id]?.logo ?? chain.logo,\n });\n }\n\n for (const token of allTokens) {\n if (\n !token.chainId ||\n (!sdkSwapSupportedChainIds &&\n !SWAP_CHAIN_DISPLAY_ORDER_SET.has(token.chainId)) ||\n !isSwapSupportedBySdkChainList(token.chainId, swapSupportedChains)\n ) {\n continue;\n }\n if (!options.has(token.chainId)) {\n options.set(token.chainId, token);\n }\n }\n\n return Array.from(options.values()).sort(compareChainsBySwapDisplayOrder);\n }, [allTokens, sdkSwapSupportedChainIds, swapSupportedChains]);\n\n const selectedChainToken =\n selectedChainFilter === null\n ? undefined\n : chainOptions.find((token) => token.chainId === selectedChainFilter);\n const selectedChainLabel =\n selectedChainFilter === null\n ? \"All chains\"\n : selectedChainToken?.chainName || \"Chain\";\n\n const handleDone = () => {\n if (hasSelectionShortfall) return;\n onDone?.(\n isMulti\n ? mergeTokenOptions(draftSelectedTokens, lockedSelectedTokens)\n : undefined,\n );\n };\n\n return (\n \n {/* Drawer Handle */}\n \n \n \n\n {/* Header */}\n \n \n \n \n \n \n {title}\n \n {subtitle && (\n \n {subtitle}\n \n )}\n \n {isMulti &&\n selectedAssetCount > 0 &&\n (onClearSelection || onSelectionChange) && (\n \n Deselect all\n \n )}\n \n\n {/* Search */}\n
\n \n \n setIsSearchFocused(false)}\n onChange={(e) => setQuery(e.target.value)}\n onFocus={() => setIsSearchFocused(true)}\n placeholder=\"Search token, chain or address\"\n style={{\n flex: 1,\n backgroundColor: \"transparent\",\n border: \"none\",\n outline: \"none\",\n fontFamily: '\"Geist\", system-ui, sans-serif',\n fontSize: 14,\n color: \"#161615\",\n minWidth: 0,\n }}\n value={query}\n />\n {query && (\n setQuery(\"\")}\n style={{\n background: \"none\",\n border: \"none\",\n cursor: \"pointer\",\n padding: 0,\n }}\n >\n \n \n )}\n {/* Chain Selector Badge */}\n \n {selectedChainFilter === null ? (\n \n ) : (\n \n )}\n \n {selectedChainLabel}\n \n \n \n
\n \n\n {/* Filter tabs */}\n \n {visibleFilterTabs.map((tab) => (\n handleFilterTabClick(tab.key)}\n disabled={tab.key === \"custom\" && autoSelectFilterTabs}\n style={{\n flex: 1,\n padding: \"6px 0\",\n backgroundColor:\n activeTab === tab.key ? \"#FFFFFE\" : \"transparent\",\n border: \"none\",\n borderRadius: 6,\n cursor: \"pointer\",\n fontFamily: '\"Geist\", system-ui, sans-serif',\n fontSize: 13,\n fontWeight: 500,\n color: activeTab === tab.key ? \"#161615\" : \"#848483\",\n boxShadow:\n activeTab === tab.key ? \"0px 1px 2px rgba(0,0,0,0.05)\" : \"none\",\n transition: \"all 0.15s\",\n }}\n >\n {autoSelectFilterTabs && tab.key === \"all\" ? \"Any\" : tab.label}\n \n ))}\n \n\n {/* Token list */}\n \n {isLoading ? (\n \n \n \n Loading assets…\n

\n \n ) : aboveMin.length === 0 && belowMin.length === 0 ? (\n \n No tokens found\n

\n ) : (\n
\n {groupedFiltered.length > 0 && (\n \n {selectorRows.map((row) =>\n row.kind === \"group\"\n ? renderGroupRow(row.group, isMulti)\n : renderTokenRow(row.token),\n )}\n
\n )}\n\n {belowMin.length > 0 && (\n \n setShowBelowMin((v) => !v)}\n style={{\n width: \"100%\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"space-between\",\n padding: \"11px 12px\",\n backgroundColor: \"transparent\",\n border: \"none\",\n cursor: \"pointer\",\n boxSizing: \"border-box\",\n }}\n >\n \n \n \n \n \n \n Tokens below minimum\n \n \n {showBelowMin\n ? \"Tokens under $1 are unavailable for swaps\"\n : \"Hidden to prevent failed swaps\"}\n \n \n \n \n {/* Small token logo cluster */}\n
\n {belowMin.slice(0, 3).map((t, i) =>\n 0 ? -6 : 0,\n }}\n />,\n )}\n {belowMin.length > 3 && (\n \n +{belowMin.length - 3}\n
\n )}\n \n {showBelowMin ? (\n \n ) : (\n \n )}\n \n \n \n
\n {belowMin.map((token, index) => (\n \n \n \n \n {token.chainLogo && (\n \n )}\n
\n \n {token.symbol} on{\" \"}\n {token.chainName || \"Unknown chain\"}\n \n \n \n {token.balanceInFiat}\n \n \n ))}\n \n \n \n )}\n \n )}\n \n\n {/* Done button */}\n {isMulti && (\n
\n {shouldShowSelectionProgress && requiredUsdAmount && (\n \n \n \n Required\n \n \n \n {formatUsdBalanceLabel(selectionDeficitUsdAmount)}\n {\" \"}\n more\n \n
\n \n \n \n \n )}\n {!hasSelectionShortfall && (\n \n Done\n \n )}\n \n )}\n\n {/* Chain Selector Modal */}\n {showChainSelector &&\n (() => {\n const chainModal = (\n \n \n \n \n \n \n \n \n \n \n \n Select chain\n \n \n\n {/* Search */}\n
\n \n \n setIsChainSearchFocused(false)}\n onChange={(e) => setChainQuery(e.target.value)}\n onFocus={() => setIsChainSearchFocused(true)}\n placeholder=\"Search chains\"\n style={{\n flex: 1,\n backgroundColor: \"transparent\",\n border: \"none\",\n outline: \"none\",\n fontFamily: '\"Geist\", system-ui, sans-serif',\n fontSize: 13,\n color: \"#161615\",\n }}\n value={chainQuery}\n />\n
\n \n\n {/* Chain list */}\n \n \n {\n setSelectedChainFilter(null);\n closeChainSelector();\n }}\n style={{\n width: \"100%\",\n display: \"flex\",\n alignItems: \"center\",\n padding: \"8px 14px\",\n backgroundColor: \"transparent\",\n border: \"none\",\n borderBottom: \"1px solid #F0F0EF\",\n cursor: \"pointer\",\n boxSizing: \"border-box\",\n }}\n >\n \n \n \n All Chains\n \n \n\n {/* Unique chains */}\n {chainOptions\n .filter((t) =>\n (t.chainName || \"\")\n .toLowerCase()\n .includes(chainQuery.toLowerCase()),\n )\n .map((t) => (\n {\n setSelectedChainFilter(t.chainId!);\n closeChainSelector();\n }}\n style={{\n width: \"100%\",\n display: \"flex\",\n alignItems: \"center\",\n padding: \"8px 14px\",\n backgroundColor: \"transparent\",\n border: \"none\",\n borderBottom: \"1px solid #F0F0EF\",\n cursor: \"pointer\",\n boxSizing: \"border-box\",\n }}\n >\n \n \n \n {t.chainName}\n \n \n ))}\n \n \n \n \n );\n return portalRoot ? createPortal(chainModal, portalRoot) : chainModal;\n })()}\n \n );\n}\n", + "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\n\"use client\";\nimport type { SupportedChainsAndTokensResult } from \"@avail-project/nexus-core\";\nimport { formatTokenBalance } from \"@avail-project/nexus-core/utils\";\nimport Decimal from \"decimal.js\";\nimport {\n Check,\n ChevronDown,\n ChevronUp,\n Globe,\n Info,\n Loader2,\n Minus,\n Search,\n X,\n} from \"lucide-react\";\nimport React, {\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n CHAIN_METADATA,\n getSdkSwapSupportedChainIds,\n getShortChainName,\n isSwapSupportedBySdkChainList,\n} from \"../../common/utils/constant\";\nimport type { UserAsset } from \"../../nexus/NexusProvider\";\nimport { NEXUS_WIDGET_FAST_SPINNER_ANIMATION } from \"../theme\";\n\nconst tabularNums: React.CSSProperties = {\n fontFeatureSettings: '\"tnum\"',\n fontVariantNumeric: \"tabular-nums\",\n};\n\nconst brand = \"var(--foreground-brand)\";\n\nexport interface SwapTokenOption {\n balance: string;\n balanceInFiat: string;\n chainId?: number;\n chainLogo?: string;\n chainName?: string;\n contractAddress: string;\n decimals: number;\n isUnified?: boolean;\n logo?: string;\n name: string;\n priceUSD?: number | string;\n sourceTokens?: SwapTokenOption[];\n symbol: string;\n unifiedSymbol?: \"USDC\" | \"USDT\" | \"ETH\";\n userAmount?: string;\n userAmountMode?: \"token\" | \"usd\";\n userAmountUsd?: string;\n}\n\ninterface SwapAssetSelectorProps {\n allowSelectedTokenRemoval?: boolean;\n allowUnified?: boolean;\n autoSelectFilterTabs?: boolean;\n editingAssetIndex?: number | null;\n filterTabBehavior?: FilterTabBehavior;\n hideCustomTab?: boolean;\n initialFilterTab?: FilterTab;\n isMulti?: boolean;\n lockedTokens?: SwapTokenOption[];\n onBack: () => void;\n onClearSelection?: () => void;\n onDone?: (tokens?: SwapTokenOption[]) => void;\n onFilterTabSelect?: (tab: Exclude) => void;\n onSelect: (token: SwapTokenOption) => void;\n onSelectionChange?: (tokens: SwapTokenOption[]) => void;\n onToggle?: (token: SwapTokenOption) => void;\n preserveSelectedBelowMinimum?: boolean;\n requiredUsd?: string;\n selectedTokens?: SwapTokenOption[];\n showBelowMinimumInline?: boolean;\n staticOptions?: SwapTokenOption[];\n swapBalance: UserAsset[] | null;\n swapSupportedChains?: SupportedChainsAndTokensResult | null;\n title: string;\n}\n\nexport function deriveTokenOptions(\n swapBalance: UserAsset[],\n swapSupportedChains?: SupportedChainsAndTokensResult | null,\n): SwapTokenOption[] {\n const tokens: SwapTokenOption[] = [];\n for (const asset of swapBalance) {\n for (const bd of asset.breakdown ?? []) {\n if (!isSwapSupportedBySdkChainList(bd.chain?.id, swapSupportedChains)) {\n continue;\n }\n if (Number.parseFloat(bd.balance ?? \"0\") <= 0) continue;\n const chainMeta = bd.chain?.id ? CHAIN_METADATA[bd.chain.id] : undefined;\n tokens.push({\n contractAddress: bd.contractAddress,\n symbol: bd.symbol ?? asset.symbol,\n name: bd.symbol ?? asset.symbol,\n logo: asset.logo ?? \"\",\n decimals: bd.decimals ?? asset.decimals ?? 18,\n balance: bd.balance,\n balanceInFiat:\n bd.balanceInFiat != null\n ? `$${Number(bd.balanceInFiat).toFixed(2)}`\n : \"$0.00\",\n chainId: bd.chain?.id,\n chainName: getShortChainName(\n bd.chain?.id,\n chainMeta?.name ?? bd.chain?.name,\n ),\n chainLogo: chainMeta?.logo ?? bd.chain?.logo,\n });\n }\n }\n const seen = new Map();\n for (const t of tokens) {\n seen.set(`${t.contractAddress.toLowerCase()}-${t.chainId}`, t);\n }\n return Array.from(seen.values());\n}\n\n/* ── Radio dot (circular) ── */\nexport const RadioDot = ({ selected }: { selected: boolean }) => (\n \n {selected && (\n \n )}\n \n);\n\nconst SelectionControl = ({\n selected,\n indeterminate = false,\n multi,\n}: {\n selected: boolean;\n indeterminate?: boolean;\n multi: boolean;\n}) => {\n if (!multi) return ;\n\n const isActive = selected || indeterminate;\n\n return (\n \n {selected && (\n \n )}\n {!selected && indeterminate && (\n \n )}\n \n );\n};\n\nfunction TokenLogo({\n src,\n label,\n size,\n fontSize,\n style,\n fallbackBackground = brand,\n fallbackColor = \"#FFFFFE\",\n}: {\n src?: string;\n label?: string;\n size: number;\n fontSize: number;\n style?: React.CSSProperties;\n fallbackBackground?: string;\n fallbackColor?: string;\n}) {\n const normalizedSrc = src?.trim();\n const [failed, setFailed] = useState(!normalizedSrc);\n\n useEffect(() => {\n setFailed(!normalizedSrc);\n }, [normalizedSrc]);\n\n if (!failed && normalizedSrc) {\n return (\n setFailed(true)}\n src={normalizedSrc}\n style={{\n borderRadius: \"999px\",\n height: size,\n objectFit: \"cover\",\n width: size,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {(label || \"?\").slice(0, 2).toUpperCase()}\n \n );\n}\n\nconst getChainLogo = (\n token?: Pick | null,\n) => {\n if (!token) return undefined;\n return token.chainId\n ? (CHAIN_METADATA[token.chainId]?.logo ?? token.chainLogo)\n : token.chainLogo;\n};\n\n/* ── Chain logo cluster ── */\nconst ChainLogos = ({ tokens }: { tokens: SwapTokenOption[] }) => {\n const clusterRef = useRef(null);\n const closeTimerRef = useRef | null>(null);\n const [showTooltip, setShowTooltip] = useState(false);\n const [tooltipRect, setTooltipRect] = useState(null);\n const uniqueChains = useMemo(() => {\n const seen = new Set();\n const out: {\n id: number;\n logo?: string;\n name?: string;\n balance?: string;\n balanceInFiat?: string;\n balanceValue: number;\n fiatValue: number;\n }[] = [];\n for (const t of tokens) {\n if (t.chainId && !seen.has(t.chainId)) {\n const balanceValue = Number(\n String(t.balance ?? \"\").replace(/[^0-9.]/g, \"\") || 0,\n );\n seen.add(t.chainId);\n out.push({\n id: t.chainId,\n logo: getChainLogo(t),\n name: getShortChainName(t.chainId, t.chainName),\n balance: t.balance,\n balanceInFiat: t.balanceInFiat,\n balanceValue:\n Number.isNaN(balanceValue) || !Number.isFinite(balanceValue)\n ? 0\n : balanceValue,\n fiatValue: getTokenFiatValue(t),\n });\n }\n }\n return out.sort((a, b) => {\n if (a.fiatValue !== b.fiatValue) return b.fiatValue - a.fiatValue;\n if (a.balanceValue !== b.balanceValue) {\n return b.balanceValue - a.balanceValue;\n }\n return (a.name ?? \"\").localeCompare(b.name ?? \"\");\n });\n }, [tokens]);\n\n const maxShow = 3;\n const shown = uniqueChains.slice(0, maxShow);\n const openTooltip = () => {\n if (closeTimerRef.current) {\n clearTimeout(closeTimerRef.current);\n closeTimerRef.current = null;\n }\n setTooltipRect(clusterRef.current?.getBoundingClientRect() ?? null);\n setShowTooltip(true);\n };\n const closeTooltip = () => {\n if (closeTimerRef.current) clearTimeout(closeTimerRef.current);\n closeTimerRef.current = setTimeout(() => {\n setShowTooltip(false);\n closeTimerRef.current = null;\n }, 120);\n };\n useEffect(() => {\n return () => {\n if (closeTimerRef.current) clearTimeout(closeTimerRef.current);\n };\n }, []);\n const showTooltipAbove = tooltipRect ? tooltipRect.top > 240 : true;\n const tooltip =\n showTooltip && uniqueChains.length > 1 && tooltipRect\n ? createPortal(\n \n \n UNIFIED · {uniqueChains.length} CHAINS\n \n {tokens\n .reduce((sum, token) => sum + getTokenFiatValue(token), 0)\n .toLocaleString(undefined, {\n currency: \"USD\",\n maximumFractionDigits: 2,\n style: \"currency\",\n })}\n \n \n {uniqueChains.map((chain) => (\n \n \n \n \n {chain.name || \"Unknown chain\"}\n \n \n \n {chain.balance\n ? formatTokenAmountDisplay(\n String(chain.balance).replace(/\\s+[^\\s]+$/, \"\"),\n )\n : chain.balanceInFiat || \"0\"}\n \n \n ))}\n ,\n document.body,\n )\n : null;\n\n return (\n \n {tooltip}\n {shown.map((c) => (\n \n ))}\n \n {uniqueChains.length} chain{uniqueChains.length !== 1 ? \"s\" : \"\"}\n \n \n );\n};\n\n/* ── Filter tabs ── */\ntype FilterTab = \"all\" | \"native\" | \"stables\" | \"custom\";\ntype FilterTabBehavior = \"select-all\" | \"source-pool\";\nconst FILTER_TABS: { key: FilterTab; label: string }[] = [\n { key: \"all\", label: \"All\" },\n { key: \"native\", label: \"Native\" },\n { key: \"stables\", label: \"Stables\" },\n { key: \"custom\", label: \"Custom\" },\n];\nconst STABLE_SYMBOLS = new Set([\n \"AUSD\",\n \"BDO\",\n \"BRZ\",\n \"BTCUSD\",\n \"BUSD\",\n \"bnbUSD\",\n \"crvUSD\",\n \"CUSD\",\n \"DAI\",\n \"DAI.e\",\n \"dEURO\",\n \"DUSD\",\n \"EURA\",\n \"EURC\",\n \"EURCV\",\n \"EURe\",\n \"EURS\",\n \"EURT\",\n \"eUSD\",\n \"frxUSD\",\n \"GHO\",\n \"GUSD\",\n \"JUST\",\n \"jEUR\",\n \"JUSD\",\n \"LUSD\",\n \"rUSD\",\n \"RLUSD\",\n \"sUSD\",\n \"svJUSD\",\n \"TUSD\",\n \"USDC\",\n \"USDC.e\",\n \"USDS\",\n \"USDT\",\n \"USDT0\",\n \"USDT.e\",\n \"ctUSD\",\n \"PYUSD\",\n \"USDe\",\n \"xDAI\",\n \"USD0\",\n \"USDM\",\n]);\nconst normalizeTokenGroupSymbol = (symbol: string) =>\n symbol\n .trim()\n .toUpperCase()\n .replaceAll(\"₮\", \"T\")\n .replaceAll(/[^A-Z0-9]/g, \"\");\nconst STABLE_SYMBOL_KEYS = new Set(\n Array.from(STABLE_SYMBOLS, normalizeTokenGroupSymbol),\n);\n\nconst isStableToken = (token: SwapTokenOption) =>\n STABLE_SYMBOL_KEYS.has(normalizeTokenGroupSymbol(token.symbol));\n\nfunction isNativeToken(t: SwapTokenOption) {\n if (isNativeLikeAddress(t.contractAddress)) return true;\n\n const sym = t.symbol.toUpperCase();\n const chain = (t.chainName || \"\").toLowerCase();\n\n if (sym === \"ETH\")\n return (\n !chain.includes(\"bnb\") &&\n !chain.includes(\"bsc\") &&\n !chain.includes(\"polygon\") &&\n !chain.includes(\"monad\") &&\n !chain.includes(\"hyperevm\")\n );\n if (sym === \"POL\" || sym === \"MATIC\") return chain.includes(\"polygon\");\n if (sym === \"HYPE\") return chain.includes(\"hyperevm\");\n if (sym === \"MON\") return chain.includes(\"monad\");\n if (sym === \"BNB\") return chain.includes(\"bnb\") || chain.includes(\"bsc\");\n if (sym === \"AVAX\") return chain.includes(\"avalanche\");\n if (sym === \"FTM\") return chain.includes(\"fantom\");\n if (sym === \"CELO\") return chain.includes(\"celo\");\n if (sym === \"SUI\") return chain.includes(\"sui\");\n if (sym === \"APT\") return chain.includes(\"aptos\");\n if (sym === \"SOL\") return chain.includes(\"solana\");\n return false;\n}\n\nconst MIN_FIAT_THRESHOLD = 1;\nconst CHAIN_SELECTOR_CLOSE_MS = 220;\nconst MODAL_HEIGHT_TRANSITION_MS = 260;\nconst modalHeightTransitionStyle = {\n interpolateSize: \"allow-keywords\",\n} as React.CSSProperties;\nconst modalHeightTransition = `height ${MODAL_HEIGHT_TRANSITION_MS}ms ease, max-height ${MODAL_HEIGHT_TRANSITION_MS}ms ease`;\nexport const SWAP_CHAIN_DISPLAY_ORDER = [\n 1, // Ethereum\n 42161, // Arbitrum\n 8453, // Base\n 137, // Polygon\n 10, // OP\n 999, // HyperEVM\n 56, // BSC\n // 43114, // Avalanche\n 143, // Monad\n 4326, // MegaETH\n 4114, // Citrea\n 8217, // Kaia\n 534352, // Scroll\n] as const;\nconst SWAP_CHAIN_DISPLAY_ORDER_RANK = new Map(\n SWAP_CHAIN_DISPLAY_ORDER.map((chainId, index) => [chainId, index]),\n);\nexport const SWAP_CHAIN_DISPLAY_ORDER_SET = new Set(\n SWAP_CHAIN_DISPLAY_ORDER,\n);\nexport const sortChainIdsBySwapDisplayOrder = (chainIds: number[]) =>\n [...chainIds].sort((a, b) => {\n const aRank =\n SWAP_CHAIN_DISPLAY_ORDER_RANK.get(a) ?? Number.MAX_SAFE_INTEGER;\n const bRank =\n SWAP_CHAIN_DISPLAY_ORDER_RANK.get(b) ?? Number.MAX_SAFE_INTEGER;\n if (aRank !== bRank) return aRank - bRank;\n\n const aName = CHAIN_METADATA[a]?.name ?? String(a);\n const bName = CHAIN_METADATA[b]?.name ?? String(b);\n return aName.localeCompare(bName);\n });\nexport const compareChainsBySwapDisplayOrder = <\n T extends { chainId?: number; chainName?: string },\n>(\n a: T,\n b: T,\n) => {\n const aRank =\n SWAP_CHAIN_DISPLAY_ORDER_RANK.get(a.chainId ?? -1) ??\n Number.MAX_SAFE_INTEGER;\n const bRank =\n SWAP_CHAIN_DISPLAY_ORDER_RANK.get(b.chainId ?? -1) ??\n Number.MAX_SAFE_INTEGER;\n if (aRank !== bRank) return aRank - bRank;\n return (a.chainName ?? \"\").localeCompare(b.chainName ?? \"\");\n};\nconst UNIFIED_MAINNET_CHAIN_IDS = new Set([\n 1, 10, 56, 137, 143, 999, 4114, 8217, 8453, 42161, 43114, 534352, 4326,\n]);\nconst UNIFIED_USDC_SYMBOL_KEYS = new Set([\"USDC\", \"USDCE\", \"USDM\"]);\nconst UNIFIED_USDT_SYMBOL_KEYS = new Set([\"USDT\", \"USDT0\", \"USDTE\"]);\n\nconst escapeRegExp = (value: string) =>\n value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\nconst getTokenFiatValue = (token: Pick) => {\n const parsed = Number(\n String(token.balanceInFiat ?? \"\").replace(/[^0-9.]/g, \"\") || 0,\n );\n return isNaN(parsed) || !isFinite(parsed) ? 0 : parsed;\n};\n\nconst formatBalanceWithSymbol = (\n token: Pick,\n) => {\n const symbol = token.symbol?.trim() || \"\";\n const balanceStr = String(token.balance ?? \"\").trim();\n let cleanBalance = balanceStr;\n if (symbol) {\n cleanBalance = balanceStr.replace(\n new RegExp(`\\\\s*${escapeRegExp(symbol)}$`, \"i\"),\n \"\",\n );\n }\n const formatted = formatTokenAmountDisplay(cleanBalance);\n return symbol ? `${formatted} ${symbol}` : formatted;\n};\n\nconst parseTokenAmount = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = String(value).replace(/[^0-9.-]/g, \"\");\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst compareTokensByUsdBalance = (a: SwapTokenOption, b: SwapTokenOption) => {\n const fiatDelta = getTokenFiatValue(b) - getTokenFiatValue(a);\n if (fiatDelta !== 0) return fiatDelta;\n\n const aBalance = parseTokenAmount(a.balance) ?? new Decimal(0);\n const bBalance = parseTokenAmount(b.balance) ?? new Decimal(0);\n const balanceDelta = bBalance.cmp(aBalance);\n if (balanceDelta !== 0) return balanceDelta;\n\n const chainDelta = compareChainsBySwapDisplayOrder(a, b);\n if (chainDelta !== 0) return chainDelta;\n\n return `${a.symbol} ${a.chainName}`.localeCompare(\n `${b.symbol} ${b.chainName}`,\n );\n};\n\nconst sortTokensByUsdBalance = (tokens: SwapTokenOption[]) =>\n [...tokens].sort(compareTokensByUsdBalance);\n\nexport const formatTokenAmountDisplay = (value: unknown) => {\n const amount = parseTokenAmount(value) ?? new Decimal(0);\n const abs = amount.abs();\n\n if (amount.isZero()) return \"0\";\n\n const compactUnits = [\n { suffix: \"T\", value: new Decimal(1_000_000_000_000) },\n { suffix: \"B\", value: new Decimal(1_000_000_000) },\n { suffix: \"M\", value: new Decimal(1_000_000) },\n ];\n\n for (const unit of compactUnits) {\n if (abs.gte(unit.value)) {\n return `${amount\n .div(unit.value)\n .toDecimalPlaces(4, Decimal.ROUND_DOWN)\n .toFixed()}${unit.suffix}`;\n }\n }\n\n const minDisplay = new Decimal(\"0.00000001\");\n if (amount.gt(0) && amount.lt(minDisplay)) {\n return `>${minDisplay.toFixed()}`;\n }\n\n return amount.toDecimalPlaces(8, Decimal.ROUND_DOWN).toFixed();\n};\n\nconst addThousandsSeparators = (value: string) => {\n const [integerPart, decimalPart] = value.split(\".\");\n const withSeparators = integerPart.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n return decimalPart === undefined\n ? withSeparators\n : `${withSeparators}.${decimalPart}`;\n};\n\nexport const formatUsdBalanceLabel = (value: unknown) => {\n const amount = parseTokenAmount(value) ?? new Decimal(0);\n const abs = amount.abs();\n\n if (amount.isZero()) return \"$0.00\";\n if (amount.gt(0) && amount.lt(0.01)) return \"<$0.01\";\n\n const compactUnits = [\n { suffix: \"T\", value: new Decimal(1_000_000_000_000) },\n { suffix: \"B\", value: new Decimal(1_000_000_000) },\n { suffix: \"M\", value: new Decimal(1_000_000) },\n ];\n\n for (const unit of compactUnits) {\n if (abs.gte(unit.value)) {\n return `$${amount\n .div(unit.value)\n .toDecimalPlaces(4, Decimal.ROUND_DOWN)\n .toFixed()}${unit.suffix}`;\n }\n }\n\n return `$${addThousandsSeparators(amount.toDecimalPlaces(2).toFixed(2))}`;\n};\n\nexport const formatSelectedTokenBalanceLabel = (\n token?: Pick,\n) => {\n if (!token) return \"\";\n const symbol = token.symbol || \"\";\n const formatted = formatTokenAmountDisplay(token.balance);\n return symbol ? `${formatted} ${symbol}` : formatted;\n};\n\nconst getSearchTerms = (query: string) =>\n query.toLowerCase().trim().split(/\\s+/).filter(Boolean);\n\nconst includesTerm = (value: string | undefined, term: string) =>\n (value ?? \"\").toLowerCase().includes(term);\n\nconst startsWithTerm = (value: string | undefined, term: string) =>\n (value ?? \"\").toLowerCase().startsWith(term);\n\nconst equalsTerm = (value: string | undefined, term: string) =>\n (value ?? \"\").toLowerCase() === term;\n\nexport const getTokenSearchRank = (\n token: Pick<\n SwapTokenOption,\n \"symbol\" | \"name\" | \"chainName\" | \"contractAddress\"\n >,\n query: string,\n) => {\n const terms = getSearchTerms(query);\n if (terms.length === 0) return null;\n\n let matchedTerms = 0;\n let symbolExactTerms = 0;\n let symbolPrefixTerms = 0;\n let symbolIncludeTerms = 0;\n let namePrefixTerms = 0;\n let tokenExactTerms = 0;\n let tokenPrefixTerms = 0;\n let tokenIncludeTerms = 0;\n let chainExactTerms = 0;\n let chainPrefixTerms = 0;\n let chainIncludeTerms = 0;\n let addressTerms = 0;\n\n for (const term of terms) {\n const symbolExact = equalsTerm(token.symbol, term);\n const symbolPrefix = symbolExact || startsWithTerm(token.symbol, term);\n const symbolInclude = symbolPrefix || includesTerm(token.symbol, term);\n const nameExact = equalsTerm(token.name, term);\n const namePrefix = nameExact || startsWithTerm(token.name, term);\n const nameInclude = namePrefix || includesTerm(token.name, term);\n const tokenExact = symbolExact || nameExact;\n const tokenPrefix = tokenExact || symbolPrefix || namePrefix;\n const tokenInclude = tokenPrefix || symbolInclude || nameInclude;\n const chainExact = equalsTerm(token.chainName, term);\n const chainPrefix = chainExact || startsWithTerm(token.chainName, term);\n const chainInclude = chainPrefix || includesTerm(token.chainName, term);\n const addressMatch = includesTerm(token.contractAddress, term);\n\n if (tokenInclude || chainInclude || addressMatch) matchedTerms += 1;\n if (symbolExact) symbolExactTerms += 1;\n if (symbolPrefix) symbolPrefixTerms += 1;\n if (symbolInclude) symbolIncludeTerms += 1;\n if (namePrefix) namePrefixTerms += 1;\n if (tokenExact) tokenExactTerms += 1;\n if (tokenPrefix) tokenPrefixTerms += 1;\n if (tokenInclude) tokenIncludeTerms += 1;\n if (chainExact) chainExactTerms += 1;\n if (chainPrefix) chainPrefixTerms += 1;\n if (chainInclude) chainIncludeTerms += 1;\n if (addressMatch) addressTerms += 1;\n }\n\n const allTermsMatched = matchedTerms === terms.length;\n if (!allTermsMatched) return null;\n\n if (\n terms.length > 1 &&\n chainIncludeTerms > 0 &&\n symbolIncludeTerms === 0 &&\n addressTerms === 0 &&\n namePrefixTerms === 0\n ) {\n return null;\n }\n\n const hasTokenMatch = tokenIncludeTerms > 0;\n const hasChainMatch = chainIncludeTerms > 0;\n const isTokenChainMatch = allTermsMatched && hasTokenMatch && hasChainMatch;\n\n let score = 20;\n if (isTokenChainMatch) {\n if (symbolExactTerms > 0 && chainExactTerms > 0) score = 0;\n else if (symbolExactTerms > 0 && chainPrefixTerms > 0) score = 1;\n else if (symbolExactTerms > 0 && chainIncludeTerms > 0) score = 2;\n else if (symbolPrefixTerms > 0 && chainIncludeTerms > 0) score = 3;\n else if (symbolIncludeTerms > 0 && chainIncludeTerms > 0) score = 4;\n else if (namePrefixTerms > 0 && chainIncludeTerms > 0) score = 5;\n else score = 6;\n } else if (symbolExactTerms > 0) score = 7;\n else if (symbolPrefixTerms > 0) score = 8;\n else if (symbolIncludeTerms > 0) score = 9;\n else if (tokenExactTerms > 0) score = 10;\n else if (tokenPrefixTerms > 0) score = 11;\n else if (tokenIncludeTerms > 0) score = 12;\n else if (chainExactTerms > 0) score = 13;\n else if (chainPrefixTerms > 0) score = 14;\n else if (chainIncludeTerms > 0) score = 15;\n else if (addressTerms > 0) score = 16;\n\n return {\n allTermsMatched,\n isTokenChainMatch,\n matchedTerms,\n score,\n tokenExactTerms,\n tokenIncludeTerms,\n };\n};\n\nconst isPrioritySearchMatch = (token: SwapTokenOption, query: string) => {\n const rank = getTokenSearchRank(token, query);\n return Boolean(\n rank &&\n (rank.isTokenChainMatch ||\n rank.tokenExactTerms > 0 ||\n rank.allTermsMatched),\n );\n};\n\nconst compareTokensBySearch = (\n a: SwapTokenOption,\n b: SwapTokenOption,\n query: string,\n) => {\n const aRank = getTokenSearchRank(a, query);\n const bRank = getTokenSearchRank(b, query);\n const aScore = aRank?.score ?? Number.MAX_SAFE_INTEGER;\n const bScore = bRank?.score ?? Number.MAX_SAFE_INTEGER;\n if (aScore !== bScore) return aScore - bScore;\n\n const aMatched = aRank?.matchedTerms ?? 0;\n const bMatched = bRank?.matchedTerms ?? 0;\n if (aMatched !== bMatched) return bMatched - aMatched;\n\n const aFiat = getTokenFiatValue(a);\n const bFiat = getTokenFiatValue(b);\n if (aFiat !== bFiat) return bFiat - aFiat;\n\n return `${a.symbol} ${a.chainName}`.localeCompare(\n `${b.symbol} ${b.chainName}`,\n );\n};\n\nfunction getUnifiedSymbol(token: Pick) {\n if (token.chainId && !UNIFIED_MAINNET_CHAIN_IDS.has(token.chainId)) {\n return null;\n }\n\n const symbol = normalizeTokenGroupSymbol(token.symbol);\n if (UNIFIED_USDC_SYMBOL_KEYS.has(symbol)) return \"USDC\" as const;\n if (UNIFIED_USDT_SYMBOL_KEYS.has(symbol)) return \"USDT\" as const;\n if (symbol === \"ETH\") return \"ETH\" as const;\n return null;\n}\n\nfunction sameTokenOption(a?: SwapTokenOption, b?: SwapTokenOption) {\n if (!a || !b) return false;\n if (a.isUnified || b.isUnified) {\n return Boolean(\n a.isUnified && b.isUnified && a.unifiedSymbol === b.unifiedSymbol,\n );\n }\n return (\n sameContractAddress(a.contractAddress, b.contractAddress) &&\n a.chainId === b.chainId\n );\n}\n\nfunction isSameTokenList(a: SwapTokenOption[], b: SwapTokenOption[]) {\n if (a.length !== b.length) return false;\n return a.every((tokenA) =>\n b.some((tokenB) => sameTokenOption(tokenA, tokenB)),\n );\n}\n\nfunction dedupeTokenOptions(tokens: SwapTokenOption[]) {\n return tokens.reduce((acc, token) => {\n if (!acc.some((item) => sameTokenOption(item, token))) {\n acc.push(token);\n }\n return acc;\n }, []);\n}\n\nfunction mergeTokenOptions(\n base: SwapTokenOption[],\n additions: SwapTokenOption[],\n) {\n return dedupeTokenOptions([...base, ...additions]);\n}\n\nfunction removeTokenOptions(\n base: SwapTokenOption[],\n removals: SwapTokenOption[],\n) {\n return base.filter(\n (token) => !removals.some((removal) => sameTokenOption(token, removal)),\n );\n}\n\nfunction isNativeLikeAddress(address?: string) {\n const normalized = (address ?? \"\").toLowerCase();\n return (\n normalized === \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\" ||\n normalized === \"0x0000000000000000000000000000000000000000\"\n );\n}\n\nfunction addressTail(address?: string) {\n const normalized = (address ?? \"\").toLowerCase();\n if (!normalized.startsWith(\"0x\")) return normalized;\n return normalized.slice(-40);\n}\n\nfunction sameContractAddress(a?: string, b?: string) {\n const normalizedA = (a ?? \"\").toLowerCase();\n const normalizedB = (b ?? \"\").toLowerCase();\n if (!normalizedA || !normalizedB) return normalizedA === normalizedB;\n if (normalizedA === normalizedB) return true;\n if (isNativeLikeAddress(normalizedA) && isNativeLikeAddress(normalizedB)) {\n return true;\n }\n return addressTail(normalizedA) === addressTail(normalizedB);\n}\n\nexport function SwapAssetSelector({\n title,\n swapBalance,\n swapSupportedChains,\n staticOptions,\n onSelect,\n onBack,\n isMulti,\n selectedTokens = [],\n editingAssetIndex = null,\n onToggle,\n onClearSelection,\n onDone,\n allowUnified = false,\n preserveSelectedBelowMinimum = false,\n showBelowMinimumInline = false,\n allowSelectedTokenRemoval = false,\n hideCustomTab = false,\n autoSelectFilterTabs = false,\n initialFilterTab = \"all\",\n filterTabBehavior = \"select-all\",\n onFilterTabSelect,\n lockedTokens = [],\n onSelectionChange,\n requiredUsd,\n}: SwapAssetSelectorProps) {\n const sdkSwapSupportedChainIds = useMemo(\n () => getSdkSwapSupportedChainIds(swapSupportedChains),\n [swapSupportedChains],\n );\n const selectorRef = useRef(null);\n const listRef = useRef(null);\n const chainCloseTimerRef = useRef | null>(null);\n const [portalRoot, setPortalRoot] = useState(null);\n const [query, setQuery] = useState(\"\");\n const normalizedInitialFilterTab =\n hideCustomTab && initialFilterTab === \"custom\" ? \"all\" : initialFilterTab;\n const [activeTab, setActiveTab] = useState(\n normalizedInitialFilterTab,\n );\n const [showBelowMin, setShowBelowMin] = useState(false);\n const [isSearchFocused, setIsSearchFocused] = useState(false);\n const [showChainSelector, setShowChainSelector] = useState(false);\n const [isChainSelectorClosing, setIsChainSelectorClosing] = useState(false);\n const [chainQuery, setChainQuery] = useState(\"\");\n const [selectedChainFilter, setSelectedChainFilter] = useState(\n null,\n );\n const [isChainSearchFocused, setIsChainSearchFocused] = useState(false);\n const stableListHeightRef = useRef(0);\n const pendingSelectionScrollTopRef = useRef(null);\n const skipNextSelectionScrollResetRef = useRef(false);\n const [stableListHeight, setStableListHeight] = useState(null);\n const lockedSelectedTokens = useMemo(\n () => dedupeTokenOptions(lockedTokens),\n [lockedTokens],\n );\n const isLockedToken = useCallback(\n (token: SwapTokenOption) =>\n lockedSelectedTokens.some((locked) => sameTokenOption(locked, token)),\n [lockedSelectedTokens],\n );\n const [draftSelectedTokens, setDraftSelectedTokens] = useState<\n SwapTokenOption[]\n >(() => mergeTokenOptions(selectedTokens, lockedSelectedTokens));\n useEffect(() => {\n if (!isMulti) return;\n setDraftSelectedTokens(\n mergeTokenOptions(selectedTokens, lockedSelectedTokens),\n );\n }, [isMulti, lockedSelectedTokens, selectedTokens]);\n const activeSelectedTokens = isMulti ? draftSelectedTokens : selectedTokens;\n const emitSelectionChange = useCallback(\n (tokens: SwapTokenOption[]) => {\n const next = mergeTokenOptions(tokens, lockedSelectedTokens);\n if (isMulti) {\n setDraftSelectedTokens(next);\n }\n onSelectionChange?.(next);\n },\n [isMulti, lockedSelectedTokens, onSelectionChange],\n );\n const visibleFilterTabs = useMemo(\n () =>\n hideCustomTab\n ? FILTER_TABS.filter((tab) => tab.key !== \"custom\")\n : FILTER_TABS,\n [hideCustomTab],\n );\n\n useEffect(() => {\n if (hideCustomTab && activeTab === \"custom\") {\n setActiveTab(\"all\");\n }\n }, [activeTab, hideCustomTab]);\n\n useEffect(() => {\n setActiveTab((current) =>\n current === normalizedInitialFilterTab\n ? current\n : normalizedInitialFilterTab,\n );\n }, [normalizedInitialFilterTab]);\n\n useEffect(() => {\n if (skipNextSelectionScrollResetRef.current) {\n skipNextSelectionScrollResetRef.current = false;\n return;\n }\n if (listRef.current) {\n listRef.current.scrollTop = 0;\n }\n }, [query, activeTab, selectedChainFilter]);\n\n useLayoutEffect(() => {\n const pendingScrollTop = pendingSelectionScrollTopRef.current;\n if (pendingScrollTop === null) return;\n\n if (listRef.current) {\n listRef.current.scrollTop = pendingScrollTop;\n }\n pendingSelectionScrollTopRef.current = null;\n }, [draftSelectedTokens]);\n\n const allTokens = useMemo(() => {\n const isSwapSupportedToken = (token: SwapTokenOption) =>\n isSwapSupportedBySdkChainList(token.chainId, swapSupportedChains);\n\n const baseTokens = staticOptions\n ? staticOptions.filter(isSwapSupportedToken)\n : swapBalance\n ? deriveTokenOptions(swapBalance, swapSupportedChains)\n : [];\n\n if (!preserveSelectedBelowMinimum && lockedSelectedTokens.length === 0) {\n return sortTokensByUsdBalance(baseTokens);\n }\n\n const merged = [...baseTokens];\n const selectedSourceTokens = [\n ...activeSelectedTokens,\n ...lockedSelectedTokens,\n ].flatMap((token) =>\n token.isUnified && token.sourceTokens?.length\n ? token.sourceTokens\n : [token],\n );\n\n for (const selectedToken of selectedSourceTokens) {\n const alreadyPresent = merged.some((token) =>\n sameTokenOption(token, selectedToken),\n );\n if (!alreadyPresent) {\n merged.push(selectedToken);\n }\n }\n\n return sortTokensByUsdBalance(merged.filter(isSwapSupportedToken));\n }, [\n lockedSelectedTokens,\n preserveSelectedBelowMinimum,\n activeSelectedTokens,\n swapBalance,\n swapSupportedChains,\n staticOptions,\n ]);\n\n const getFilterTabTokens = useCallback(\n (tab: FilterTab) => {\n let result = allTokens;\n if (selectedChainFilter !== null) {\n result = result.filter(\n (token) => token.chainId === selectedChainFilter,\n );\n }\n if (tab === \"native\") result = result.filter(isNativeToken);\n else if (tab === \"stables\") {\n result = result.filter(isStableToken);\n }\n\n if (showBelowMinimumInline) {\n return mergeTokenOptions(result, lockedSelectedTokens);\n }\n\n return mergeTokenOptions(\n result.filter(\n (token) => getTokenFiatValue(token) >= MIN_FIAT_THRESHOLD,\n ),\n lockedSelectedTokens,\n );\n },\n [allTokens, lockedSelectedTokens, selectedChainFilter],\n );\n\n const selectionMatchesFilterTab = useCallback(\n (tab: FilterTab) => {\n if (tab === \"custom\") return true;\n const expected = getFilterTabTokens(tab);\n const selected = mergeTokenOptions(\n activeSelectedTokens,\n lockedSelectedTokens,\n );\n return (\n selected.length === expected.length &&\n selected.every((token) =>\n expected.some((expectedToken) =>\n sameTokenOption(expectedToken, token),\n ),\n )\n );\n },\n [activeSelectedTokens, getFilterTabTokens, lockedSelectedTokens],\n );\n\n useEffect(() => {\n if (\n !autoSelectFilterTabs ||\n filterTabBehavior === \"source-pool\" ||\n !isMulti ||\n activeTab === \"custom\"\n )\n return;\n if (activeSelectedTokens.length === 0 && lockedSelectedTokens.length === 0)\n return;\n if (!selectionMatchesFilterTab(activeTab)) {\n setActiveTab(\"custom\");\n }\n }, [\n activeTab,\n activeSelectedTokens.length,\n autoSelectFilterTabs,\n filterTabBehavior,\n isMulti,\n lockedSelectedTokens.length,\n selectionMatchesFilterTab,\n ]);\n\n /* Search + tab + chain filter */\n const filtered = useMemo(() => {\n let result = allTokens;\n if (selectedChainFilter !== null) {\n result = result.filter((t) => t.chainId === selectedChainFilter);\n }\n if (query.trim()) {\n result = result\n .filter((t) => getTokenSearchRank(t, query) !== null)\n .sort((a, b) => compareTokensBySearch(a, b, query));\n }\n if (activeTab === \"native\") result = result.filter(isNativeToken);\n else if (activeTab === \"stables\") result = result.filter(isStableToken);\n else if (activeTab === \"custom\" && !autoSelectFilterTabs)\n result = result.filter((t) => !isNativeToken(t) && !isStableToken(t));\n return result;\n }, [activeTab, allTokens, autoSelectFilterTabs, query, selectedChainFilter]);\n\n const isTokenSelectedForVisibility = useCallback(\n (token: SwapTokenOption) => {\n if (isLockedToken(token)) return true;\n if (!preserveSelectedBelowMinimum) return false;\n\n return activeSelectedTokens.some(\n (selected) =>\n sameTokenOption(selected, token) ||\n Boolean(\n selected.isUnified &&\n selected.sourceTokens?.some((source) =>\n sameTokenOption(source, token),\n ),\n ),\n );\n },\n [activeSelectedTokens, isLockedToken, preserveSelectedBelowMinimum],\n );\n\n const isUnifiedSelectedForVisibility = useCallback(\n (symbol: string) =>\n preserveSelectedBelowMinimum &&\n activeSelectedTokens.some(\n (selected) => selected.isUnified && selected.unifiedSymbol === symbol,\n ),\n [activeSelectedTokens, preserveSelectedBelowMinimum],\n );\n\n /* Split into above/below minimum */\n const { aboveMin, belowMin } = useMemo(() => {\n const above: SwapTokenOption[] = [];\n const below: SwapTokenOption[] = [];\n for (const t of filtered) {\n const fiat = getTokenFiatValue(t);\n if (\n showBelowMinimumInline ||\n fiat >= MIN_FIAT_THRESHOLD ||\n isTokenSelectedForVisibility(t) ||\n isPrioritySearchMatch(t, query)\n )\n above.push(t);\n else below.push(t);\n }\n return { aboveMin: above, belowMin: below };\n }, [filtered, isTokenSelectedForVisibility, query, showBelowMinimumInline]);\n\n /* Group by symbol */\n const groupedFiltered = useMemo(() => {\n const groups: Record = {};\n for (const token of filtered) {\n const unifiedSym = allowUnified ? getUnifiedSymbol(token) : null;\n if (\n !showBelowMinimumInline &&\n unifiedSym &&\n getTokenFiatValue(token) < MIN_FIAT_THRESHOLD &&\n !isTokenSelectedForVisibility(token)\n ) {\n continue;\n }\n const key = unifiedSym ?? `${token.contractAddress}-${token.chainId}`;\n if (!groups[key]) groups[key] = [];\n groups[key].push(token);\n }\n return Object.values(groups)\n .map((group) => {\n const sortedGroup = sortTokensByUsdBalance(group);\n let totalFiatVal = 0;\n let totalBalVal = 0;\n for (const t of sortedGroup) {\n const fiatVal = getTokenFiatValue(t);\n totalFiatVal += isNaN(fiatVal) || !isFinite(fiatVal) ? 0 : fiatVal;\n const balStr = String(t.balance ?? \"\").replace(/[^0-9.]/g, \"\");\n const balVal = Number(balStr || 0);\n totalBalVal += isNaN(balVal) || !isFinite(balVal) ? 0 : balVal;\n }\n const unifiedSym = allowUnified\n ? getUnifiedSymbol(sortedGroup[0])\n : null;\n const symbol = unifiedSym ?? sortedGroup[0].symbol;\n const isStable = [\"USDC\", \"USDT\", \"DAI\", \"USDM\", \"CTUSD\"].includes(\n symbol.toUpperCase(),\n );\n const maxDigits = isStable ? 2 : 6;\n const formattedBal = totalBalVal.toLocaleString(undefined, {\n minimumFractionDigits: 0,\n maximumFractionDigits: maxDigits,\n });\n\n return {\n symbol,\n logo: sortedGroup[0].logo,\n totalFiat: totalFiatVal,\n totalFiatStr: `$${totalFiatVal.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 })}`,\n totalBalStr: `${formattedBal} ${symbol}`,\n totalBalRaw: totalBalVal,\n tokens: sortedGroup,\n isUnifiedCandidate: Boolean(unifiedSym && group.length > 1),\n };\n })\n .filter((group) => {\n const hasSelectedToken = group.tokens.some(\n isTokenSelectedForVisibility,\n );\n const hasSelectedUnified = isUnifiedSelectedForVisibility(group.symbol);\n const hasPrioritySearchMatch = group.tokens.some((token) =>\n isPrioritySearchMatch(token, query),\n );\n if (showBelowMinimumInline) {\n return true;\n }\n if (group.isUnifiedCandidate) {\n return (\n group.totalFiat >= MIN_FIAT_THRESHOLD ||\n hasSelectedToken ||\n hasSelectedUnified ||\n hasPrioritySearchMatch\n );\n }\n return group.tokens.some(\n (token) =>\n getTokenFiatValue(token) >= MIN_FIAT_THRESHOLD ||\n isTokenSelectedForVisibility(token) ||\n isPrioritySearchMatch(token, query),\n );\n })\n .sort((a, b) => {\n if (query.trim()) {\n const aScore = Math.min(\n ...a.tokens.map(\n (token) =>\n getTokenSearchRank(token, query)?.score ??\n Number.MAX_SAFE_INTEGER,\n ),\n );\n const bScore = Math.min(\n ...b.tokens.map(\n (token) =>\n getTokenSearchRank(token, query)?.score ??\n Number.MAX_SAFE_INTEGER,\n ),\n );\n if (aScore !== bScore) return aScore - bScore;\n }\n return b.totalFiat - a.totalFiat;\n });\n }, [\n filtered,\n allowUnified,\n isTokenSelectedForVisibility,\n isUnifiedSelectedForVisibility,\n query,\n ]);\n\n const [expandedGroups, setExpandedGroups] = useState>(new Set());\n\n const toggleGroup = (symbol: string, e: React.MouseEvent) => {\n e.stopPropagation();\n setExpandedGroups((prev) => {\n const next = new Set(prev);\n if (next.has(symbol)) next.delete(symbol);\n else next.add(symbol);\n return next;\n });\n };\n\n const isTokenSelectedInOtherSlot = (token: SwapTokenOption) =>\n !allowSelectedTokenRemoval &&\n activeSelectedTokens.some(\n (st, idx) => idx !== editingAssetIndex && sameTokenOption(st, token),\n );\n\n const isTokenSelectedInCurrentSlot = (token: SwapTokenOption) => {\n if (isMulti) {\n return activeSelectedTokens.some(\n (st) =>\n sameTokenOption(st, token) ||\n Boolean(\n st.isUnified &&\n st.sourceTokens?.some((source) => sameTokenOption(source, token)),\n ),\n );\n }\n if (allowSelectedTokenRemoval) {\n return activeSelectedTokens.some(\n (st) =>\n sameTokenOption(st, token) ||\n Boolean(\n st.isUnified &&\n st.sourceTokens?.some((source) => sameTokenOption(source, token)),\n ),\n );\n }\n if (editingAssetIndex === null) return false;\n const st = activeSelectedTokens[editingAssetIndex];\n return sameTokenOption(st, token);\n };\n\n const isGroupUnifiedSelectedInOtherSlot = (\n group: (typeof groupedFiltered)[0],\n ) => {\n if (allowSelectedTokenRemoval) return false;\n const relevantTokens = isMulti\n ? activeSelectedTokens\n : activeSelectedTokens.filter((_, idx) => idx !== editingAssetIndex);\n return relevantTokens.some(\n (st) => st.isUnified && st.unifiedSymbol === group.symbol,\n );\n };\n\n const isGroupUnifiedSelectedInCurrentSlot = (\n group: (typeof groupedFiltered)[0],\n ) => {\n if (isMulti) {\n return activeSelectedTokens.some(\n (st) => st.isUnified && st.unifiedSymbol === group.symbol,\n );\n }\n if (editingAssetIndex === null) return false;\n const st = activeSelectedTokens[editingAssetIndex];\n return Boolean(st?.isUnified && st.unifiedSymbol === group.symbol);\n };\n\n const isAnyTokenInGroupSelectedInOtherSlot = (\n group: (typeof groupedFiltered)[0],\n ) => {\n if (allowSelectedTokenRemoval) return false;\n const relevantTokens = isMulti\n ? activeSelectedTokens\n : activeSelectedTokens.filter((_, idx) => idx !== editingAssetIndex);\n return relevantTokens.some(\n (st) =>\n group.tokens.some((gt) => sameTokenOption(gt, st)) ||\n (st.isUnified && st.unifiedSymbol === group.symbol),\n );\n };\n\n const selectorRows = useMemo(() => {\n if (isMulti) {\n return groupedFiltered.map((group) => ({\n fiat: group.totalFiat,\n group,\n key: `group:${group.symbol}`,\n kind: \"group\" as const,\n label: group.symbol,\n searchScore: query.trim()\n ? Math.min(\n ...group.tokens.map(\n (token) =>\n getTokenSearchRank(token, query)?.score ??\n Number.MAX_SAFE_INTEGER,\n ),\n )\n : 0,\n }));\n }\n\n const rows: Array<\n | {\n fiat: number;\n group: (typeof groupedFiltered)[number];\n key: string;\n kind: \"group\";\n label: string;\n searchScore: number;\n }\n | {\n fiat: number;\n key: string;\n kind: \"token\";\n label: string;\n searchScore: number;\n token: SwapTokenOption;\n }\n > = [];\n const hasQuery = Boolean(query.trim());\n const getSearchScore = (token: SwapTokenOption) =>\n hasQuery\n ? (getTokenSearchRank(token, query)?.score ?? Number.MAX_SAFE_INTEGER)\n : 0;\n const isVisibleTokenRow = (token: SwapTokenOption) =>\n showBelowMinimumInline ||\n getTokenFiatValue(token) >= MIN_FIAT_THRESHOLD ||\n isTokenSelectedForVisibility(token) ||\n isPrioritySearchMatch(token, query);\n\n for (const group of groupedFiltered) {\n if (!group.isUnifiedCandidate) {\n for (const token of group.tokens) {\n if (!isVisibleTokenRow(token)) continue;\n rows.push({\n fiat: getTokenFiatValue(token),\n key: `token:${token.chainId}:${token.contractAddress}`,\n kind: \"token\",\n label: `${token.symbol} ${token.chainName}`,\n searchScore: getSearchScore(token),\n token,\n });\n }\n continue;\n }\n\n const unifiedSelectedInOther = isGroupUnifiedSelectedInOtherSlot(group);\n const unifiedSelectedInCurrent =\n isGroupUnifiedSelectedInCurrentSlot(group);\n const anyIndividualSelectedInOther =\n isAnyTokenInGroupSelectedInOtherSlot(group);\n const anyIndividualSelectedInCurrent = group.tokens.some(\n isTokenSelectedInCurrentSlot,\n );\n const shouldHideUnifiedRow =\n anyIndividualSelectedInOther ||\n anyIndividualSelectedInCurrent ||\n (group.totalFiat < MIN_FIAT_THRESHOLD &&\n !isUnifiedSelectedForVisibility(group.symbol));\n const shouldHideIndividualRows =\n unifiedSelectedInOther || unifiedSelectedInCurrent;\n\n if (!unifiedSelectedInOther && !shouldHideUnifiedRow) {\n rows.push({\n fiat: group.totalFiat,\n group,\n key: `group:${group.symbol}`,\n kind: \"group\",\n label: group.symbol,\n searchScore: hasQuery\n ? Math.min(\n ...group.tokens.map(\n (token) =>\n getTokenSearchRank(token, query)?.score ??\n Number.MAX_SAFE_INTEGER,\n ),\n )\n : 0,\n });\n }\n\n if (!shouldHideIndividualRows) {\n for (const token of group.tokens) {\n if (!isVisibleTokenRow(token)) continue;\n rows.push({\n fiat: getTokenFiatValue(token),\n key: `token:${token.chainId}:${token.contractAddress}`,\n kind: \"token\",\n label: `${token.symbol} ${token.chainName}`,\n searchScore: getSearchScore(token),\n token,\n });\n }\n }\n }\n\n return rows.sort((a, b) => {\n if (hasQuery && a.searchScore !== b.searchScore) {\n return a.searchScore - b.searchScore;\n }\n if (a.fiat !== b.fiat) return b.fiat - a.fiat;\n if (a.kind !== b.kind) return a.kind === \"group\" ? -1 : 1;\n return a.label.localeCompare(b.label);\n });\n }, [\n groupedFiltered,\n isMulti,\n isGroupUnifiedSelectedInCurrentSlot,\n isGroupUnifiedSelectedInOtherSlot,\n isAnyTokenInGroupSelectedInOtherSlot,\n isTokenSelectedInCurrentSlot,\n isTokenSelectedForVisibility,\n isUnifiedSelectedForVisibility,\n query,\n showBelowMinimumInline,\n ]);\n\n const handleFilterTabClick = (tab: FilterTab) => {\n setActiveTab(tab);\n if (autoSelectFilterTabs && isMulti && tab !== \"custom\") {\n if (tab === \"all\" && onFilterTabSelect) {\n onFilterTabSelect(tab);\n return;\n }\n if (filterTabBehavior === \"source-pool\") {\n onFilterTabSelect?.(tab);\n return;\n }\n if (!onSelectionChange) return;\n emitSelectionChange(getFilterTabTokens(tab));\n }\n };\n\n const handleClearSelection = () => {\n if (isMulti && onSelectionChange) {\n setActiveTab(\"custom\");\n emitSelectionChange([]);\n return;\n }\n onClearSelection?.();\n };\n\n const handleMultiTokenToggle = (token: SwapTokenOption) => {\n if (!autoSelectFilterTabs || !isMulti || !onSelectionChange) {\n onToggle?.(token);\n return;\n }\n\n pendingSelectionScrollTopRef.current = listRef.current?.scrollTop ?? null;\n skipNextSelectionScrollResetRef.current = true;\n setActiveTab(\"custom\");\n const current = mergeTokenOptions(\n activeSelectedTokens,\n lockedSelectedTokens,\n );\n const targets =\n token.isUnified && token.sourceTokens?.length\n ? token.sourceTokens\n : [token];\n const unlockedTargets = targets.filter((target) => !isLockedToken(target));\n if (unlockedTargets.length === 0) return;\n\n const allTargetsSelected = unlockedTargets.every((target) =>\n current.some((item) => sameTokenOption(item, target)),\n );\n const next = allTargetsSelected\n ? removeTokenOptions(current, unlockedTargets)\n : mergeTokenOptions(current, unlockedTargets);\n emitSelectionChange(next);\n };\n\n /* ── Render a single-chain token row ── */\n const renderTokenRow = (\n token: SwapTokenOption,\n indent = false,\n isDisabledByUnified = false,\n ) => {\n const selectedInOther = !isMulti && isTokenSelectedInOtherSlot(token);\n if (selectedInOther) return null;\n\n const selectedInCurrent = isTokenSelectedInCurrentSlot(token);\n const locked = isLockedToken(token);\n const disabled = isDisabledByUnified || locked;\n return (\n {\n if (disabled) return;\n if (isMulti) {\n handleMultiTokenToggle(token);\n } else if (\n allowSelectedTokenRemoval &&\n selectedInCurrent &&\n onToggle\n ) {\n onToggle(token);\n } else onSelect(token);\n }}\n style={{\n width: \"100%\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"space-between\",\n padding: \"10px 14px\",\n paddingLeft: indent ? \"36px\" : \"14px\",\n backgroundColor: \"transparent\",\n border: \"none\",\n cursor: disabled ? \"not-allowed\" : \"pointer\",\n borderBottom: \"1px solid #F0F0EF\",\n boxSizing: \"border-box\",\n opacity: isDisabledByUnified ? 0.5 : 1,\n }}\n >\n
\n \n
\n \n
\n \n \n {token.symbol}\n \n {token.chainName && (\n
\n \n \n {token.chainName}\n \n
\n )}\n
\n \n \n \n {formatBalanceWithSymbol(token)}\n \n \n ≈ {token.balanceInFiat}\n \n \n \n );\n };\n\n /* ── Render a unified (multi-chain) group row ── */\n const renderGroupRow = (\n group: (typeof groupedFiltered)[0],\n includeIndividualRows = true,\n ) => {\n if (!group.isUnifiedCandidate) {\n return group.tokens\n .filter(\n (token) =>\n getTokenFiatValue(token) >= MIN_FIAT_THRESHOLD ||\n isTokenSelectedForVisibility(token) ||\n isPrioritySearchMatch(token, query),\n )\n .map((token) => renderTokenRow(token));\n }\n\n const unifiedSelectedInOther =\n !isMulti && isGroupUnifiedSelectedInOtherSlot(group);\n if (unifiedSelectedInOther) return null;\n\n const individualTokens = group.tokens.filter(\n (token) =>\n getTokenFiatValue(token) >= MIN_FIAT_THRESHOLD ||\n isTokenSelectedForVisibility(token) ||\n isPrioritySearchMatch(token, query),\n );\n const hasVisibleUnifiedRow =\n (group.totalFiat >= MIN_FIAT_THRESHOLD ||\n isUnifiedSelectedForVisibility(group.symbol)) &&\n !unifiedSelectedInOther;\n const visibleTokensCount = individualTokens.filter(\n (t) => !isTokenSelectedInOtherSlot(t),\n ).length;\n if (!hasVisibleUnifiedRow && visibleTokensCount === 0) return null;\n\n const isExpanded = expandedGroups.has(group.symbol);\n const unifiedSelectedInCurrent = isGroupUnifiedSelectedInCurrentSlot(group);\n const anyIndividualSelectedInOther =\n isAnyTokenInGroupSelectedInOtherSlot(group);\n const anyIndividualSelectedInCurrent = group.tokens.some(\n isTokenSelectedInCurrentSlot,\n );\n const selectedChildCount = group.tokens.filter(\n isTokenSelectedInCurrentSlot,\n ).length;\n const areAllChildrenSelected =\n group.tokens.length > 0 && selectedChildCount === group.tokens.length;\n const isPartiallySelected =\n selectedChildCount > 0 && selectedChildCount < group.tokens.length;\n const shouldHideUnifiedRow =\n !isMulti &&\n (anyIndividualSelectedInOther ||\n anyIndividualSelectedInCurrent ||\n (group.totalFiat < MIN_FIAT_THRESHOLD &&\n !isUnifiedSelectedForVisibility(group.symbol)));\n const shouldHideIndividualRows =\n !isMulti && (unifiedSelectedInOther || unifiedSelectedInCurrent);\n const unifiedToken: SwapTokenOption = {\n ...group.tokens[0],\n balance: String(group.totalBalRaw),\n balanceInFiat: group.totalFiatStr,\n chainId: undefined,\n chainName: \"All Chains\",\n chainLogo: \"/nexus-widget/all-chains.png\",\n contractAddress: `${group.symbol}-UNIFIED`,\n isUnified: true,\n name: group.symbol,\n sourceTokens: group.tokens,\n symbol: group.symbol,\n unifiedSymbol: group.symbol as \"USDC\" | \"USDT\" | \"ETH\",\n };\n\n return (\n \n {!shouldHideUnifiedRow && (\n {\n if (isMulti) {\n toggleGroup(group.symbol, e);\n return;\n }\n onSelect(unifiedToken);\n }}\n style={{\n width: \"100%\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"space-between\",\n padding: \"10px 14px\",\n backgroundColor: \"transparent\",\n border: \"none\",\n cursor: \"pointer\",\n borderBottom: \"1px solid #F0F0EF\",\n boxSizing: \"border-box\",\n }}\n >\n
\n {\n e.stopPropagation();\n if (isMulti) handleMultiTokenToggle(unifiedToken);\n else onSelect(unifiedToken);\n }}\n style={{ cursor: \"pointer\" }}\n >\n \n
\n \n \n \n \n
\n \n {group.symbol}\n \n \n UNIFIED\n \n
\n \n \n \n
\n \n \n {group.totalBalStr}\n \n \n ≈ {group.totalFiatStr}\n \n
\n \n \n )}\n {isMulti ? (\n \n
\n {group.tokens.map((token) => renderTokenRow(token, true, false))}\n
\n \n ) : (\n includeIndividualRows &&\n !shouldHideIndividualRows &&\n individualTokens.map((token) => renderTokenRow(token))\n )}\n \n );\n };\n\n const isLoading = !staticOptions && swapBalance === null;\n const selectedAssetCount = activeSelectedTokens.length;\n const requiredUsdAmount = parseTokenAmount(requiredUsd);\n const shouldCountSelectedUsd = (token: SwapTokenOption, value: Decimal) =>\n value.gt(0) && (isLockedToken(token) || value.gte(MIN_FIAT_THRESHOLD));\n const selectedUsdAmount = activeSelectedTokens.reduce((sum, token) => {\n if (token.isUnified && token.sourceTokens?.length) {\n return sum.plus(\n token.sourceTokens.reduce((sourceSum, source) => {\n const value =\n parseTokenAmount(source.balanceInFiat) ?? new Decimal(0);\n return shouldCountSelectedUsd(source, value)\n ? sourceSum.plus(value)\n : sourceSum;\n }, new Decimal(0)),\n );\n }\n const value = parseTokenAmount(token.balanceInFiat) ?? new Decimal(0);\n return shouldCountSelectedUsd(token, value) ? sum.plus(value) : sum;\n }, new Decimal(0));\n const selectionDeficitUsdAmount =\n requiredUsdAmount && selectedUsdAmount.lt(requiredUsdAmount)\n ? requiredUsdAmount.minus(selectedUsdAmount)\n : new Decimal(0);\n const hasSelectionShortfall = Boolean(\n requiredUsdAmount &&\n requiredUsdAmount.gt(0) &&\n selectionDeficitUsdAmount.gt(0),\n );\n const shouldShowSelectionProgress = Boolean(\n isMulti && hasSelectionShortfall,\n );\n const selectionProgressPercent =\n shouldShowSelectionProgress && requiredUsdAmount\n ? Decimal.min(\n 100,\n selectedUsdAmount.div(requiredUsdAmount).mul(100),\n ).toNumber()\n : 0;\n const subtitle = isMulti\n ? `${selectedAssetCount} asset${selectedAssetCount === 1 ? \"\" : \"s\"} selected`\n : \"\";\n\n useEffect(() => {\n setPortalRoot(\n selectorRef.current?.closest(\n \"[data-nexus-widget-root]\",\n ) as HTMLElement | null,\n );\n }, []);\n\n useEffect(() => {\n return () => {\n if (chainCloseTimerRef.current) {\n clearTimeout(chainCloseTimerRef.current);\n }\n };\n }, []);\n\n const openChainSelector = () => {\n if (chainCloseTimerRef.current) {\n clearTimeout(chainCloseTimerRef.current);\n chainCloseTimerRef.current = null;\n }\n setChainQuery(\"\");\n setIsChainSelectorClosing(false);\n setShowChainSelector(true);\n };\n\n const closeChainSelector = () => {\n if (chainCloseTimerRef.current) {\n clearTimeout(chainCloseTimerRef.current);\n }\n setIsChainSelectorClosing(true);\n chainCloseTimerRef.current = setTimeout(() => {\n setShowChainSelector(false);\n setIsChainSelectorClosing(false);\n chainCloseTimerRef.current = null;\n }, CHAIN_SELECTOR_CLOSE_MS);\n };\n\n const chainOptions = useMemo(() => {\n const options = new Map();\n\n for (const chain of swapSupportedChains ?? []) {\n if (\n !sdkSwapSupportedChainIds &&\n !SWAP_CHAIN_DISPLAY_ORDER_SET.has(chain.id)\n ) {\n continue;\n }\n if (!isSwapSupportedBySdkChainList(chain.id, swapSupportedChains)) {\n continue;\n }\n options.set(chain.id, {\n contractAddress: \"\",\n symbol: \"\",\n name: getShortChainName(chain.id, chain.name),\n decimals: 18,\n balance: \"0\",\n balanceInFiat: \"$0.00\",\n chainId: chain.id,\n chainName: getShortChainName(chain.id, chain.name),\n chainLogo: CHAIN_METADATA[chain.id]?.logo ?? chain.logo,\n });\n }\n\n for (const token of allTokens) {\n if (\n !token.chainId ||\n (!sdkSwapSupportedChainIds &&\n !SWAP_CHAIN_DISPLAY_ORDER_SET.has(token.chainId)) ||\n !isSwapSupportedBySdkChainList(token.chainId, swapSupportedChains)\n ) {\n continue;\n }\n if (!options.has(token.chainId)) {\n options.set(token.chainId, token);\n }\n }\n\n return Array.from(options.values()).sort(compareChainsBySwapDisplayOrder);\n }, [allTokens, sdkSwapSupportedChainIds, swapSupportedChains]);\n\n const selectedChainToken =\n selectedChainFilter === null\n ? undefined\n : chainOptions.find((token) => token.chainId === selectedChainFilter);\n const selectedChainLabel =\n selectedChainFilter === null\n ? \"All chains\"\n : selectedChainToken?.chainName || \"Chain\";\n\n const handleDone = () => {\n if (hasSelectionShortfall) return;\n onDone?.(\n isMulti\n ? mergeTokenOptions(draftSelectedTokens, lockedSelectedTokens)\n : undefined,\n );\n };\n\n return (\n \n {/* Drawer Handle */}\n \n \n \n\n {/* Header */}\n \n \n \n \n \n \n {title}\n \n {subtitle && (\n \n {subtitle}\n \n )}\n \n {isMulti &&\n selectedAssetCount > 0 &&\n (onClearSelection || onSelectionChange) && (\n \n Deselect all\n \n )}\n \n\n {/* Search */}\n
\n \n \n setIsSearchFocused(false)}\n onChange={(e) => setQuery(e.target.value)}\n onFocus={() => setIsSearchFocused(true)}\n placeholder=\"Search token, chain or address\"\n style={{\n flex: 1,\n backgroundColor: \"transparent\",\n border: \"none\",\n outline: \"none\",\n fontFamily: '\"Geist\", system-ui, sans-serif',\n fontSize: 14,\n color: \"#161615\",\n minWidth: 0,\n }}\n value={query}\n />\n {query && (\n setQuery(\"\")}\n style={{\n background: \"none\",\n border: \"none\",\n cursor: \"pointer\",\n padding: 0,\n }}\n >\n \n \n )}\n {/* Chain Selector Badge */}\n \n {selectedChainFilter === null ? (\n \n ) : (\n \n )}\n \n {selectedChainLabel}\n \n \n \n
\n \n\n {/* Filter tabs */}\n \n {visibleFilterTabs.map((tab) => (\n handleFilterTabClick(tab.key)}\n disabled={tab.key === \"custom\" && autoSelectFilterTabs}\n style={{\n flex: 1,\n padding: \"6px 0\",\n backgroundColor:\n activeTab === tab.key ? \"#FFFFFE\" : \"transparent\",\n border: \"none\",\n borderRadius: 6,\n cursor: \"pointer\",\n fontFamily: '\"Geist\", system-ui, sans-serif',\n fontSize: 13,\n fontWeight: 500,\n color: activeTab === tab.key ? \"#161615\" : \"#848483\",\n boxShadow:\n activeTab === tab.key ? \"0px 1px 2px rgba(0,0,0,0.05)\" : \"none\",\n transition: \"all 0.15s\",\n }}\n >\n {autoSelectFilterTabs && tab.key === \"all\" ? \"Any\" : tab.label}\n \n ))}\n \n\n {/* Token list */}\n \n {isLoading ? (\n \n \n \n Loading assets…\n

\n \n ) : aboveMin.length === 0 && belowMin.length === 0 ? (\n \n No tokens found\n

\n ) : (\n
\n {groupedFiltered.length > 0 && (\n \n {selectorRows.map((row) =>\n row.kind === \"group\"\n ? renderGroupRow(row.group, isMulti)\n : renderTokenRow(row.token),\n )}\n
\n )}\n\n {belowMin.length > 0 && (\n \n setShowBelowMin((v) => !v)}\n style={{\n width: \"100%\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"space-between\",\n padding: \"11px 12px\",\n backgroundColor: \"transparent\",\n border: \"none\",\n cursor: \"pointer\",\n boxSizing: \"border-box\",\n }}\n >\n \n \n \n \n \n \n Tokens below minimum\n \n \n {showBelowMin\n ? \"Tokens under $1 are unavailable for swaps\"\n : \"Hidden to prevent failed swaps\"}\n \n \n \n \n {/* Small token logo cluster */}\n
\n {belowMin.slice(0, 3).map((t, i) =>\n 0 ? -6 : 0,\n }}\n />,\n )}\n {belowMin.length > 3 && (\n \n +{belowMin.length - 3}\n
\n )}\n \n {showBelowMin ? (\n \n ) : (\n \n )}\n \n \n \n
\n {belowMin.map((token, index) => (\n \n \n \n \n {token.chainLogo && (\n \n )}\n
\n \n {token.symbol} on{\" \"}\n {token.chainName || \"Unknown chain\"}\n \n \n \n {token.balanceInFiat}\n \n \n ))}\n \n \n \n )}\n \n )}\n \n\n {/* Done button */}\n {isMulti && (\n
\n {shouldShowSelectionProgress && requiredUsdAmount && (\n \n \n \n Required\n \n \n \n {formatUsdBalanceLabel(selectionDeficitUsdAmount)}\n {\" \"}\n more\n \n
\n \n \n \n \n )}\n {!hasSelectionShortfall && (\n \n Done\n \n )}\n \n )}\n\n {/* Chain Selector Modal */}\n {showChainSelector &&\n (() => {\n const chainModal = (\n \n \n \n \n \n \n \n \n \n \n \n Select chain\n \n \n\n {/* Search */}\n
\n \n \n setIsChainSearchFocused(false)}\n onChange={(e) => setChainQuery(e.target.value)}\n onFocus={() => setIsChainSearchFocused(true)}\n placeholder=\"Search chains\"\n style={{\n flex: 1,\n backgroundColor: \"transparent\",\n border: \"none\",\n outline: \"none\",\n fontFamily: '\"Geist\", system-ui, sans-serif',\n fontSize: 13,\n color: \"#161615\",\n }}\n value={chainQuery}\n />\n
\n \n\n {/* Chain list */}\n \n \n {\n setSelectedChainFilter(null);\n closeChainSelector();\n }}\n style={{\n width: \"100%\",\n display: \"flex\",\n alignItems: \"center\",\n padding: \"8px 14px\",\n backgroundColor: \"transparent\",\n border: \"none\",\n borderBottom: \"1px solid #F0F0EF\",\n cursor: \"pointer\",\n boxSizing: \"border-box\",\n }}\n >\n \n \n \n All Chains\n \n \n\n {/* Unique chains */}\n {chainOptions\n .filter((t) =>\n (t.chainName || \"\")\n .toLowerCase()\n .includes(chainQuery.toLowerCase()),\n )\n .map((t) => (\n {\n setSelectedChainFilter(t.chainId!);\n closeChainSelector();\n }}\n style={{\n width: \"100%\",\n display: \"flex\",\n alignItems: \"center\",\n padding: \"8px 14px\",\n backgroundColor: \"transparent\",\n border: \"none\",\n borderBottom: \"1px solid #F0F0EF\",\n cursor: \"pointer\",\n boxSizing: \"border-box\",\n }}\n >\n \n \n \n {t.chainName}\n \n \n ))}\n \n \n \n \n );\n return portalRoot ? createPortal(chainModal, portalRoot) : chainModal;\n })()}\n \n );\n}\n", "type": "registry:component", "target": "components/nexus/components/swap-asset-selector.tsx" }, @@ -92,13 +92,13 @@ }, { "path": "registry/avail-widgets/nexus-widget/components/swap-intent-preview.tsx", - "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\n\"use client\";\n\nimport Decimal from \"decimal.js\";\nimport { ChevronDown, Info, Loader2 } from \"lucide-react\";\nimport React, { useRef, useState } from \"react\";\nimport type { SwapStepType } from \"../../common/types/transaction-flow\";\nimport { CHAIN_METADATA, getShortChainName } from \"../../common/utils/constant\";\nimport TransactionProgress from \"../../swaps/components/transaction-progress\";\nimport { Button } from \"../../ui/button\";\nimport {\n type NexusWidgetDepositOpportunityMetadata,\n type NexusWidgetMode,\n} from \"../types\";\nimport { type SwapTokenOption } from \"./swap-asset-selector\";\n\nexport interface SwapIntentSource {\n amount: string;\n chain: { id: number; logo: string; name: string };\n token: {\n contractAddress: string;\n decimals: number;\n logo?: string;\n symbol: string;\n };\n value?: string;\n}\n\nexport interface SwapIntentDestination {\n amount: string;\n chain: { id: number; logo: string; name: string };\n gas: {\n amount: string;\n value?: string;\n token: {\n contractAddress: string;\n decimals: number;\n logo?: string;\n symbol: string;\n };\n };\n token: {\n contractAddress: string;\n decimals: number;\n logo?: string;\n symbol: string;\n };\n value?: string;\n}\n\nexport type BridgeProvider = \"nexus\" | \"mayan\" | null;\n\nexport interface SwapIntentData {\n bridgeProvider?: BridgeProvider;\n destination: SwapIntentDestination;\n feesAndBuffer?: {\n buffer?: string;\n bridge?:\n | {\n caGas?: string;\n collection?: string;\n fulfilment?: string;\n gasSupplied?: string;\n protocol?: string;\n solver?: string;\n total?: string;\n }\n | string\n | null;\n };\n sources: SwapIntentSource[];\n}\n\nexport interface SwapIntentPreviewProps {\n activeMode?: NexusWidgetMode;\n estimatedTime?: string;\n explorerUrls?: {\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n };\n fromAmount: string;\n fromAmountUsd?: string;\n destinationGasFeeUsd?: string;\n fromToken?: SwapTokenOption;\n fromTokens?: SwapTokenOption[];\n intentData?: SwapIntentData | null;\n isExecuting?: boolean;\n isLoading?: boolean;\n isRefreshing?: boolean;\n mode?: NexusWidgetMode;\n onAccept: () => void;\n onReject: () => void;\n onTransitionChange?: (isTransitioning: boolean) => void;\n opportunity?: NexusWidgetDepositOpportunityMetadata;\n recipientAddress?: string;\n steps?: Array<{ id: number; completed: boolean; step: SwapStepType }>;\n supportedTokenAssets?: any[] | null;\n swapBalances?: any[] | null;\n swapType?: \"exactIn\" | \"exactOut\";\n toAmount?: string;\n toAmountTokens?: string;\n toAmountUsd?: string;\n toToken?: SwapTokenOption;\n totalFeeUsd?: string;\n}\n\nconst fontFamily = '\"Geist\", var(--font-geist-sans), system-ui, sans-serif';\nconst primary = \"var(--foreground-primary, #161615)\";\nconst muted = \"var(--foreground-muted, #848483)\";\nconst border = \"var(--border-default, #E8E8E7)\";\nconst brand = \"var(--foreground-brand)\";\n\nconst stripNumeric = (value: unknown) => String(value).replace(/[^0-9.-]/g, \"\");\n\nconst parseDecimal = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = stripNumeric(value);\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst toDecimal = (value: unknown) => parseDecimal(value) ?? new Decimal(0);\n\nconst formatAmount = (\n value: unknown,\n options: { min?: number; max?: number } = {},\n) => {\n const amount = toDecimal(value);\n const max = options.max ?? 2;\n return amount.toDecimalPlaces(max).toFixed();\n};\n\nconst formatUsdDelta = (value: Decimal) => {\n if (value.gt(0) && value.lt(0.01)) return \"-<0.01 USD\";\n return value.gt(0) ? `-${formatAmount(value)} USD` : \"0 USD\";\n};\n\nconst formatUsdAmount = (value: Decimal) => {\n if (value.gt(0) && value.lt(0.01)) return \"<0.01 USD\";\n return value.gt(0) ? `${formatAmount(value)} USD` : \"0 USD\";\n};\n\nconst formatUsdValue = (value: Decimal) => {\n const absolute = value.abs();\n if (absolute.eq(0)) return \"$0\";\n if (absolute.lt(0.01)) return value.lt(0) ? \"-<$0.01\" : \"<$0.01\";\n\n const amount = formatAmount(absolute, { max: 2 });\n\n return value.lt(0) ? `-$${amount}` : `$${amount}`;\n};\n\nconst formatTokenAmount = (value: unknown) => {\n const amount = toDecimal(value);\n return amount.toDecimalPlaces(8).toFixed();\n};\n\nconst formatHeaderTokenAmount = (value: unknown) => {\n const amount = toDecimal(value);\n if (amount.isZero()) return \"0\";\n if (amount.abs().gte(1000)) {\n return amount.toDecimalPlaces(2).toFixed();\n }\n if (amount.abs().gte(1)) {\n return amount.toDecimalPlaces(4).toFixed();\n }\n return amount.toDecimalPlaces(8).toFixed();\n};\n\nconst getFontSize = (amountStr: string, symbolStr: string) => {\n const totalLength =\n String(amountStr || \"\").length + String(symbolStr || \"\").length;\n if (totalLength > 16) return \"13px\";\n if (totalLength > 12) return \"15px\";\n return \"17px\";\n};\n\nconst unique = (values: string[]) =>\n Array.from(new Set(values.filter(Boolean)));\n\nconst isNativeTokenAddress = (address?: string) => {\n const lower = (address ?? \"\").toLowerCase();\n return (\n !lower ||\n lower === \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\" ||\n lower === \"0x0000000000000000000000000000000000000000\"\n );\n};\n\nconst getSourceDisplayKey = (\n chainId?: number | string,\n address?: string,\n symbol?: string,\n) => {\n const chainKey = chainId ?? \"\";\n const normalizedAddress = address?.toLowerCase();\n return normalizedAddress\n ? `${chainKey}-${normalizedAddress}`\n : `${chainKey}-symbol-${(symbol ?? \"\").toUpperCase()}`;\n};\n\nconst normalizeIntentToken = <\n T extends {\n contractAddress?: string;\n decimals?: number;\n logo?: string;\n symbol?: string;\n },\n>(\n token: T | undefined,\n chainId?: number,\n) => {\n const chainMeta = chainId ? CHAIN_METADATA[chainId] : undefined;\n const shouldUseNative =\n isNativeTokenAddress(token?.contractAddress) && Boolean(chainMeta);\n const symbol =\n shouldUseNative && chainMeta?.nativeCurrency.symbol\n ? chainMeta.nativeCurrency.symbol\n : token?.symbol || chainMeta?.nativeCurrency.symbol || \"-\";\n const decimals =\n shouldUseNative && chainMeta?.nativeCurrency.decimals !== undefined\n ? chainMeta.nativeCurrency.decimals\n : (token?.decimals ?? chainMeta?.nativeCurrency.decimals ?? 18);\n\n return {\n contractAddress: token?.contractAddress ?? \"\",\n decimals,\n logo: token?.logo || (shouldUseNative ? chainMeta?.logo : undefined),\n symbol,\n };\n};\n\nconst formatSymbolSummary = (symbols: string[]) => {\n const visible = unique(symbols);\n if (visible.length === 0) return \"-\";\n if (visible.length <= 2) return visible.join(\", \");\n if (visible.length === 3)\n return `${visible[0]}, ${visible[1]} and ${visible[2]}`;\n\n const others = visible.length - 2;\n return `${visible[0]}, ${visible[1]} and ${others} other${others === 1 ? \"\" : \"s\"}`;\n};\n\nconst sortSourceDetailRowsByUsdDesc = <\n T extends { symbol?: string; usdAmount?: string },\n>(\n rows: T[],\n) =>\n [...rows].sort((a, b) => {\n const usdDelta = toDecimal(b.usdAmount).cmp(toDecimal(a.usdAmount));\n if (usdDelta !== 0) return usdDelta;\n return (a.symbol ?? \"\").localeCompare(b.symbol ?? \"\");\n });\n\nfunction IntentLogo({\n src,\n alt,\n label,\n size,\n fontSize,\n outline,\n style,\n}: {\n src?: string;\n alt?: string;\n label?: string;\n size: number;\n fontSize: number;\n outline?: string;\n style?: React.CSSProperties;\n}) {\n const [failed, setFailed] = useState(!src);\n\n React.useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n const fallbackLabel = (label || alt || \"?\").trim().slice(0, 1).toUpperCase();\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n backgroundColor: \"#FFFFFE\",\n borderRadius: \"999px\",\n height: `${size}px`,\n objectFit: \"cover\",\n outline,\n width: `${size}px`,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {fallbackLabel || \"?\"}\n \n );\n}\n\nfunction DetailToggle({\n expanded,\n onClick,\n}: {\n expanded: boolean;\n onClick: () => void;\n}) {\n return (\n \n {expanded ? \"Hide Details\" : \"View Details\"}\n \n \n );\n}\n\nfunction TruncatedAddress({ address }: { address: string }) {\n const [showTooltip, setShowTooltip] = useState(false);\n const label =\n address.length > 12\n ? `${address.slice(0, 6)}...${address.slice(-4)}`\n : address;\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n style={{\n color: brand,\n display: \"inline-flex\",\n fontFamily,\n fontSize: \"13px\",\n fontWeight: 500,\n lineHeight: \"15px\",\n outline: \"none\",\n position: \"relative\",\n }}\n tabIndex={0}\n >\n {label}\n {showTooltip && (\n \n {address}\n \n )}\n \n );\n}\n\nfunction RecipientRow({ address }: { address: string }) {\n return (\n \n
\n \n Recipient\n
\n \n Wallet address\n \n \n
\n \n
\n \n );\n}\n\nfunction InlineInfoTooltip({ message }: { message: string }) {\n const [showTooltip, setShowTooltip] = useState(false);\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n style={{\n alignItems: \"center\",\n color: muted,\n display: \"inline-flex\",\n lineHeight: 0,\n outline: \"none\",\n position: \"relative\",\n }}\n tabIndex={0}\n >\n \n {showTooltip && (\n \n {message}\n \n )}\n \n );\n}\n\nfunction MayanPoweredBadge() {\n return (\n \n \n This transaction is powered by\n \n \n );\n}\n\nfunction Row({\n title,\n subtitle,\n value,\n secondaryValue,\n children,\n}: {\n title: React.ReactNode;\n subtitle: string;\n value: string;\n secondaryValue?: string;\n children?: React.ReactNode;\n}) {\n return (\n \n
\n \n {title}\n
\n \n {subtitle}\n \n \n \n \n {value}\n \n {secondaryValue ? (\n \n {secondaryValue}\n \n ) : (\n children\n )}\n \n \n );\n}\n\nfunction AnimatedDetails({\n open,\n children,\n background = \"#F9F9F8\",\n gap = \"9px\",\n padding = \"12px 14px\",\n}: {\n open: boolean;\n children: React.ReactNode;\n background?: string;\n gap?: string;\n padding?: string;\n}) {\n return (\n \n
\n \n {children}\n
\n \n \n );\n}\n\nexport function SwapIntentPreview({\n fromTokens,\n fromToken,\n toToken,\n fromAmount,\n fromAmountUsd,\n destinationGasFeeUsd,\n toAmount,\n toAmountUsd,\n toAmountTokens,\n totalFeeUsd,\n isLoading,\n isRefreshing,\n isExecuting,\n swapType,\n intentData,\n mode,\n opportunity,\n recipientAddress,\n activeMode,\n steps,\n explorerUrls,\n onAccept,\n onTransitionChange,\n}: SwapIntentPreviewProps) {\n const [showSourceDetails, setShowSourceDetails] = useState(false);\n const [showFeeDetails, setShowFeeDetails] = useState(false);\n const [showImpactDetails, setShowImpactDetails] = useState(false);\n const sourceDetailsScrollRef = useRef(null);\n const [transitionTimeoutId, setTransitionTimeoutId] = useState | null>(null);\n\n const startTransition = () => {\n if (onTransitionChange) {\n onTransitionChange(true);\n }\n if (transitionTimeoutId) {\n clearTimeout(transitionTimeoutId);\n }\n const id = setTimeout(() => {\n if (onTransitionChange) {\n onTransitionChange(false);\n }\n setTransitionTimeoutId(null);\n }, 280);\n setTransitionTimeoutId(id);\n };\n\n React.useEffect(() => {\n return () => {\n if (transitionTimeoutId) {\n clearTimeout(transitionTimeoutId);\n }\n };\n }, [transitionTimeoutId]);\n\n const flowMode = mode ?? activeMode ?? \"swap\";\n const isDepositMode = flowMode === \"deposit\";\n const isSendMode = flowMode === \"send\";\n const hasRecipientTransfer = Boolean(recipientAddress) && !isDepositMode;\n const isExactOutDisplayFlow =\n (isDepositMode || isSendMode) && swapType === \"exactOut\";\n const shouldShowSwapBuffer = swapType !== \"exactIn\";\n const intentSources = intentData?.sources ?? [];\n const intentDest = intentData?.destination;\n const normalizedIntentSources = intentSources.map((source) => ({\n ...source,\n token: normalizeIntentToken(source.token, source.chain.id),\n }));\n const normalizedIntentDest = intentDest\n ? {\n ...intentDest,\n token: normalizeIntentToken(intentDest.token, intentDest.chain.id),\n gas: {\n ...intentDest.gas,\n token: normalizeIntentToken(\n intentDest.gas?.token,\n intentDest.chain.id,\n ),\n },\n }\n : undefined;\n const fallbackSources =\n fromTokens && fromTokens.length > 0\n ? fromTokens\n : fromToken\n ? [fromToken]\n : [];\n\n const baseSourceSymbols =\n normalizedIntentSources.length > 0\n ? unique(normalizedIntentSources.map((source) => source.token.symbol))\n : unique(fallbackSources.map((source) => source.symbol));\n const baseSourceAssetCount =\n normalizedIntentSources.length ||\n fallbackSources.length ||\n baseSourceSymbols.length;\n const hasResolvedQuote = Boolean(\n normalizedIntentDest &&\n (normalizedIntentSources.length > 0 || isExactOutDisplayFlow),\n );\n const quoteUnavailable = !isLoading && !hasResolvedQuote;\n\n const destTokenSymbol =\n normalizedIntentDest?.token.symbol ||\n toToken?.symbol ||\n opportunity?.tokenSymbol ||\n \"-\";\n const destChainName =\n flowMode === \"deposit\"\n ? opportunity?.title || opportunity?.protocol || \"App\"\n : getShortChainName(\n normalizedIntentDest?.chain.id ?? toToken?.chainId,\n normalizedIntentDest?.chain.name || toToken?.chainName || \"\",\n );\n\n const requestedDestinationAmount = isExactOutDisplayFlow\n ? parseDecimal(toAmountTokens ?? toAmount)\n : undefined;\n const quotedDestinationAmount = parseDecimal(normalizedIntentDest?.amount);\n const destinationBalanceAmount = parseDecimal(toToken?.balance);\n const displayOnlyDestinationCoverage = (() => {\n if (\n !isExactOutDisplayFlow ||\n !requestedDestinationAmount ||\n requestedDestinationAmount.lte(0) ||\n !destinationBalanceAmount ||\n destinationBalanceAmount.lte(0)\n ) {\n return undefined;\n }\n\n const externallyProducedAmount =\n normalizedIntentSources.length > 0 &&\n quotedDestinationAmount &&\n quotedDestinationAmount.gt(0)\n ? quotedDestinationAmount\n : new Decimal(0);\n const destinationBalanceAmountNeeded = Decimal.max(\n requestedDestinationAmount.minus(externallyProducedAmount),\n 0,\n );\n const coveredAmount = Decimal.min(\n destinationBalanceAmountNeeded,\n destinationBalanceAmount,\n );\n return coveredAmount.gt(0) ? coveredAmount : undefined;\n })();\n const displayOnlyDestinationSourceAmount = displayOnlyDestinationCoverage;\n const requestedDestinationUsd = parseDecimal(toAmountUsd);\n const destinationDisplayUsdRate =\n requestedDestinationAmount &&\n requestedDestinationAmount.gt(0) &&\n requestedDestinationUsd &&\n requestedDestinationUsd.gt(0)\n ? requestedDestinationUsd.div(requestedDestinationAmount)\n : quotedDestinationAmount &&\n quotedDestinationAmount.gt(0) &&\n normalizedIntentDest?.value\n ? (parseDecimal(normalizedIntentDest.value) ?? new Decimal(0)).div(\n quotedDestinationAmount,\n )\n : undefined;\n const displayOnlyDestinationCoverageUsd =\n displayOnlyDestinationCoverage &&\n displayOnlyDestinationCoverage.gt(0) &&\n destinationDisplayUsdRate &&\n destinationDisplayUsdRate.gt(0)\n ? displayOnlyDestinationCoverage.mul(destinationDisplayUsdRate)\n : undefined;\n const displayOnlyDestinationSourceUsd =\n displayOnlyDestinationSourceAmount &&\n displayOnlyDestinationSourceAmount.gt(0) &&\n destinationDisplayUsdRate &&\n destinationDisplayUsdRate.gt(0)\n ? displayOnlyDestinationSourceAmount.mul(destinationDisplayUsdRate)\n : undefined;\n\n const intentSourceUsdValues = normalizedIntentSources.map((source) =>\n parseDecimal(source.value),\n );\n const intentSourceUsdNumber =\n normalizedIntentSources.length > 0\n ? intentSourceUsdValues.every((value) => value !== undefined)\n ? intentSourceUsdValues.reduce(\n (sum, value) => sum.plus(value ?? 0),\n new Decimal(0),\n )\n : parseDecimal(fromAmountUsd)\n : parseDecimal(fromAmountUsd);\n const effectiveSourceUsdNumber =\n displayOnlyDestinationCoverageUsd !== undefined\n ? (intentSourceUsdNumber ?? new Decimal(0)).plus(\n displayOnlyDestinationCoverageUsd,\n )\n : intentSourceUsdNumber;\n\n const destinationUsdNumber = hasResolvedQuote\n ? isExactOutDisplayFlow\n ? (parseDecimal(toAmountUsd) ?? parseDecimal(normalizedIntentDest?.value))\n : (parseDecimal(normalizedIntentDest?.value) ?? parseDecimal(toAmountUsd))\n : undefined;\n const hasFiatQuote =\n effectiveSourceUsdNumber !== undefined &&\n destinationUsdNumber !== undefined &&\n effectiveSourceUsdNumber.gt(0) &&\n destinationUsdNumber.gt(0);\n\n const bridgeFees = intentData?.feesAndBuffer?.bridge;\n const bridgeFeeData =\n bridgeFees && typeof bridgeFees === \"object\" ? bridgeFees : undefined;\n const bridgeTotalNumber =\n typeof bridgeFees === \"string\"\n ? parseDecimal(bridgeFees)\n : parseDecimal(bridgeFeeData?.total);\n const collectionFeeNumber = parseDecimal(bridgeFeeData?.collection);\n const fulfilmentFeeNumber = parseDecimal(bridgeFeeData?.fulfilment);\n const executionGasFeeNumber =\n parseDecimal(bridgeFeeData?.caGas) ??\n (collectionFeeNumber !== undefined || fulfilmentFeeNumber !== undefined\n ? (collectionFeeNumber ?? new Decimal(0)).plus(\n fulfilmentFeeNumber ?? new Decimal(0),\n )\n : undefined);\n const protocolFeeNumber = parseDecimal(bridgeFeeData?.protocol);\n const solverFeeNumber = parseDecimal(bridgeFeeData?.solver);\n const bridgeGasSuppliedNumber = parseDecimal(bridgeFeeData?.gasSupplied);\n const destinationGasValueNumber =\n parseDecimal(normalizedIntentDest?.gas?.value) ??\n parseDecimal(destinationGasFeeUsd);\n const destinationGasSuppliedNumber =\n bridgeGasSuppliedNumber ?? destinationGasValueNumber;\n const bridgeTotalWithDestinationGas =\n bridgeTotalNumber &&\n !bridgeGasSuppliedNumber &&\n destinationGasValueNumber &&\n destinationGasValueNumber.gt(0)\n ? bridgeTotalNumber.plus(destinationGasValueNumber)\n : bridgeTotalNumber;\n const swapBufferNumber = parseDecimal(intentData?.feesAndBuffer?.buffer);\n const bridgeComponentsTotalNumber = bridgeFeeData\n ? [\n executionGasFeeNumber,\n protocolFeeNumber,\n solverFeeNumber,\n destinationGasSuppliedNumber,\n ].reduce(\n (sum, value) => sum.plus(value ?? new Decimal(0)),\n new Decimal(0),\n )\n : undefined;\n const explicitFeeNumber =\n bridgeTotalWithDestinationGas ??\n (bridgeComponentsTotalNumber && bridgeComponentsTotalNumber.gt(0)\n ? bridgeComponentsTotalNumber\n : undefined) ??\n parseDecimal(totalFeeUsd) ??\n parseDecimal((intentData as any)?.fees?.total) ??\n destinationGasSuppliedNumber;\n const feeNumber =\n explicitFeeNumber ?? (hasFiatQuote ? new Decimal(0) : undefined);\n const quotedDestinationUsdNumber = parseDecimal(normalizedIntentDest?.value);\n const exactOutPaidUsdNumber = (() => {\n if (!isExactOutDisplayFlow) return effectiveSourceUsdNumber;\n\n const candidates = [\n effectiveSourceUsdNumber,\n requestedDestinationUsd,\n ].filter((value): value is Decimal => Boolean(value && value.gt(0)));\n\n if (requestedDestinationUsd && requestedDestinationUsd.gt(0)) {\n if (\n intentSourceUsdNumber &&\n intentSourceUsdNumber.gt(0) &&\n quotedDestinationUsdNumber &&\n quotedDestinationUsdNumber.gt(0)\n ) {\n candidates.push(\n requestedDestinationUsd.plus(\n Decimal.max(\n intentSourceUsdNumber.minus(quotedDestinationUsdNumber),\n 0,\n ),\n ),\n );\n }\n\n const knownOverhead = (feeNumber ?? new Decimal(0)).plus(\n swapBufferNumber ?? new Decimal(0),\n );\n if (knownOverhead.gt(0)) {\n candidates.push(requestedDestinationUsd.plus(knownOverhead));\n }\n }\n\n return candidates.reduce(\n (max, value) => (!max || value.gt(max) ? value : max),\n undefined,\n );\n })();\n const quoteImpactUsd =\n hasFiatQuote && feeNumber !== undefined\n ? Decimal.max(\n effectiveSourceUsdNumber\n .minus(destinationUsdNumber)\n .minus(feeNumber)\n .minus(swapBufferNumber ?? new Decimal(0)),\n 0,\n )\n : undefined;\n const priceImpactUsd =\n quoteImpactUsd ?? parseDecimal((intentData as any)?.priceImpactUsd);\n const computedSwapImpactPercent =\n hasFiatQuote && priceImpactUsd !== undefined\n ? priceImpactUsd.eq(0)\n ? new Decimal(0)\n : effectiveSourceUsdNumber !== undefined &&\n effectiveSourceUsdNumber.gt(0)\n ? priceImpactUsd.neg().div(effectiveSourceUsdNumber).mul(100)\n : undefined\n : undefined;\n const swapImpactPercent =\n computedSwapImpactPercent ??\n parseDecimal((intentData as any)?.swapImpactPercent) ??\n parseDecimal((intentData as any)?.priceImpactPercent);\n\n const destinationTokenAmount =\n isExactOutDisplayFlow && (toAmountTokens || toAmount)\n ? toAmountTokens || toAmount || \"0\"\n : normalizedIntentDest?.amount || toAmountTokens || toAmount || \"0\";\n const feeDetailRows = bridgeFeeData\n ? [\n {\n label: \"Execution Gas Fee\",\n value: executionGasFeeNumber ?? new Decimal(0),\n },\n {\n label: \"Protocol Fee\",\n value: protocolFeeNumber ?? new Decimal(0),\n },\n {\n label: \"Solver Fee\",\n value: solverFeeNumber ?? new Decimal(0),\n },\n ...(destinationGasSuppliedNumber && destinationGasSuppliedNumber.gt(0)\n ? [{ label: \"Gas Sponsorship\", value: destinationGasSuppliedNumber }]\n : []),\n ]\n : destinationGasSuppliedNumber && destinationGasSuppliedNumber.gt(0)\n ? [\n ...(feeNumber && feeNumber.gt(destinationGasSuppliedNumber)\n ? [\n {\n label: \"Network & protocol\",\n value: feeNumber.minus(destinationGasSuppliedNumber),\n },\n ]\n : []),\n { label: \"Gas Sponsorship\", value: destinationGasSuppliedNumber },\n ]\n : feeNumber !== undefined\n ? [{ label: \"Network & protocol\", value: feeNumber }]\n : [];\n\n const pendingLabel = isLoading ? \"Fetching quote\" : \"Quote unavailable\";\n const pendingValue = isLoading ? \"...\" : \"--\";\n const sourceUsd =\n exactOutPaidUsdNumber !== undefined\n ? `${formatAmount(exactOutPaidUsdNumber)} USD`\n : pendingValue;\n const receiveUsd = hasFiatQuote\n ? `${formatAmount(destinationUsdNumber)} USD`\n : pendingValue;\n const feeUsd =\n feeNumber !== undefined ? formatUsdAmount(feeNumber) : pendingValue;\n const impactUsd =\n priceImpactUsd !== undefined\n ? formatUsdDelta(priceImpactUsd)\n : pendingValue;\n const impactPercent =\n swapImpactPercent !== undefined\n ? `${formatAmount(swapImpactPercent, {\n min: 2,\n max: 2,\n })}%`\n : pendingValue;\n const destinationHeaderAmount = hasResolvedQuote\n ? formatHeaderTokenAmount(destinationTokenAmount)\n : pendingValue;\n const destinationTokenDisplay = hasResolvedQuote\n ? `${formatTokenAmount(destinationTokenAmount)} ${destTokenSymbol}`\n : pendingLabel;\n const destinationSourceAddress =\n normalizedIntentDest?.token.contractAddress || toToken?.contractAddress;\n const destinationSourceKey = getSourceDisplayKey(\n normalizedIntentDest?.chain.id ?? toToken?.chainId,\n destinationSourceAddress,\n destTokenSymbol,\n );\n const hasDestinationSourceRow = Boolean(\n destinationSourceKey &&\n (normalizedIntentSources.length > 0\n ? normalizedIntentSources.some((source) => {\n const sourceKey = getSourceDisplayKey(\n source.chain.id,\n source.token.contractAddress,\n source.token.symbol,\n );\n return sourceKey === destinationSourceKey;\n })\n : fallbackSources.some((source) => {\n const sourceKey = getSourceDisplayKey(\n source.chainId ?? \"\",\n source.contractAddress,\n source.symbol,\n );\n return sourceKey === destinationSourceKey;\n })),\n );\n const swapBufferDisplay =\n swapBufferNumber !== undefined\n ? formatUsdValue(swapBufferNumber)\n : pendingValue;\n const baseSourceDetailRows =\n normalizedIntentSources.length > 0\n ? normalizedIntentSources.map((source, index) => {\n const fallbackSource = fallbackSources.find(\n (token) =>\n token.chainId === source.chain.id &&\n (token.contractAddress?.toLowerCase() ===\n source.token.contractAddress?.toLowerCase() ||\n token.symbol === source.token.symbol),\n );\n\n const sourceKey = getSourceDisplayKey(\n source.chain.id,\n source.token.contractAddress,\n source.token.symbol,\n );\n const isDestinationSource =\n sourceKey === destinationSourceKey &&\n displayOnlyDestinationSourceAmount !== undefined;\n const sourceAmountNumber = parseDecimal(source.amount);\n const displaySourceAmount =\n isDestinationSource && displayOnlyDestinationSourceAmount\n ? (sourceAmountNumber ?? new Decimal(0)).plus(\n displayOnlyDestinationSourceAmount,\n )\n : sourceAmountNumber;\n const sourceValueNumber = parseDecimal(source.value);\n const displaySourceUsd =\n isDestinationSource && displayOnlyDestinationSourceUsd\n ? (sourceValueNumber ?? new Decimal(0)).plus(\n displayOnlyDestinationSourceUsd,\n )\n : sourceValueNumber;\n const tokenAmountValue =\n displaySourceAmount !== undefined\n ? formatTokenAmount(displaySourceAmount)\n : formatTokenAmount(source.amount);\n\n return {\n key: `${source.chain.id}-${source.token.contractAddress}-${index}`,\n tokenLogo: source.token.logo || fallbackSource?.logo || \"\",\n chainLogo: source.chain.logo || fallbackSource?.chainLogo || \"\",\n symbol: source.token.symbol,\n chainName: getShortChainName(source.chain.id, source.chain.name),\n tokenAmount: `${tokenAmountValue} ${source.token.symbol}`,\n tokenAmountValue,\n usdAmount:\n displaySourceUsd !== undefined\n ? formatUsdValue(displaySourceUsd)\n : pendingValue,\n index,\n };\n })\n : fallbackSources.map((source, index) => {\n const sourceAmount =\n source.userAmount ||\n (fallbackSources.length === 1 ? fromAmount : \"\");\n const sourceKey = getSourceDisplayKey(\n source.chainId ?? \"\",\n source.contractAddress,\n source.symbol,\n );\n const isDestinationSource =\n sourceKey === destinationSourceKey &&\n displayOnlyDestinationSourceAmount !== undefined;\n const sourceAmountNumber = parseDecimal(sourceAmount);\n const displaySourceAmount =\n isDestinationSource && displayOnlyDestinationSourceAmount\n ? (sourceAmountNumber ?? new Decimal(0)).plus(\n displayOnlyDestinationSourceAmount,\n )\n : sourceAmountNumber;\n const sourceUsdNumber =\n source.balanceInFiat && source.balance\n ? toDecimal(source.userAmount || 0).mul(\n toDecimal(source.balanceInFiat).div(\n Decimal.max(toDecimal(source.balance), 1),\n ),\n )\n : undefined;\n const displaySourceUsd =\n isDestinationSource && displayOnlyDestinationSourceUsd\n ? (sourceUsdNumber ?? new Decimal(0)).plus(\n displayOnlyDestinationSourceUsd,\n )\n : sourceUsdNumber;\n const tokenAmountValue =\n displaySourceAmount !== undefined\n ? formatTokenAmount(displaySourceAmount)\n : \"\";\n\n return {\n key: `${source.chainId ?? \"chain\"}-${source.contractAddress}-${index}`,\n tokenLogo: source.logo || \"\",\n chainLogo: source.chainLogo || \"\",\n symbol: source.symbol,\n chainName: getShortChainName(source.chainId, source.chainName),\n tokenAmount: tokenAmountValue\n ? `${tokenAmountValue} ${source.symbol}`\n : pendingLabel,\n tokenAmountValue,\n usdAmount:\n displaySourceUsd !== undefined\n ? formatUsdValue(displaySourceUsd)\n : pendingValue,\n index,\n };\n });\n const displayOnlyDestinationSourceRow =\n displayOnlyDestinationSourceAmount &&\n displayOnlyDestinationSourceAmount.gt(0) &&\n !hasDestinationSourceRow\n ? {\n key: `destination-existing-${normalizedIntentDest?.chain.id ?? toToken?.chainId ?? \"chain\"}-${normalizedIntentDest?.token.contractAddress ?? toToken?.contractAddress ?? \"token\"}`,\n tokenLogo: normalizedIntentDest?.token.logo || toToken?.logo || \"\",\n chainLogo:\n normalizedIntentDest?.chain.logo || toToken?.chainLogo || \"\",\n symbol: destTokenSymbol,\n chainName: getShortChainName(\n normalizedIntentDest?.chain.id ?? toToken?.chainId,\n normalizedIntentDest?.chain.name || toToken?.chainName || \"\",\n ),\n tokenAmount: `${formatTokenAmount(displayOnlyDestinationSourceAmount)} ${destTokenSymbol}`,\n tokenAmountValue: formatTokenAmount(\n displayOnlyDestinationSourceAmount,\n ),\n usdAmount:\n displayOnlyDestinationSourceUsd !== undefined\n ? formatUsdValue(displayOnlyDestinationSourceUsd)\n : pendingValue,\n index: baseSourceDetailRows.length,\n }\n : undefined;\n const sourceDetailRows = sortSourceDetailRowsByUsdDesc(\n displayOnlyDestinationSourceRow\n ? [...baseSourceDetailRows, displayOnlyDestinationSourceRow]\n : baseSourceDetailRows,\n );\n const sourceSymbols = (() => {\n const symbols =\n sourceDetailRows.length > 0\n ? sourceDetailRows.map((source) => source.symbol)\n : baseSourceSymbols;\n return destTokenSymbol && symbols.includes(destTokenSymbol)\n ? unique([\n destTokenSymbol,\n ...symbols.filter((symbol) => symbol !== destTokenSymbol),\n ])\n : unique(symbols);\n })();\n const sourceLabel = formatSymbolSummary(sourceSymbols);\n const sourceAssetCount =\n sourceDetailRows.length || baseSourceAssetCount || sourceSymbols.length;\n const singleSourceHeader = (() => {\n if (displayOnlyDestinationSourceRow) return null;\n if (\n !displayOnlyDestinationSourceRow &&\n normalizedIntentSources.length === 1\n ) {\n const source = normalizedIntentSources[0];\n const sourceRow = baseSourceDetailRows[0];\n return {\n amount:\n sourceRow?.tokenAmountValue ?? formatHeaderTokenAmount(source.amount),\n chainName: getShortChainName(source.chain.id, source.chain.name),\n symbol: source.token.symbol,\n };\n }\n\n if (normalizedIntentSources.length === 0 && fallbackSources.length === 1) {\n const source = fallbackSources[0];\n const sourceAmount = source.userAmount || fromAmount;\n if (!sourceAmount) return null;\n return {\n amount:\n baseSourceDetailRows[0]?.tokenAmountValue ??\n formatHeaderTokenAmount(sourceAmount),\n chainName: getShortChainName(source.chainId, source.chainName),\n symbol: source.symbol,\n };\n }\n\n return null;\n })();\n const sourceHeaderAmount =\n isExactOutDisplayFlow && exactOutPaidUsdNumber !== undefined\n ? formatAmount(exactOutPaidUsdNumber)\n : singleSourceHeader?.amount ||\n (exactOutPaidUsdNumber !== undefined\n ? formatAmount(exactOutPaidUsdNumber)\n : pendingValue);\n const sourceHeaderUnit =\n isExactOutDisplayFlow && exactOutPaidUsdNumber !== undefined\n ? \"USD\"\n : singleSourceHeader?.symbol || \"USD\";\n const sourceHeaderSubtitle = (() => {\n if (isExactOutDisplayFlow && exactOutPaidUsdNumber !== undefined) {\n const count = sourceAssetCount || 1;\n return `${count} asset${count === 1 ? \"\" : \"s\"}`;\n }\n\n if (singleSourceHeader) {\n return singleSourceHeader.chainName\n ? `on ${singleSourceHeader.chainName}`\n : \"\";\n }\n\n const count = sourceAssetCount || 1;\n return `${count} asset${count === 1 ? \"\" : \"s\"}`;\n })();\n // The preview panel owns vertical scrolling; nested source scrollbars make\n // confirm screens show multiple scrollbars.\n const shouldScrollSourceDetails = false;\n const progressExplorerUrls = explorerUrls ?? {\n destinationExplorerUrl: null,\n sourceExplorerUrl: null,\n };\n const progressSources = sourceDetailRows.map((source) => ({\n chainLogo: source.chainLogo,\n symbol: source.symbol,\n tokenLogo: source.tokenLogo,\n }));\n const primarySourceForProgress = progressSources[0] ?? {\n chainLogo: fromToken?.chainLogo ?? \"\",\n symbol: sourceSymbols[0] ?? \"\",\n tokenLogo: fromToken?.logo ?? \"\",\n };\n const destinationProgressLogos = {\n chain: normalizedIntentDest?.chain.logo || toToken?.chainLogo || \"\",\n token: normalizedIntentDest?.token.logo || toToken?.logo || \"\",\n };\n\n const ctaLabel =\n flowMode === \"deposit\"\n ? \"Deposit now\"\n : flowMode === \"send\" || hasRecipientTransfer\n ? \"Send now\"\n : \"Swap now\";\n const shouldPulseCta =\n !isLoading && !isRefreshing && !isExecuting && !quoteUnavailable;\n const shouldShowMayanBadge = intentData?.bridgeProvider === \"mayan\";\n\n return (\n
\n \n \n \n
\n \n {sourceHeaderAmount}\n \n {sourceHeaderUnit}\n \n
\n \n {sourceHeaderSubtitle}\n
\n \n\n \n {[0, 1, 2, 3, 4].map((index) => (\n \n ))}\n \n\n \n \n {destinationHeaderAmount}\n \n {destTokenSymbol}\n \n \n \n {destChainName ? `on ${destChainName}` : destTokenSymbol}\n \n \n \n\n {singleSourceHeader ? (\n \n ) : (\n \n {\n startTransition();\n setShowSourceDetails((value) => !value);\n }}\n />\n \n )}\n\n {!singleSourceHeader && (\n \n {sourceDetailRows.length > 0 ? (\n \n \n {sourceDetailRows.map((source) => (\n \n \n \n \n {source.chainLogo && (\n \n )}\n \n \n \n {source.symbol}\n \n \n on {source.chainName || \"Unknown chain\"}\n \n \n \n \n \n {source.tokenAmount}\n \n {!isExactOutDisplayFlow && (\n \n {source.usdAmount}\n \n )}\n \n \n ))}\n \n {shouldScrollSourceDetails && (\n {\n sourceDetailsScrollRef.current?.scrollBy({\n behavior: \"smooth\",\n top: 54,\n });\n }}\n style={{\n alignItems: \"center\",\n background: \"#FFFFFE\",\n border: `1px solid ${border}`,\n borderRadius: \"999px\",\n boxShadow: \"0 2px 8px rgba(22,22,21,0.08)\",\n bottom: \"4px\",\n cursor: \"pointer\",\n display: \"flex\",\n height: \"20px\",\n justifyContent: \"center\",\n left: \"50%\",\n padding: 0,\n position: \"absolute\",\n transform: \"translateX(-50%)\",\n width: \"20px\",\n }}\n type=\"button\"\n >\n \n \n )}\n \n ) : (\n \n \n {pendingLabel}\n \n \n )}\n \n )}\n\n \n\n {hasRecipientTransfer && recipientAddress && (\n \n )}\n\n \n {\n startTransition();\n setShowFeeDetails((value) => !value);\n }}\n />\n \n\n \n {feeDetailRows.length > 0 ? (\n feeDetailRows.map((row) => (\n \n \n {row.label}\n \n \n {formatUsdValue(row.value)}\n \n \n ))\n ) : (\n \n \n Network & protocol\n \n \n {pendingValue}\n \n \n )}\n \n\n \n {\n startTransition();\n setShowImpactDetails((value) => !value);\n }}\n />\n \n\n \n \n \n Swap Impact\n \n \n {impactPercent}\n \n \n \n \n Max. Slippage\n \n \n Auto\n \n \n \n\n {shouldShowSwapBuffer && (\n \n Swap Buffer\n \n \n }\n value={swapBufferDisplay}\n />\n )}\n \n\n {isExecuting && steps && steps.length > 0 && (\n \n 1}\n isTransferMode={hasRecipientTransfer}\n sourceLogos={{\n chain: primarySourceForProgress.chainLogo,\n token: primarySourceForProgress.tokenLogo,\n }}\n sourceSymbol={primarySourceForProgress.symbol}\n sources={progressSources.length > 1 ? progressSources : undefined}\n steps={steps}\n />\n \n )}\n\n {shouldShowMayanBadge && }\n\n \n {isExecuting ? (\n isDepositMode ? (\n \"Depositing...\"\n ) : isSendMode || hasRecipientTransfer ? (\n \"Sending...\"\n ) : (\n \"Swapping...\"\n )\n ) : isLoading ? (\n \n ) : isRefreshing ? (\n \"Refreshing quotes...\"\n ) : quoteUnavailable ? (\n pendingLabel\n ) : (\n ctaLabel\n )}\n \n \n );\n}\n", + "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\n\"use client\";\n\nimport Decimal from \"decimal.js\";\nimport { ChevronDown, Info, Loader2 } from \"lucide-react\";\nimport React, { useRef, useState } from \"react\";\nimport type { SwapStepType } from \"../../common/types/transaction-flow\";\nimport { CHAIN_METADATA, getShortChainName } from \"../../common/utils/constant\";\nimport TransactionProgress from \"../../swaps/components/transaction-progress\";\nimport { Button } from \"../../ui/button\";\nimport {\n type NexusWidgetDepositOpportunityMetadata,\n type NexusWidgetMode,\n} from \"../types\";\nimport { NEXUS_WIDGET_FAST_SPINNER_STYLE } from \"../theme\";\nimport { type SwapTokenOption } from \"./swap-asset-selector\";\n\nexport interface SwapIntentSource {\n amount: string;\n chain: { id: number; logo: string; name: string };\n token: {\n contractAddress: string;\n decimals: number;\n logo?: string;\n symbol: string;\n };\n value?: string;\n}\n\nexport interface SwapIntentDestination {\n amount: string;\n chain: { id: number; logo: string; name: string };\n gas: {\n amount: string;\n value?: string;\n token: {\n contractAddress: string;\n decimals: number;\n logo?: string;\n symbol: string;\n };\n };\n token: {\n contractAddress: string;\n decimals: number;\n logo?: string;\n symbol: string;\n };\n value?: string;\n}\n\nexport type BridgeProvider = \"nexus\" | \"mayan\" | null;\n\nexport interface SwapIntentData {\n bridgeProvider?: BridgeProvider;\n destination: SwapIntentDestination;\n feesAndBuffer?: {\n buffer?: string;\n bridge?:\n | {\n caGas?: string;\n collection?: string;\n fulfilment?: string;\n gasSupplied?: string;\n protocol?: string;\n solver?: string;\n total?: string;\n }\n | string\n | null;\n };\n sources: SwapIntentSource[];\n}\n\nexport interface SwapIntentPreviewProps {\n activeMode?: NexusWidgetMode;\n estimatedTime?: string;\n explorerUrls?: {\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n };\n fromAmount: string;\n fromAmountUsd?: string;\n destinationGasFeeUsd?: string;\n fromToken?: SwapTokenOption;\n fromTokens?: SwapTokenOption[];\n intentData?: SwapIntentData | null;\n isExecuting?: boolean;\n isLoading?: boolean;\n isRefreshing?: boolean;\n mode?: NexusWidgetMode;\n onAccept: () => void;\n onReject: () => void;\n onTransitionChange?: (isTransitioning: boolean) => void;\n opportunity?: NexusWidgetDepositOpportunityMetadata;\n recipientAddress?: string;\n steps?: Array<{ id: number; completed: boolean; step: SwapStepType }>;\n supportedTokenAssets?: any[] | null;\n swapBalances?: any[] | null;\n swapType?: \"exactIn\" | \"exactOut\";\n toAmount?: string;\n toAmountTokens?: string;\n toAmountUsd?: string;\n toToken?: SwapTokenOption;\n totalFeeUsd?: string;\n}\n\nconst fontFamily = '\"Geist\", var(--font-geist-sans), system-ui, sans-serif';\nconst primary = \"var(--foreground-primary, #161615)\";\nconst muted = \"var(--foreground-muted, #848483)\";\nconst border = \"var(--border-default, #E8E8E7)\";\nconst brand = \"var(--foreground-brand)\";\n\nconst stripNumeric = (value: unknown) => String(value).replace(/[^0-9.-]/g, \"\");\n\nconst parseDecimal = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = stripNumeric(value);\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst toDecimal = (value: unknown) => parseDecimal(value) ?? new Decimal(0);\n\nconst formatAmount = (\n value: unknown,\n options: { min?: number; max?: number } = {},\n) => {\n const amount = toDecimal(value);\n const max = options.max ?? 2;\n return amount.toDecimalPlaces(max).toFixed();\n};\n\nconst formatUsdDelta = (value: Decimal) => {\n if (value.gt(0) && value.lt(0.01)) return \"-<0.01 USD\";\n return value.gt(0) ? `-${formatAmount(value)} USD` : \"0 USD\";\n};\n\nconst formatUsdAmount = (value: Decimal) => {\n if (value.gt(0) && value.lt(0.01)) return \"<0.01 USD\";\n return value.gt(0) ? `${formatAmount(value)} USD` : \"0 USD\";\n};\n\nconst formatUsdValue = (value: Decimal) => {\n const absolute = value.abs();\n if (absolute.eq(0)) return \"$0\";\n if (absolute.lt(0.01)) return value.lt(0) ? \"-<$0.01\" : \"<$0.01\";\n\n const amount = formatAmount(absolute, { max: 2 });\n\n return value.lt(0) ? `-$${amount}` : `$${amount}`;\n};\n\nconst formatTokenAmount = (value: unknown) => {\n const amount = toDecimal(value);\n return amount.toDecimalPlaces(8).toFixed();\n};\n\nconst formatHeaderTokenAmount = (value: unknown) => {\n const amount = toDecimal(value);\n if (amount.isZero()) return \"0\";\n if (amount.abs().gte(1000)) {\n return amount.toDecimalPlaces(2).toFixed();\n }\n if (amount.abs().gte(1)) {\n return amount.toDecimalPlaces(4).toFixed();\n }\n return amount.toDecimalPlaces(8).toFixed();\n};\n\nconst getFontSize = (amountStr: string, symbolStr: string) => {\n const totalLength =\n String(amountStr || \"\").length + String(symbolStr || \"\").length;\n if (totalLength > 16) return \"13px\";\n if (totalLength > 12) return \"15px\";\n return \"17px\";\n};\n\nconst unique = (values: string[]) =>\n Array.from(new Set(values.filter(Boolean)));\n\nconst isNativeTokenAddress = (address?: string) => {\n const lower = (address ?? \"\").toLowerCase();\n return (\n !lower ||\n lower === \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\" ||\n lower === \"0x0000000000000000000000000000000000000000\"\n );\n};\n\nconst getSourceDisplayKey = (\n chainId?: number | string,\n address?: string,\n symbol?: string,\n) => {\n const chainKey = chainId ?? \"\";\n const normalizedAddress = address?.toLowerCase();\n return normalizedAddress\n ? `${chainKey}-${normalizedAddress}`\n : `${chainKey}-symbol-${(symbol ?? \"\").toUpperCase()}`;\n};\n\nconst normalizeIntentToken = <\n T extends {\n contractAddress?: string;\n decimals?: number;\n logo?: string;\n symbol?: string;\n },\n>(\n token: T | undefined,\n chainId?: number,\n) => {\n const chainMeta = chainId ? CHAIN_METADATA[chainId] : undefined;\n const shouldUseNative =\n isNativeTokenAddress(token?.contractAddress) && Boolean(chainMeta);\n const symbol =\n shouldUseNative && chainMeta?.nativeCurrency.symbol\n ? chainMeta.nativeCurrency.symbol\n : token?.symbol || chainMeta?.nativeCurrency.symbol || \"-\";\n const decimals =\n shouldUseNative && chainMeta?.nativeCurrency.decimals !== undefined\n ? chainMeta.nativeCurrency.decimals\n : (token?.decimals ?? chainMeta?.nativeCurrency.decimals ?? 18);\n\n return {\n contractAddress: token?.contractAddress ?? \"\",\n decimals,\n logo: token?.logo || (shouldUseNative ? chainMeta?.logo : undefined),\n symbol,\n };\n};\n\nconst formatSymbolSummary = (symbols: string[]) => {\n const visible = unique(symbols);\n if (visible.length === 0) return \"-\";\n if (visible.length <= 2) return visible.join(\", \");\n if (visible.length === 3)\n return `${visible[0]}, ${visible[1]} and ${visible[2]}`;\n\n const others = visible.length - 2;\n return `${visible[0]}, ${visible[1]} and ${others} other${others === 1 ? \"\" : \"s\"}`;\n};\n\nconst sortSourceDetailRowsByUsdDesc = <\n T extends { symbol?: string; usdAmount?: string },\n>(\n rows: T[],\n) =>\n [...rows].sort((a, b) => {\n const usdDelta = toDecimal(b.usdAmount).cmp(toDecimal(a.usdAmount));\n if (usdDelta !== 0) return usdDelta;\n return (a.symbol ?? \"\").localeCompare(b.symbol ?? \"\");\n });\n\nfunction IntentLogo({\n src,\n alt,\n label,\n size,\n fontSize,\n outline,\n style,\n}: {\n src?: string;\n alt?: string;\n label?: string;\n size: number;\n fontSize: number;\n outline?: string;\n style?: React.CSSProperties;\n}) {\n const [failed, setFailed] = useState(!src);\n\n React.useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n const fallbackLabel = (label || alt || \"?\").trim().slice(0, 1).toUpperCase();\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n backgroundColor: \"#FFFFFE\",\n borderRadius: \"999px\",\n height: `${size}px`,\n objectFit: \"cover\",\n outline,\n width: `${size}px`,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {fallbackLabel || \"?\"}\n \n );\n}\n\nfunction DetailToggle({\n expanded,\n onClick,\n}: {\n expanded: boolean;\n onClick: () => void;\n}) {\n return (\n \n {expanded ? \"Hide Details\" : \"View Details\"}\n \n \n );\n}\n\nfunction TruncatedAddress({ address }: { address: string }) {\n const [showTooltip, setShowTooltip] = useState(false);\n const label =\n address.length > 12\n ? `${address.slice(0, 6)}...${address.slice(-4)}`\n : address;\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n style={{\n color: brand,\n display: \"inline-flex\",\n fontFamily,\n fontSize: \"13px\",\n fontWeight: 500,\n lineHeight: \"15px\",\n outline: \"none\",\n position: \"relative\",\n }}\n tabIndex={0}\n >\n {label}\n {showTooltip && (\n \n {address}\n \n )}\n \n );\n}\n\nfunction RecipientRow({ address }: { address: string }) {\n return (\n \n
\n \n Recipient\n
\n \n Wallet address\n \n \n
\n \n
\n \n );\n}\n\nfunction InlineInfoTooltip({ message }: { message: string }) {\n const [showTooltip, setShowTooltip] = useState(false);\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n style={{\n alignItems: \"center\",\n color: muted,\n display: \"inline-flex\",\n lineHeight: 0,\n outline: \"none\",\n position: \"relative\",\n }}\n tabIndex={0}\n >\n \n {showTooltip && (\n \n {message}\n \n )}\n \n );\n}\n\nfunction MayanPoweredBadge() {\n return (\n \n \n This transaction is powered by\n \n \n );\n}\n\nfunction Row({\n title,\n subtitle,\n value,\n secondaryValue,\n children,\n}: {\n title: React.ReactNode;\n subtitle: string;\n value: string;\n secondaryValue?: string;\n children?: React.ReactNode;\n}) {\n return (\n \n
\n \n {title}\n
\n \n {subtitle}\n \n \n \n \n {value}\n \n {secondaryValue ? (\n \n {secondaryValue}\n \n ) : (\n children\n )}\n \n \n );\n}\n\nfunction AnimatedDetails({\n open,\n children,\n background = \"#F9F9F8\",\n gap = \"9px\",\n padding = \"12px 14px\",\n}: {\n open: boolean;\n children: React.ReactNode;\n background?: string;\n gap?: string;\n padding?: string;\n}) {\n return (\n \n
\n \n {children}\n
\n \n \n );\n}\n\nexport function SwapIntentPreview({\n fromTokens,\n fromToken,\n toToken,\n fromAmount,\n fromAmountUsd,\n destinationGasFeeUsd,\n toAmount,\n toAmountUsd,\n toAmountTokens,\n totalFeeUsd,\n isLoading,\n isRefreshing,\n isExecuting,\n swapType,\n intentData,\n mode,\n opportunity,\n recipientAddress,\n activeMode,\n steps,\n explorerUrls,\n onAccept,\n onTransitionChange,\n}: SwapIntentPreviewProps) {\n const [showSourceDetails, setShowSourceDetails] = useState(false);\n const [showFeeDetails, setShowFeeDetails] = useState(false);\n const [showImpactDetails, setShowImpactDetails] = useState(false);\n const sourceDetailsScrollRef = useRef(null);\n const [transitionTimeoutId, setTransitionTimeoutId] = useState | null>(null);\n\n const startTransition = () => {\n if (onTransitionChange) {\n onTransitionChange(true);\n }\n if (transitionTimeoutId) {\n clearTimeout(transitionTimeoutId);\n }\n const id = setTimeout(() => {\n if (onTransitionChange) {\n onTransitionChange(false);\n }\n setTransitionTimeoutId(null);\n }, 280);\n setTransitionTimeoutId(id);\n };\n\n React.useEffect(() => {\n return () => {\n if (transitionTimeoutId) {\n clearTimeout(transitionTimeoutId);\n }\n };\n }, [transitionTimeoutId]);\n\n const flowMode = mode ?? activeMode ?? \"swap\";\n const isDepositMode = flowMode === \"deposit\";\n const isSendMode = flowMode === \"send\";\n const hasRecipientTransfer = Boolean(recipientAddress) && !isDepositMode;\n const isExactOutDisplayFlow =\n (isDepositMode || isSendMode) && swapType === \"exactOut\";\n const shouldShowSwapBuffer = swapType !== \"exactIn\";\n const intentSources = intentData?.sources ?? [];\n const intentDest = intentData?.destination;\n const normalizedIntentSources = intentSources.map((source) => ({\n ...source,\n token: normalizeIntentToken(source.token, source.chain.id),\n }));\n const normalizedIntentDest = intentDest\n ? {\n ...intentDest,\n token: normalizeIntentToken(intentDest.token, intentDest.chain.id),\n gas: {\n ...intentDest.gas,\n token: normalizeIntentToken(\n intentDest.gas?.token,\n intentDest.chain.id,\n ),\n },\n }\n : undefined;\n const fallbackSources =\n fromTokens && fromTokens.length > 0\n ? fromTokens\n : fromToken\n ? [fromToken]\n : [];\n\n const baseSourceSymbols =\n normalizedIntentSources.length > 0\n ? unique(normalizedIntentSources.map((source) => source.token.symbol))\n : unique(fallbackSources.map((source) => source.symbol));\n const baseSourceAssetCount =\n normalizedIntentSources.length ||\n fallbackSources.length ||\n baseSourceSymbols.length;\n const hasResolvedQuote = Boolean(\n normalizedIntentDest &&\n (normalizedIntentSources.length > 0 || isExactOutDisplayFlow),\n );\n const quoteUnavailable = !isLoading && !hasResolvedQuote;\n\n const destTokenSymbol =\n normalizedIntentDest?.token.symbol ||\n toToken?.symbol ||\n opportunity?.tokenSymbol ||\n \"-\";\n const destChainName =\n flowMode === \"deposit\"\n ? opportunity?.title || opportunity?.protocol || \"App\"\n : getShortChainName(\n normalizedIntentDest?.chain.id ?? toToken?.chainId,\n normalizedIntentDest?.chain.name || toToken?.chainName || \"\",\n );\n\n const requestedDestinationAmount = isExactOutDisplayFlow\n ? parseDecimal(toAmountTokens ?? toAmount)\n : undefined;\n const quotedDestinationAmount = parseDecimal(normalizedIntentDest?.amount);\n const destinationBalanceAmount = parseDecimal(toToken?.balance);\n const displayOnlyDestinationCoverage = (() => {\n if (\n !isExactOutDisplayFlow ||\n !requestedDestinationAmount ||\n requestedDestinationAmount.lte(0) ||\n !destinationBalanceAmount ||\n destinationBalanceAmount.lte(0)\n ) {\n return undefined;\n }\n\n const externallyProducedAmount =\n normalizedIntentSources.length > 0 &&\n quotedDestinationAmount &&\n quotedDestinationAmount.gt(0)\n ? quotedDestinationAmount\n : new Decimal(0);\n const destinationBalanceAmountNeeded = Decimal.max(\n requestedDestinationAmount.minus(externallyProducedAmount),\n 0,\n );\n const coveredAmount = Decimal.min(\n destinationBalanceAmountNeeded,\n destinationBalanceAmount,\n );\n return coveredAmount.gt(0) ? coveredAmount : undefined;\n })();\n const displayOnlyDestinationSourceAmount = displayOnlyDestinationCoverage;\n const requestedDestinationUsd = parseDecimal(toAmountUsd);\n const destinationDisplayUsdRate =\n requestedDestinationAmount &&\n requestedDestinationAmount.gt(0) &&\n requestedDestinationUsd &&\n requestedDestinationUsd.gt(0)\n ? requestedDestinationUsd.div(requestedDestinationAmount)\n : quotedDestinationAmount &&\n quotedDestinationAmount.gt(0) &&\n normalizedIntentDest?.value\n ? (parseDecimal(normalizedIntentDest.value) ?? new Decimal(0)).div(\n quotedDestinationAmount,\n )\n : undefined;\n const displayOnlyDestinationCoverageUsd =\n displayOnlyDestinationCoverage &&\n displayOnlyDestinationCoverage.gt(0) &&\n destinationDisplayUsdRate &&\n destinationDisplayUsdRate.gt(0)\n ? displayOnlyDestinationCoverage.mul(destinationDisplayUsdRate)\n : undefined;\n const displayOnlyDestinationSourceUsd =\n displayOnlyDestinationSourceAmount &&\n displayOnlyDestinationSourceAmount.gt(0) &&\n destinationDisplayUsdRate &&\n destinationDisplayUsdRate.gt(0)\n ? displayOnlyDestinationSourceAmount.mul(destinationDisplayUsdRate)\n : undefined;\n\n const intentSourceUsdValues = normalizedIntentSources.map((source) =>\n parseDecimal(source.value),\n );\n const intentSourceUsdNumber =\n normalizedIntentSources.length > 0\n ? intentSourceUsdValues.every((value) => value !== undefined)\n ? intentSourceUsdValues.reduce(\n (sum, value) => sum.plus(value ?? 0),\n new Decimal(0),\n )\n : parseDecimal(fromAmountUsd)\n : parseDecimal(fromAmountUsd);\n const effectiveSourceUsdNumber =\n displayOnlyDestinationCoverageUsd !== undefined\n ? (intentSourceUsdNumber ?? new Decimal(0)).plus(\n displayOnlyDestinationCoverageUsd,\n )\n : intentSourceUsdNumber;\n\n const destinationUsdNumber = hasResolvedQuote\n ? isExactOutDisplayFlow\n ? (parseDecimal(toAmountUsd) ?? parseDecimal(normalizedIntentDest?.value))\n : (parseDecimal(normalizedIntentDest?.value) ?? parseDecimal(toAmountUsd))\n : undefined;\n const hasFiatQuote =\n effectiveSourceUsdNumber !== undefined &&\n destinationUsdNumber !== undefined &&\n effectiveSourceUsdNumber.gt(0) &&\n destinationUsdNumber.gt(0);\n\n const bridgeFees = intentData?.feesAndBuffer?.bridge;\n const bridgeFeeData =\n bridgeFees && typeof bridgeFees === \"object\" ? bridgeFees : undefined;\n const bridgeTotalNumber =\n typeof bridgeFees === \"string\"\n ? parseDecimal(bridgeFees)\n : parseDecimal(bridgeFeeData?.total);\n const collectionFeeNumber = parseDecimal(bridgeFeeData?.collection);\n const fulfilmentFeeNumber = parseDecimal(bridgeFeeData?.fulfilment);\n const executionGasFeeNumber =\n parseDecimal(bridgeFeeData?.caGas) ??\n (collectionFeeNumber !== undefined || fulfilmentFeeNumber !== undefined\n ? (collectionFeeNumber ?? new Decimal(0)).plus(\n fulfilmentFeeNumber ?? new Decimal(0),\n )\n : undefined);\n const protocolFeeNumber = parseDecimal(bridgeFeeData?.protocol);\n const solverFeeNumber = parseDecimal(bridgeFeeData?.solver);\n const bridgeGasSuppliedNumber = parseDecimal(bridgeFeeData?.gasSupplied);\n const destinationGasValueNumber =\n parseDecimal(normalizedIntentDest?.gas?.value) ??\n parseDecimal(destinationGasFeeUsd);\n const destinationGasSuppliedNumber =\n bridgeGasSuppliedNumber ?? destinationGasValueNumber;\n const bridgeTotalWithDestinationGas =\n bridgeTotalNumber &&\n !bridgeGasSuppliedNumber &&\n destinationGasValueNumber &&\n destinationGasValueNumber.gt(0)\n ? bridgeTotalNumber.plus(destinationGasValueNumber)\n : bridgeTotalNumber;\n const swapBufferNumber = parseDecimal(intentData?.feesAndBuffer?.buffer);\n const bridgeComponentsTotalNumber = bridgeFeeData\n ? [\n executionGasFeeNumber,\n protocolFeeNumber,\n solverFeeNumber,\n destinationGasSuppliedNumber,\n ].reduce(\n (sum, value) => sum.plus(value ?? new Decimal(0)),\n new Decimal(0),\n )\n : undefined;\n const explicitFeeNumber =\n bridgeTotalWithDestinationGas ??\n (bridgeComponentsTotalNumber && bridgeComponentsTotalNumber.gt(0)\n ? bridgeComponentsTotalNumber\n : undefined) ??\n parseDecimal(totalFeeUsd) ??\n parseDecimal((intentData as any)?.fees?.total) ??\n destinationGasSuppliedNumber;\n const feeNumber =\n explicitFeeNumber ?? (hasFiatQuote ? new Decimal(0) : undefined);\n const quotedDestinationUsdNumber = parseDecimal(normalizedIntentDest?.value);\n const exactOutPaidUsdNumber = (() => {\n if (!isExactOutDisplayFlow) return effectiveSourceUsdNumber;\n\n const candidates = [\n effectiveSourceUsdNumber,\n requestedDestinationUsd,\n ].filter((value): value is Decimal => Boolean(value && value.gt(0)));\n\n if (requestedDestinationUsd && requestedDestinationUsd.gt(0)) {\n if (\n intentSourceUsdNumber &&\n intentSourceUsdNumber.gt(0) &&\n quotedDestinationUsdNumber &&\n quotedDestinationUsdNumber.gt(0)\n ) {\n candidates.push(\n requestedDestinationUsd.plus(\n Decimal.max(\n intentSourceUsdNumber.minus(quotedDestinationUsdNumber),\n 0,\n ),\n ),\n );\n }\n\n const knownOverhead = (feeNumber ?? new Decimal(0)).plus(\n swapBufferNumber ?? new Decimal(0),\n );\n if (knownOverhead.gt(0)) {\n candidates.push(requestedDestinationUsd.plus(knownOverhead));\n }\n }\n\n return candidates.reduce(\n (max, value) => (!max || value.gt(max) ? value : max),\n undefined,\n );\n })();\n const quoteImpactUsd =\n hasFiatQuote && feeNumber !== undefined\n ? Decimal.max(\n effectiveSourceUsdNumber\n .minus(destinationUsdNumber)\n .minus(feeNumber)\n .minus(swapBufferNumber ?? new Decimal(0)),\n 0,\n )\n : undefined;\n const priceImpactUsd =\n quoteImpactUsd ?? parseDecimal((intentData as any)?.priceImpactUsd);\n const computedSwapImpactPercent =\n hasFiatQuote && priceImpactUsd !== undefined\n ? priceImpactUsd.eq(0)\n ? new Decimal(0)\n : effectiveSourceUsdNumber !== undefined &&\n effectiveSourceUsdNumber.gt(0)\n ? priceImpactUsd.neg().div(effectiveSourceUsdNumber).mul(100)\n : undefined\n : undefined;\n const swapImpactPercent =\n computedSwapImpactPercent ??\n parseDecimal((intentData as any)?.swapImpactPercent) ??\n parseDecimal((intentData as any)?.priceImpactPercent);\n\n const destinationTokenAmount =\n isExactOutDisplayFlow && (toAmountTokens || toAmount)\n ? toAmountTokens || toAmount || \"0\"\n : normalizedIntentDest?.amount || toAmountTokens || toAmount || \"0\";\n const feeDetailRows = bridgeFeeData\n ? [\n {\n label: \"Execution Gas Fee\",\n value: executionGasFeeNumber ?? new Decimal(0),\n },\n {\n label: \"Protocol Fee\",\n value: protocolFeeNumber ?? new Decimal(0),\n },\n {\n label: \"Solver Fee\",\n value: solverFeeNumber ?? new Decimal(0),\n },\n ...(destinationGasSuppliedNumber && destinationGasSuppliedNumber.gt(0)\n ? [{ label: \"Gas Sponsorship\", value: destinationGasSuppliedNumber }]\n : []),\n ]\n : destinationGasSuppliedNumber && destinationGasSuppliedNumber.gt(0)\n ? [\n ...(feeNumber && feeNumber.gt(destinationGasSuppliedNumber)\n ? [\n {\n label: \"Network & protocol\",\n value: feeNumber.minus(destinationGasSuppliedNumber),\n },\n ]\n : []),\n { label: \"Gas Sponsorship\", value: destinationGasSuppliedNumber },\n ]\n : feeNumber !== undefined\n ? [{ label: \"Network & protocol\", value: feeNumber }]\n : [];\n\n const pendingLabel = isLoading ? \"Fetching quote\" : \"Quote unavailable\";\n const pendingValue = isLoading ? \"...\" : \"--\";\n const sourceUsd =\n exactOutPaidUsdNumber !== undefined\n ? `${formatAmount(exactOutPaidUsdNumber)} USD`\n : pendingValue;\n const receiveUsd = hasFiatQuote\n ? `${formatAmount(destinationUsdNumber)} USD`\n : pendingValue;\n const feeUsd =\n feeNumber !== undefined ? formatUsdAmount(feeNumber) : pendingValue;\n const impactUsd =\n priceImpactUsd !== undefined\n ? formatUsdDelta(priceImpactUsd)\n : pendingValue;\n const impactPercent =\n swapImpactPercent !== undefined\n ? `${formatAmount(swapImpactPercent, {\n min: 2,\n max: 2,\n })}%`\n : pendingValue;\n const destinationHeaderAmount = hasResolvedQuote\n ? formatHeaderTokenAmount(destinationTokenAmount)\n : pendingValue;\n const destinationTokenDisplay = hasResolvedQuote\n ? `${formatTokenAmount(destinationTokenAmount)} ${destTokenSymbol}`\n : pendingLabel;\n const destinationSourceAddress =\n normalizedIntentDest?.token.contractAddress || toToken?.contractAddress;\n const destinationSourceKey = getSourceDisplayKey(\n normalizedIntentDest?.chain.id ?? toToken?.chainId,\n destinationSourceAddress,\n destTokenSymbol,\n );\n const hasDestinationSourceRow = Boolean(\n destinationSourceKey &&\n (normalizedIntentSources.length > 0\n ? normalizedIntentSources.some((source) => {\n const sourceKey = getSourceDisplayKey(\n source.chain.id,\n source.token.contractAddress,\n source.token.symbol,\n );\n return sourceKey === destinationSourceKey;\n })\n : fallbackSources.some((source) => {\n const sourceKey = getSourceDisplayKey(\n source.chainId ?? \"\",\n source.contractAddress,\n source.symbol,\n );\n return sourceKey === destinationSourceKey;\n })),\n );\n const swapBufferDisplay =\n swapBufferNumber !== undefined\n ? formatUsdValue(swapBufferNumber)\n : pendingValue;\n const baseSourceDetailRows =\n normalizedIntentSources.length > 0\n ? normalizedIntentSources.map((source, index) => {\n const fallbackSource = fallbackSources.find(\n (token) =>\n token.chainId === source.chain.id &&\n (token.contractAddress?.toLowerCase() ===\n source.token.contractAddress?.toLowerCase() ||\n token.symbol === source.token.symbol),\n );\n\n const sourceKey = getSourceDisplayKey(\n source.chain.id,\n source.token.contractAddress,\n source.token.symbol,\n );\n const isDestinationSource =\n sourceKey === destinationSourceKey &&\n displayOnlyDestinationSourceAmount !== undefined;\n const sourceAmountNumber = parseDecimal(source.amount);\n const displaySourceAmount =\n isDestinationSource && displayOnlyDestinationSourceAmount\n ? (sourceAmountNumber ?? new Decimal(0)).plus(\n displayOnlyDestinationSourceAmount,\n )\n : sourceAmountNumber;\n const sourceValueNumber = parseDecimal(source.value);\n const displaySourceUsd =\n isDestinationSource && displayOnlyDestinationSourceUsd\n ? (sourceValueNumber ?? new Decimal(0)).plus(\n displayOnlyDestinationSourceUsd,\n )\n : sourceValueNumber;\n const tokenAmountValue =\n displaySourceAmount !== undefined\n ? formatTokenAmount(displaySourceAmount)\n : formatTokenAmount(source.amount);\n\n return {\n key: `${source.chain.id}-${source.token.contractAddress}-${index}`,\n tokenLogo: source.token.logo || fallbackSource?.logo || \"\",\n chainLogo: source.chain.logo || fallbackSource?.chainLogo || \"\",\n symbol: source.token.symbol,\n chainName: getShortChainName(source.chain.id, source.chain.name),\n tokenAmount: `${tokenAmountValue} ${source.token.symbol}`,\n tokenAmountValue,\n usdAmount:\n displaySourceUsd !== undefined\n ? formatUsdValue(displaySourceUsd)\n : pendingValue,\n index,\n };\n })\n : fallbackSources.map((source, index) => {\n const sourceAmount =\n source.userAmount ||\n (fallbackSources.length === 1 ? fromAmount : \"\");\n const sourceKey = getSourceDisplayKey(\n source.chainId ?? \"\",\n source.contractAddress,\n source.symbol,\n );\n const isDestinationSource =\n sourceKey === destinationSourceKey &&\n displayOnlyDestinationSourceAmount !== undefined;\n const sourceAmountNumber = parseDecimal(sourceAmount);\n const displaySourceAmount =\n isDestinationSource && displayOnlyDestinationSourceAmount\n ? (sourceAmountNumber ?? new Decimal(0)).plus(\n displayOnlyDestinationSourceAmount,\n )\n : sourceAmountNumber;\n const sourceUsdNumber =\n source.balanceInFiat && source.balance\n ? toDecimal(source.userAmount || 0).mul(\n toDecimal(source.balanceInFiat).div(\n Decimal.max(toDecimal(source.balance), 1),\n ),\n )\n : undefined;\n const displaySourceUsd =\n isDestinationSource && displayOnlyDestinationSourceUsd\n ? (sourceUsdNumber ?? new Decimal(0)).plus(\n displayOnlyDestinationSourceUsd,\n )\n : sourceUsdNumber;\n const tokenAmountValue =\n displaySourceAmount !== undefined\n ? formatTokenAmount(displaySourceAmount)\n : \"\";\n\n return {\n key: `${source.chainId ?? \"chain\"}-${source.contractAddress}-${index}`,\n tokenLogo: source.logo || \"\",\n chainLogo: source.chainLogo || \"\",\n symbol: source.symbol,\n chainName: getShortChainName(source.chainId, source.chainName),\n tokenAmount: tokenAmountValue\n ? `${tokenAmountValue} ${source.symbol}`\n : pendingLabel,\n tokenAmountValue,\n usdAmount:\n displaySourceUsd !== undefined\n ? formatUsdValue(displaySourceUsd)\n : pendingValue,\n index,\n };\n });\n const displayOnlyDestinationSourceRow =\n displayOnlyDestinationSourceAmount &&\n displayOnlyDestinationSourceAmount.gt(0) &&\n !hasDestinationSourceRow\n ? {\n key: `destination-existing-${normalizedIntentDest?.chain.id ?? toToken?.chainId ?? \"chain\"}-${normalizedIntentDest?.token.contractAddress ?? toToken?.contractAddress ?? \"token\"}`,\n tokenLogo: normalizedIntentDest?.token.logo || toToken?.logo || \"\",\n chainLogo:\n normalizedIntentDest?.chain.logo || toToken?.chainLogo || \"\",\n symbol: destTokenSymbol,\n chainName: getShortChainName(\n normalizedIntentDest?.chain.id ?? toToken?.chainId,\n normalizedIntentDest?.chain.name || toToken?.chainName || \"\",\n ),\n tokenAmount: `${formatTokenAmount(displayOnlyDestinationSourceAmount)} ${destTokenSymbol}`,\n tokenAmountValue: formatTokenAmount(\n displayOnlyDestinationSourceAmount,\n ),\n usdAmount:\n displayOnlyDestinationSourceUsd !== undefined\n ? formatUsdValue(displayOnlyDestinationSourceUsd)\n : pendingValue,\n index: baseSourceDetailRows.length,\n }\n : undefined;\n const sourceDetailRows = sortSourceDetailRowsByUsdDesc(\n displayOnlyDestinationSourceRow\n ? [...baseSourceDetailRows, displayOnlyDestinationSourceRow]\n : baseSourceDetailRows,\n );\n const sourceSymbols = (() => {\n const symbols =\n sourceDetailRows.length > 0\n ? sourceDetailRows.map((source) => source.symbol)\n : baseSourceSymbols;\n return destTokenSymbol && symbols.includes(destTokenSymbol)\n ? unique([\n destTokenSymbol,\n ...symbols.filter((symbol) => symbol !== destTokenSymbol),\n ])\n : unique(symbols);\n })();\n const sourceLabel = formatSymbolSummary(sourceSymbols);\n const sourceAssetCount =\n sourceDetailRows.length || baseSourceAssetCount || sourceSymbols.length;\n const singleSourceHeader = (() => {\n if (displayOnlyDestinationSourceRow) return null;\n if (\n !displayOnlyDestinationSourceRow &&\n normalizedIntentSources.length === 1\n ) {\n const source = normalizedIntentSources[0];\n const sourceRow = baseSourceDetailRows[0];\n return {\n amount:\n sourceRow?.tokenAmountValue ?? formatHeaderTokenAmount(source.amount),\n chainName: getShortChainName(source.chain.id, source.chain.name),\n symbol: source.token.symbol,\n };\n }\n\n if (normalizedIntentSources.length === 0 && fallbackSources.length === 1) {\n const source = fallbackSources[0];\n const sourceAmount = source.userAmount || fromAmount;\n if (!sourceAmount) return null;\n return {\n amount:\n baseSourceDetailRows[0]?.tokenAmountValue ??\n formatHeaderTokenAmount(sourceAmount),\n chainName: getShortChainName(source.chainId, source.chainName),\n symbol: source.symbol,\n };\n }\n\n return null;\n })();\n const sourceHeaderAmount =\n isExactOutDisplayFlow && exactOutPaidUsdNumber !== undefined\n ? formatAmount(exactOutPaidUsdNumber)\n : singleSourceHeader?.amount ||\n (exactOutPaidUsdNumber !== undefined\n ? formatAmount(exactOutPaidUsdNumber)\n : pendingValue);\n const sourceHeaderUnit =\n isExactOutDisplayFlow && exactOutPaidUsdNumber !== undefined\n ? \"USD\"\n : singleSourceHeader?.symbol || \"USD\";\n const sourceHeaderSubtitle = (() => {\n if (isExactOutDisplayFlow && exactOutPaidUsdNumber !== undefined) {\n const count = sourceAssetCount || 1;\n return `${count} asset${count === 1 ? \"\" : \"s\"}`;\n }\n\n if (singleSourceHeader) {\n return singleSourceHeader.chainName\n ? `on ${singleSourceHeader.chainName}`\n : \"\";\n }\n\n const count = sourceAssetCount || 1;\n return `${count} asset${count === 1 ? \"\" : \"s\"}`;\n })();\n // The preview panel owns vertical scrolling; nested source scrollbars make\n // confirm screens show multiple scrollbars.\n const shouldScrollSourceDetails = false;\n const progressExplorerUrls = explorerUrls ?? {\n destinationExplorerUrl: null,\n sourceExplorerUrl: null,\n };\n const progressSources = sourceDetailRows.map((source) => ({\n chainLogo: source.chainLogo,\n symbol: source.symbol,\n tokenLogo: source.tokenLogo,\n }));\n const primarySourceForProgress = progressSources[0] ?? {\n chainLogo: fromToken?.chainLogo ?? \"\",\n symbol: sourceSymbols[0] ?? \"\",\n tokenLogo: fromToken?.logo ?? \"\",\n };\n const destinationProgressLogos = {\n chain: normalizedIntentDest?.chain.logo || toToken?.chainLogo || \"\",\n token: normalizedIntentDest?.token.logo || toToken?.logo || \"\",\n };\n\n const ctaLabel =\n flowMode === \"deposit\"\n ? \"Deposit now\"\n : flowMode === \"send\" || hasRecipientTransfer\n ? \"Send now\"\n : \"Swap now\";\n const shouldPulseCta =\n !isLoading && !isRefreshing && !isExecuting && !quoteUnavailable;\n const shouldShowMayanBadge = intentData?.bridgeProvider === \"mayan\";\n\n return (\n
\n \n \n \n
\n \n {sourceHeaderAmount}\n \n {sourceHeaderUnit}\n \n
\n \n {sourceHeaderSubtitle}\n
\n \n\n \n {[0, 1, 2, 3, 4].map((index) => (\n \n ))}\n \n\n \n \n {destinationHeaderAmount}\n \n {destTokenSymbol}\n \n \n \n {destChainName ? `on ${destChainName}` : destTokenSymbol}\n \n \n \n\n {singleSourceHeader ? (\n \n ) : (\n \n {\n startTransition();\n setShowSourceDetails((value) => !value);\n }}\n />\n \n )}\n\n {!singleSourceHeader && (\n \n {sourceDetailRows.length > 0 ? (\n \n \n {sourceDetailRows.map((source) => (\n \n \n \n \n {source.chainLogo && (\n \n )}\n \n \n \n {source.symbol}\n \n \n on {source.chainName || \"Unknown chain\"}\n \n \n \n \n \n {source.tokenAmount}\n \n {!isExactOutDisplayFlow && (\n \n {source.usdAmount}\n \n )}\n \n \n ))}\n \n {shouldScrollSourceDetails && (\n {\n sourceDetailsScrollRef.current?.scrollBy({\n behavior: \"smooth\",\n top: 54,\n });\n }}\n style={{\n alignItems: \"center\",\n background: \"#FFFFFE\",\n border: `1px solid ${border}`,\n borderRadius: \"999px\",\n boxShadow: \"0 2px 8px rgba(22,22,21,0.08)\",\n bottom: \"4px\",\n cursor: \"pointer\",\n display: \"flex\",\n height: \"20px\",\n justifyContent: \"center\",\n left: \"50%\",\n padding: 0,\n position: \"absolute\",\n transform: \"translateX(-50%)\",\n width: \"20px\",\n }}\n type=\"button\"\n >\n \n \n )}\n \n ) : (\n \n \n {pendingLabel}\n \n \n )}\n \n )}\n\n \n\n {hasRecipientTransfer && recipientAddress && (\n \n )}\n\n \n {\n startTransition();\n setShowFeeDetails((value) => !value);\n }}\n />\n \n\n \n {feeDetailRows.length > 0 ? (\n feeDetailRows.map((row) => (\n \n \n {row.label}\n \n \n {formatUsdValue(row.value)}\n \n \n ))\n ) : (\n \n \n Network & protocol\n \n \n {pendingValue}\n \n \n )}\n \n\n \n {\n startTransition();\n setShowImpactDetails((value) => !value);\n }}\n />\n \n\n \n \n \n Swap Impact\n \n \n {impactPercent}\n \n \n \n \n Max. Slippage\n \n \n Auto\n \n \n \n\n {shouldShowSwapBuffer && (\n \n Swap Buffer\n \n \n }\n value={swapBufferDisplay}\n />\n )}\n \n\n {isExecuting && steps && steps.length > 0 && (\n \n 1}\n isTransferMode={hasRecipientTransfer}\n sourceLogos={{\n chain: primarySourceForProgress.chainLogo,\n token: primarySourceForProgress.tokenLogo,\n }}\n sourceSymbol={primarySourceForProgress.symbol}\n sources={progressSources.length > 1 ? progressSources : undefined}\n steps={steps}\n />\n \n )}\n\n {shouldShowMayanBadge && }\n\n \n {isExecuting ? (\n isDepositMode ? (\n \"Depositing...\"\n ) : isSendMode || hasRecipientTransfer ? (\n \"Sending...\"\n ) : (\n \"Swapping...\"\n )\n ) : isLoading ? (\n \n ) : isRefreshing ? (\n \"Refreshing quotes...\"\n ) : quoteUnavailable ? (\n pendingLabel\n ) : (\n ctaLabel\n )}\n \n \n );\n}\n", "type": "registry:component", "target": "components/nexus/components/swap-intent-preview.tsx" }, { "path": "registry/avail-widgets/nexus-widget/nexus-widget.tsx", - "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\"use client\";\n\nimport {\n ERROR_CODES,\n type EthereumProvider,\n} from \"@avail-project/nexus-core\";\nimport Decimal from \"decimal.js\";\nimport { AlertCircle, ArrowLeft, ChevronDown, Loader2 } from \"lucide-react\";\nimport React, {\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { flushSync } from \"react-dom\";\nimport {\n createPublicClient,\n encodeFunctionData,\n erc20Abi,\n http,\n isAddress,\n parseUnits,\n zeroAddress,\n} from \"viem\";\nimport { mainnet } from \"viem/chains\";\nimport { normalize } from \"viem/ens\";\nimport {\n useAccount,\n useConnect,\n useConnectorClient,\n usePublicClient,\n useWalletClient,\n} from \"wagmi\";\nimport { ErrorBoundary } from \"../common/components/ErrorBoundary\";\nimport { useTransactionSteps } from \"../common/tx/useTransactionSteps\";\nimport type {\n BridgeStepType,\n SwapStepType,\n} from \"../common/types/transaction-flow\";\nimport {\n CHAIN_METADATA,\n getShortChainName,\n isSwapSupportedBySdkChainList,\n SUPPORTED_CHAINS,\n TOKEN_CONTRACT_ADDRESSES,\n TOKEN_METADATA,\n} from \"../common/utils/constant\";\nimport { type UserAsset, useNexus } from \"../nexus/NexusProvider\";\nimport { Button } from \"../ui/button\";\nimport { Dialog, DialogContent, DialogTrigger } from \"../ui/dialog\";\nimport { DepositIdleForm } from \"./components/deposit-idle-form\";\nimport {\n type NexusWidgetProgressEvent,\n NexusWidgetProgressScreen,\n} from \"./components/nexus-widget-progress-screen\";\nimport {\n getCachedReceiveTokenMatch,\n preloadReceiveTokens,\n ReceiveAssetSelector,\n} from \"./components/receive-asset-selector\";\nimport { RecipientInput } from \"./components/recipient-input\";\nimport { SendIdleForm } from \"./components/send-idle-form\";\nimport { StatusAlert } from \"./components/status-alerts\";\nimport {\n deriveTokenOptions,\n SwapAssetSelector,\n type SwapTokenOption,\n} from \"./components/swap-asset-selector\";\nimport { SwapIdleForm } from \"./components/swap-idle-form\";\nimport {\n type BridgeProvider,\n type SwapIntentData,\n type SwapIntentDestination,\n type SwapIntentSource,\n SwapIntentPreview,\n} from \"./components/swap-intent-preview\";\nimport {\n NEXUS_WIDGET_DEFAULT_PRIMARY_COLOR,\n nexusWidgetTheme,\n} from \"./theme\";\nimport {\n type NexusWidgetAppearance,\n type NexusWidgetConfig,\n type NexusWidgetDepositConfig,\n type NexusWidgetDestination,\n type NexusWidgetDestinationToken,\n type NexusWidgetDepositOpportunityConfig,\n type NexusWidgetDepositOpportunityMetadata,\n type NexusWidgetMode,\n type NexusWidgetPrefillToken,\n type NexusWidgetProps,\n type NexusWidgetRuntimePrefill,\n type SwapType,\n} from \"./types\";\nimport { findCitreaReceiveToken } from \"./utils/citrea-tokens\";\nimport {\n type DepositSourceFilter,\n getDepositSourceId,\n resolveDepositSourceSelection,\n} from \"./utils/deposit-source-selection\";\n\n// ---------------------------------------------------------------------------\n// Types for swap step machine\n// ---------------------------------------------------------------------------\n\ntype SwapStep =\n | \"idle\" // main screen\n | \"choose-swap-asset\" // pick source token\n | \"choose-receive-asset\" // pick receive token\n | \"enter-recipient\" // pick recipient (send mode)\n | \"preview-intent\" // intent preview card\n | \"progress\" // transaction in flight\n | \"success\" // completed seamlessly\n | \"failed\" // failed swap receipt\n | \"history\"; // transaction history\n\ntype SourceFilterTab = \"all\" | \"native\" | \"stables\";\n\ntype SwapHistoryStatus =\n | \"pending\"\n | \"fulfilled\"\n | \"failed\"\n | \"timeout\"\n | \"refund-initiated\";\n\ninterface SwapHistoryEntry {\n autoRefundAvailable?: boolean;\n createdAt: number;\n durationSeconds?: number;\n endedAt?: number;\n error?: string;\n failedStepType?: string;\n failureDescription?: string;\n failureMessage?: string;\n feeUsd?: string;\n finalExplorerUrl?: string | null;\n fromTokens: SwapTokenOption[];\n id: string;\n intentData: SwapIntentData | null;\n intentExplorerUrl?: string | null;\n intentId?: number;\n mode: NexusWidgetMode;\n opportunity?: NexusWidgetDepositOpportunityMetadata;\n recipientAddress?: string;\n requestedToAmount?: string;\n requestedToValue?: string;\n sourceExplorerUrl?: string | null;\n startedAt: number;\n status: SwapHistoryStatus;\n toToken?: SwapTokenOption;\n}\n\ntype HistorySourceRow = {\n amount: string;\n chainId?: number;\n chainLogo?: string;\n chainName: string;\n contractAddress?: string;\n key: string;\n symbol: string;\n tokenLogo?: string;\n value?: unknown;\n};\n\ntype SwapQuoteIssue = {\n type: \"insufficientSources\";\n message: string;\n missingUsd?: string;\n};\n\ntype ReceiveAmountIssue = {\n ctaLabel: string;\n message: string;\n type: \"configuredAmountLimit\" | \"receiveLimitExceeded\" | \"unpricedReceiveToken\";\n};\n\ntype CachedIntentUsdRate = {\n amount: string;\n rate: string;\n updatedAt: number;\n value: string;\n};\n\ntype PredictiveQuote = {\n key: string;\n mode: \"exactIn\" | \"exactOut\";\n sources?: SwapTokenOption[];\n toAmount?: string;\n toUsd?: string;\n};\n\ntype IntentFetchTiming = {\n background: boolean;\n endTimeMs?: number;\n fetchDurationMs?: number;\n operation: string;\n quoteInputKey: string;\n runId: number;\n sourceCount?: number;\n startTimeMs: number;\n walletAddress?: string | null;\n};\n\ntype PredictiveQuoteBaseline = {\n destinationUsdRate: string;\n exactInDestinationAmountPerSourceUsd?: string;\n exactOutSourceUsdPerDestinationUsd?: string;\n updatedAt: number;\n};\n\nconst DESTINATION_RECEIVE_LIMIT_USD_BY_CHAIN_ID: Record = {\n [SUPPORTED_CHAINS.MEGAETH]: 10_000,\n [SUPPORTED_CHAINS.CITREA]: 2000,\n [SUPPORTED_CHAINS.SCROLL]: 500,\n};\n\nconst SCIENTIFIC_DECIMAL_REGEX = /^-?(?:\\d+\\.?\\d*|\\.\\d+)e[+-]?\\d+$/i;\n\nconst QUOTE_REFRESH_INTERVAL_MS = 30000;\nconst EXACT_OUT_INPUT_DEBOUNCE_MS = 1300;\nconst DRAWER_CLOSE_MS = 220;\nconst BALANCE_REFRESH_AFTER_TERMINAL_MS = 3000;\nconst MODAL_HEIGHT_TRANSITION_MS = 220;\nconst ROOT_HEIGHT_TRANSITION_MS = 140;\nconst ASSET_SELECTOR_DRAWER_HEIGHT = \"90%\";\nconst TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT = 620;\nconst CONFIGURED_RECEIVE_SELECTOR_BASE_HEIGHT = 170;\nconst CONFIGURED_RECEIVE_SELECTOR_ROW_HEIGHT = 62;\nconst BASIS_POINTS = 10000;\nconst PREDICTIVE_EXACT_IN_DISCOUNT_BPS = 50;\nconst PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_PCT = 0.1;\nconst PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_MAX_USD = 2;\nconst PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_PCT = 0.02;\nconst PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_MAX_USD = 1;\nconst PREDICTIVE_QUOTE_DISPLAY_DECIMALS = 8;\nconst DEPOSIT_TOKEN_DISPLAY_DECIMALS = 8;\nconst ETHEREUM_MAINNET_CHAIN_ID = 1;\nconst SDK_EXACT_OUT_STABLE_SYMBOLS = new Set([\n \"USDC\",\n \"USDT\",\n \"DAI\",\n \"BUSD\",\n \"TUSD\",\n \"FRAX\",\n \"LUSD\",\n \"USDD\",\n \"USDP\",\n \"GUSD\",\n]);\nconst SWAP_HISTORY_STORAGE_KEY_PREFIX = \"nexus-widget-transaction-history-v1\";\nconst TIMEOUT_LABEL = \"Timed Out\";\nconst PROGRESS_EVENT_NAMES = {\n BRIDGE_PLAN_LIST: \"bridge_plan_list\",\n BRIDGE_PLAN_PROGRESS: \"bridge_plan_progress\",\n SWAP_PLAN_LIST: \"swap_plan_list\",\n SWAP_PLAN_PROGRESS: \"swap_plan_progress\",\n} as const;\nconst PLAN_FINAL_STATES = new Set([\"completed\", \"confirmed\", \"submitted\"]);\nconst PLAN_STEP_FUNDS_MAY_HAVE_MOVED_STATES = new Set([\n \"completed\",\n \"confirmed\",\n \"submitted\",\n \"failed\",\n]);\nconst theme = nexusWidgetTheme;\nconst tooltipSurface = theme.colors.surface;\nconst tooltipText = theme.colors.textStrong;\nconst tooltipBorder = theme.colors.border;\nconst uiFont = theme.fonts.sans;\nconst modalHeightTransitionStyle = {\n interpolateSize: \"allow-keywords\",\n} as React.CSSProperties;\n\nconst getCappedTokenDisplayDecimals = (decimals?: number) => {\n const parsedDecimals = Number(decimals);\n if (!Number.isFinite(parsedDecimals) || parsedDecimals < 0) {\n return DEPOSIT_TOKEN_DISPLAY_DECIMALS;\n }\n return Math.min(Math.floor(parsedDecimals), DEPOSIT_TOKEN_DISPLAY_DECIMALS);\n};\nconst modalHeightTransition = `height ${MODAL_HEIGHT_TRANSITION_MS}ms ease, max-height ${MODAL_HEIGHT_TRANSITION_MS}ms ease`;\n\nconst normalizeSdkExactOutStableSymbol = (symbol?: string) =>\n (symbol ?? \"\")\n .trim()\n .toUpperCase()\n .replaceAll(\"\\u20ae\", \"T\")\n .replaceAll(/[^A-Z0-9]/g, \"\");\n\nconst isSdkExactOutStableSymbol = (symbol?: string) =>\n SDK_EXACT_OUT_STABLE_SYMBOLS.has(normalizeSdkExactOutStableSymbol(symbol));\n\nconst applyPredictiveExactOutBufferUsd = (\n amount: Decimal,\n pct: number,\n maxUsd: number\n) => Decimal.min(amount.mul(pct), new Decimal(maxUsd));\n\nconst getPredictiveExactOutSourceTargetUsd = (\n destinationUsdNeedingSources: Decimal,\n cachedSourceUsdRatio?: Decimal\n) => {\n if (destinationUsdNeedingSources.lte(0)) return new Decimal(0);\n if (cachedSourceUsdRatio?.gt(0)) {\n return destinationUsdNeedingSources.mul(cachedSourceUsdRatio);\n }\n\n const destinationBuffer = applyPredictiveExactOutBufferUsd(\n destinationUsdNeedingSources,\n PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_PCT,\n PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_MAX_USD\n );\n const destinationBuffered = destinationUsdNeedingSources.plus(\n destinationBuffer\n );\n const sourceBuffer = applyPredictiveExactOutBufferUsd(\n destinationBuffered,\n PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_PCT,\n PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_MAX_USD\n );\n\n return destinationBuffered.plus(sourceBuffer);\n};\n\nconst getSwapHistoryStorageKey = (ownerAddress?: string) =>\n `${SWAP_HISTORY_STORAGE_KEY_PREFIX}:${ownerAddress?.toLowerCase() || \"anonymous\"}`;\n\nconst getTokenSelectionKey = (token?: SwapTokenOption | null) => {\n if (!token) return \"\";\n if (token.isUnified) {\n return `unified:${token.unifiedSymbol ?? token.symbol}`;\n }\n return `${token.chainId ?? \"unknown\"}:${token.contractAddress.toLowerCase()}`;\n};\n\nconst getDisplaySourceTokenKey = (token?: SwapTokenOption | null) => {\n if (!token) return \"\";\n const chainId = token.chainId ?? \"unknown\";\n const address = token.contractAddress?.toLowerCase();\n return address\n ? `${chainId}:${address}`\n : `${chainId}:symbol:${token.symbol.toUpperCase()}`;\n};\n\nconst sumDecimalStrings = (left?: string, right?: string) => {\n const leftAmount = parseDecimalLoose(left) ?? new Decimal(0);\n const rightAmount = parseDecimalLoose(right) ?? new Decimal(0);\n const sum = leftAmount.plus(rightAmount);\n return sum.gt(0) ? sum.toFixed() : undefined;\n};\n\nconst mergeDisplaySourceTokens = (tokens: SwapTokenOption[]) => {\n const merged = new Map();\n\n for (const token of tokens) {\n const key = getDisplaySourceTokenKey(token);\n if (!key) continue;\n const existing = merged.get(key);\n if (!existing) {\n merged.set(key, token);\n continue;\n }\n\n const userAmount = sumDecimalStrings(\n existing.userAmount || existing.balance,\n token.userAmount || token.balance\n );\n const userAmountUsd = sumDecimalStrings(\n existing.userAmountUsd || existing.balanceInFiat,\n token.userAmountUsd || token.balanceInFiat\n );\n\n merged.set(key, {\n ...existing,\n balance: userAmount ?? existing.balance,\n balanceInFiat: userAmountUsd ?? existing.balanceInFiat,\n chainLogo: existing.chainLogo || token.chainLogo,\n logo: existing.logo || token.logo,\n userAmount: userAmount ?? existing.userAmount,\n userAmountUsd: userAmountUsd ?? existing.userAmountUsd,\n });\n }\n\n return Array.from(merged.values());\n};\n\nconst getTokenQuoteKey = (token?: SwapTokenOption | null) => {\n if (!token) return \"\";\n return [\n getTokenSelectionKey(token),\n token.symbol ?? \"\",\n token.decimals ?? \"\",\n ].join(\":\");\n};\n\nconst getSourceTokensQuoteKey = (tokens: SwapTokenOption[]) =>\n tokens\n .filter((token) => {\n const amt = token.userAmount ?? \"\";\n const cleaned = amt.replaceAll(/[^0-9.]/g, \"\");\n const num = Number.parseFloat(cleaned);\n return !Number.isNaN(num) && num > 0;\n })\n .map((token) =>\n [\n getTokenSelectionKey(token),\n token.symbol ?? \"\",\n token.decimals ?? \"\",\n token.userAmount ?? \"\",\n token.userAmountUsd ?? \"\",\n token.userAmountMode ?? \"\",\n ].join(\":\")\n )\n .join(\"|\");\n\nconst getSourceTokenSyncKey = (token: SwapTokenOption) =>\n [\n getTokenSelectionKey(token),\n token.userAmount ?? \"\",\n token.userAmountUsd ?? \"\",\n token.balance ?? \"\",\n token.balanceInFiat ?? \"\",\n ].join(\":\");\n\nconst getSourceTokenListSyncKey = (tokens: SwapTokenOption[]) =>\n tokens.map(getSourceTokenSyncKey).join(\"|\");\n\nconst isSameTokenSelection = (\n a?: SwapTokenOption | null,\n b?: SwapTokenOption | null\n) => Boolean(a && b && getTokenSelectionKey(a) === getTokenSelectionKey(b));\n\nconst getDepositConfigIdentity = (deposit?: NexusWidgetDepositOpportunityMetadata | null) => {\n if (!deposit) return \"\";\n return [\n deposit.chainId,\n deposit.tokenAddress.toLowerCase(),\n deposit.tokenSymbol,\n deposit.tokenDecimals,\n deposit.protocol ?? \"\",\n deposit.title ?? \"\",\n ].join(\":\");\n};\n\nconst isSameDepositConfig = (\n a?: NexusWidgetDepositOpportunityConfig | null,\n b?: NexusWidgetDepositOpportunityConfig | null\n) => {\n if (!a || !b) return false;\n return getDepositConfigIdentity(a) === getDepositConfigIdentity(b);\n};\n\nconst getConfiguredReceiveSelectorRootHeight = (tokenCount: number) => {\n if (tokenCount <= 0) return TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT;\n const visibleRows = Math.min(Math.max(tokenCount, 1), 6);\n return Math.min(\n TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT,\n Math.ceil(\n (CONFIGURED_RECEIVE_SELECTOR_BASE_HEIGHT +\n visibleRows * CONFIGURED_RECEIVE_SELECTOR_ROW_HEIGHT) /\n 0.9\n )\n );\n};\n\nconst getConfiguredDeposit = (\n config: RuntimeNexusWidgetConfig\n): NexusWidgetDepositOpportunityConfig | undefined => config.deposit ?? config.deposits?.[0];\n\ntype RuntimeDestinationPair = {\n chain: number;\n decimals?: number;\n logo?: string;\n symbol?: string;\n token: `0x${string}`;\n};\n\ntype RuntimeNexusWidgetAppearance = NexusWidgetAppearance & {\n logoUrl?: string;\n themeMode?: \"auto\" | \"light\" | \"dark\";\n widgetHeading?: string;\n};\n\ntype RuntimeNexusWidgetConfig = {\n allowedDestinationChains?: number[];\n allowedDestinationPairs?: RuntimeDestinationPair[];\n allowedSourcePairs?: RuntimeDestinationPair[];\n appearance?: RuntimeNexusWidgetAppearance;\n deposit?: NexusWidgetDepositOpportunityConfig;\n deposits?: NexusWidgetDepositOpportunityConfig[];\n mode: NexusWidgetMode;\n prefill?: NexusWidgetRuntimePrefill;\n};\n\ntype RuntimeNexusWidgetAmountInput = {\n max?: string;\n min?: string;\n mode: \"fixed\" | \"user\";\n value?: string;\n};\n\ntype NormalizedNexusWidgetConfig = {\n activeMode: NexusWidgetMode;\n amountInput?: RuntimeNexusWidgetAmountInput;\n appearance?: RuntimeNexusWidgetAppearance;\n config: RuntimeNexusWidgetConfig;\n depositOptions: NexusWidgetDepositOpportunityConfig[];\n isAmountFixed: boolean;\n isRecipientLocked: boolean;\n};\n\nconst normalizeConfiguredString = (value: unknown) => {\n if (value === null || value === undefined) return undefined;\n const text = String(value).trim();\n return text || undefined;\n};\n\nconst normalizeConfiguredAddress = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n return text && isAddress(text) ? text : undefined;\n};\n\nconst isPositiveGasLimit = (value: unknown): value is bigint =>\n typeof value === \"bigint\" && value > BigInt(0);\n\nconst normalizePositiveNumericString = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n if (!text) return undefined;\n try {\n const decimal = new Decimal(text);\n return decimal.isFinite() && decimal.gt(0) ? text : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst normalizeNonNegativeNumericString = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n if (!text) return undefined;\n try {\n const decimal = new Decimal(text);\n return decimal.isFinite() && decimal.gte(0) ? text : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst normalizeHttpUrlString = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n if (!text) return undefined;\n return /^https?:\\/\\//i.test(text) ? text : undefined;\n};\n\nconst normalizeNexusWidgetPrimaryColor = (value?: string) => {\n return normalizeConfiguredString(value);\n};\n\nconst expandHexColor = (value: string) => {\n const text = value.trim();\n if (/^#[0-9a-fA-F]{6}$/.test(text)) return text;\n if (/^#[0-9a-fA-F]{3}$/.test(text)) {\n const [r, g, b] = text.slice(1);\n return `#${r}${r}${g}${g}${b}${b}`;\n }\n return undefined;\n};\n\nconst parseNumberComponent = (value: string, max: number) => {\n const text = value.trim();\n if (!text) return undefined;\n if (text.endsWith(\"%\")) {\n const percent = Number.parseFloat(text.slice(0, -1));\n return Number.isFinite(percent)\n ? Math.max(0, Math.min(max, (percent / 100) * max))\n : undefined;\n }\n const parsed = Number.parseFloat(text);\n return Number.isFinite(parsed) ? Math.max(0, Math.min(max, parsed)) : undefined;\n};\n\nconst parseAngleComponent = (value: string) => {\n const parsed = Number.parseFloat(value.trim());\n if (!Number.isFinite(parsed)) return undefined;\n return ((parsed % 360) + 360) % 360;\n};\n\nconst hslToRgb = (h: number, s: number, l: number): [number, number, number] => {\n const saturation = Math.max(0, Math.min(100, s)) / 100;\n const lightness = Math.max(0, Math.min(100, l)) / 100;\n const c = (1 - Math.abs(2 * lightness - 1)) * saturation;\n const hp = h / 60;\n const x = c * (1 - Math.abs((hp % 2) - 1));\n let r1 = 0;\n let g1 = 0;\n let b1 = 0;\n if (hp >= 0 && hp < 1) {\n r1 = c;\n g1 = x;\n } else if (hp >= 1 && hp < 2) {\n r1 = x;\n g1 = c;\n } else if (hp >= 2 && hp < 3) {\n g1 = c;\n b1 = x;\n } else if (hp >= 3 && hp < 4) {\n g1 = x;\n b1 = c;\n } else if (hp >= 4 && hp < 5) {\n r1 = x;\n b1 = c;\n } else {\n r1 = c;\n b1 = x;\n }\n const m = lightness - c / 2;\n return [\n Math.round((r1 + m) * 255),\n Math.round((g1 + m) * 255),\n Math.round((b1 + m) * 255),\n ];\n};\n\nconst parseCssColorToRgb = (\n color: string,\n allowBrowserResolution = true\n): [number, number, number] | undefined => {\n const text = normalizeConfiguredString(color);\n if (!text) return undefined;\n\n const hex = expandHexColor(text);\n if (hex) {\n return [\n Number.parseInt(hex.slice(1, 3), 16),\n Number.parseInt(hex.slice(3, 5), 16),\n Number.parseInt(hex.slice(5, 7), 16),\n ];\n }\n\n const rgbMatch = text.match(/^rgba?\\((.*)\\)$/i);\n if (rgbMatch) {\n const parts = rgbMatch[1].trim().split(/[\\s,\\/]+/).filter(Boolean);\n if (parts.length >= 3) {\n const r = parseNumberComponent(parts[0], 255);\n const g = parseNumberComponent(parts[1], 255);\n const b = parseNumberComponent(parts[2], 255);\n if (r !== undefined && g !== undefined && b !== undefined) {\n return [Math.round(r), Math.round(g), Math.round(b)];\n }\n }\n }\n\n const hslMatch = text.match(/^hsla?\\((.*)\\)$/i);\n if (hslMatch) {\n const parts = hslMatch[1].trim().split(/[\\s,\\/]+/).filter(Boolean);\n if (parts.length >= 3) {\n const h = parseAngleComponent(parts[0]);\n const s = parseNumberComponent(parts[1], 100);\n const l = parseNumberComponent(parts[2], 100);\n if (h !== undefined && s !== undefined && l !== undefined) {\n return hslToRgb(h, s, l);\n }\n }\n }\n\n if (allowBrowserResolution && typeof document !== \"undefined\") {\n const canvas = document.createElement(\"canvas\");\n const context = canvas.getContext(\"2d\");\n if (context) {\n const baseline = \"#010203\";\n context.fillStyle = baseline;\n context.fillStyle = text;\n const resolved = String(context.fillStyle);\n if (resolved !== baseline || text.toLowerCase() === baseline) {\n return parseCssColorToRgb(resolved, false);\n }\n }\n }\n\n return undefined;\n};\n\nconst getReadableTextColor = (background: string) => {\n const rgb = parseCssColorToRgb(background);\n if (!rgb) return \"#FFFFFE\";\n const [rValue, gValue, bValue] = rgb;\n const r = rValue / 255;\n const g = gValue / 255;\n const b = bValue / 255;\n const linear = [r, g, b].map((channel) =>\n channel <= 0.03928\n ? channel / 12.92\n : Math.pow((channel + 0.055) / 1.055, 2.4)\n );\n const luminance = 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];\n return luminance > 0.54 ? \"#161615\" : \"#FFFFFE\";\n};\n\nconst normalizeNexusWidgetAmountInput = (\n config: NexusWidgetConfig\n): RuntimeNexusWidgetAmountInput | undefined => {\n if (config.mode === \"swap\") return undefined;\n const value = normalizePositiveNumericString(\n \"amount\" in (config.prefill ?? {}) ? config.prefill?.amount : undefined\n );\n const min = normalizeNonNegativeNumericString(config.validation?.minAmount);\n const max = normalizePositiveNumericString(config.validation?.maxAmount);\n return value || min || max ? { mode: \"user\", value, min, max } : undefined;\n};\n\nconst normalizeNexusWidgetAppearance = (\n appearance?: NexusWidgetAppearance\n): RuntimeNexusWidgetAppearance | undefined => {\n if (!appearance) return undefined;\n const appName = normalizeConfiguredString(appearance.appName);\n const appLogoURL = normalizeHttpUrlString(appearance.appLogoURL);\n const heading = normalizeConfiguredString(appearance.heading);\n const mode =\n appearance.mode === \"dark\" || appearance.mode === \"light\"\n ? appearance.mode\n : \"system\";\n const primaryColor = normalizeConfiguredString(appearance.primaryColor);\n return {\n ...appearance,\n appLogoURL,\n appName,\n heading,\n logoUrl: appLogoURL,\n mode,\n primaryColor,\n themeMode: mode === \"system\" ? \"auto\" : mode,\n widgetHeading: heading,\n };\n};\n\nconst normalizeConfiguredChainId = (value: unknown) => {\n const chainId = Number(value);\n return Number.isFinite(chainId) && chainId > 0 ? chainId : undefined;\n};\n\nconst getConfiguredDestinationChainId = (\n destination?: NexusWidgetDestination\n) => normalizeConfiguredChainId(destination?.chain);\n\nconst getConfiguredDestinationTokenSymbol = (\n token: NexusWidgetDestinationToken\n) => normalizeConfiguredString(token.symbol);\n\nconst getConfiguredDestinationTokenAddress = (\n token: NexusWidgetDestinationToken\n) => normalizeConfiguredAddress(token.address);\n\nconst toSwapTokenOptionFromConfiguredDestinationToken = (\n token: NexusWidgetDestinationToken,\n chainId: number\n): SwapTokenOption | undefined => {\n const contractAddress = getConfiguredDestinationTokenAddress(token);\n const symbol = getConfiguredDestinationTokenSymbol(token);\n if (!contractAddress || !symbol) return undefined;\n const chainMeta = CHAIN_METADATA[chainId];\n return {\n balance: \"0\",\n balanceInFiat: \"$0.00\",\n chainId,\n chainLogo: chainMeta?.logo,\n chainName: getShortChainName(chainId, chainMeta?.name ?? `Chain ${chainId}`),\n contractAddress,\n decimals: token.decimals ?? 18,\n logo: token.logo ?? \"\",\n name: symbol,\n symbol,\n };\n};\n\nconst toSwapTokenOptionFromDepositConfig = (\n deposit: NexusWidgetDepositOpportunityConfig\n): SwapTokenOption => {\n const chainMeta = CHAIN_METADATA[deposit.chainId];\n return {\n balance: \"0\",\n balanceInFiat: \"$0.00\",\n chainId: deposit.chainId,\n chainLogo: chainMeta?.logo,\n chainName: getShortChainName(\n deposit.chainId,\n chainMeta?.name ?? `Chain ${deposit.chainId}`\n ),\n contractAddress: deposit.tokenAddress,\n decimals: deposit.tokenDecimals,\n logo: deposit.tokenLogo || deposit.logo || \"\",\n name: deposit.tokenSymbol,\n symbol: deposit.tokenSymbol,\n };\n};\n\nconst applyNexusWidgetAppearanceToDeposit = (\n deposit: NexusWidgetDepositOpportunityConfig,\n appearance?: RuntimeNexusWidgetAppearance\n): NexusWidgetDepositOpportunityConfig => {\n const appName = normalizeConfiguredString(appearance?.appName);\n const logoUrl = normalizeConfiguredString(appearance?.logoUrl);\n if (!appName && !logoUrl) return deposit;\n return {\n ...deposit,\n depositTargetLogo: logoUrl ?? deposit.depositTargetLogo,\n logo: logoUrl ?? deposit.logo,\n protocol: appName ?? deposit.protocol,\n title: appName ?? deposit.title,\n };\n};\n\nconst getConfiguredDestinationPairs = (\n destination?: NexusWidgetDestination\n) => {\n const pairs: RuntimeNexusWidgetConfig[\"allowedDestinationPairs\"] = [];\n const chain = getConfiguredDestinationChainId(destination);\n if (!chain) return pairs;\n for (const token of destination?.tokens ?? []) {\n const address = getConfiguredDestinationTokenAddress(token);\n if (!address) continue;\n pairs.push({ chain, token: address });\n }\n return pairs;\n};\n\nconst hasConfiguredDestinationTokenList = (\n destination?: NexusWidgetDestination\n) => Boolean(destination?.tokens?.length);\n\nconst getConfiguredPrefillTokenPair = (\n prefill?: { token?: NexusWidgetPrefillToken }\n): RuntimeDestinationPair | undefined => {\n const token = prefill?.token;\n if (!token) return undefined;\n const chain = normalizeConfiguredChainId(token.chain);\n const address = getConfiguredDestinationTokenAddress(token);\n const symbol = getConfiguredDestinationTokenSymbol(token);\n if (!chain || !address || !symbol) return undefined;\n return {\n chain,\n decimals: token.decimals,\n logo: normalizeConfiguredString(token.logo),\n symbol,\n token: address,\n };\n};\n\nconst getConfiguredDestinationChainIds = (\n destination?: NexusWidgetDestination\n) => {\n const chain = getConfiguredDestinationChainId(destination);\n return chain ? [chain] : [];\n};\n\nconst toDepositConfigFromDestination = (\n config: NexusWidgetDepositConfig,\n token: NexusWidgetDestinationToken,\n appearance?: RuntimeNexusWidgetAppearance\n): NexusWidgetDepositOpportunityConfig | undefined => {\n const chainId = getConfiguredDestinationChainId(config.destination);\n const depositAddress = normalizeConfiguredAddress(config.depositAddress);\n const tokenAddress = getConfiguredDestinationTokenAddress(token);\n const tokenSymbol = getConfiguredDestinationTokenSymbol(token);\n if (\n !chainId ||\n !depositAddress ||\n !tokenAddress ||\n !tokenSymbol ||\n typeof config.executeDeposit !== \"function\"\n ) {\n return undefined;\n }\n\n const chainMeta = CHAIN_METADATA[chainId];\n return {\n chainId,\n depositTargetLogo: appearance?.logoUrl,\n executeDeposit: config.executeDeposit,\n logo: appearance?.logoUrl ?? token.logo ?? undefined,\n protocol:\n appearance?.appName ??\n appearance?.widgetHeading ??\n \"Deposit\",\n subtitle:\n chainMeta?.name\n ? `on ${getShortChainName(chainId, chainMeta.name)}`\n : undefined,\n title: appearance?.appName ?? appearance?.widgetHeading ?? \"Deposit\",\n tokenAddress,\n tokenDecimals: token.decimals ?? 18,\n tokenLogo: token.logo,\n tokenSymbol,\n };\n};\n\nconst getNexusWidgetDepositOptions = (\n config: NexusWidgetConfig,\n appearance?: RuntimeNexusWidgetAppearance\n) => {\n if (config.mode !== \"deposit\") return [];\n return (config.destination.tokens ?? [])\n .map((token) => toDepositConfigFromDestination(config, token, appearance))\n .filter((deposit): deposit is NexusWidgetDepositOpportunityConfig => Boolean(deposit));\n};\n\nconst normalizeNexusWidgetConfig = (\n rawConfig: NexusWidgetConfig\n): NormalizedNexusWidgetConfig => {\n const appearance = normalizeNexusWidgetAppearance(rawConfig.appearance);\n const amountInput = normalizeNexusWidgetAmountInput(rawConfig);\n const prefill: NexusWidgetRuntimePrefill = {};\n let activeMode: NexusWidgetMode =\n rawConfig.mode === \"deposit\" ||\n rawConfig.mode === \"send\" ||\n rawConfig.mode === \"swap\"\n ? rawConfig.mode\n : \"swap\";\n let isRecipientLocked = false;\n const depositOptions = getNexusWidgetDepositOptions(rawConfig, appearance).map(\n (deposit) => applyNexusWidgetAppearanceToDeposit(deposit, appearance)\n );\n let allowedDestinationPairs:\n | RuntimeNexusWidgetConfig[\"allowedDestinationPairs\"]\n | undefined;\n let allowedDestinationChains:\n | RuntimeNexusWidgetConfig[\"allowedDestinationChains\"]\n | undefined;\n\n if (amountInput?.value) {\n prefill.amount = amountInput.value;\n }\n\n if (rawConfig.mode === \"deposit\") {\n activeMode = \"deposit\";\n const pairs = getConfiguredDestinationPairs(rawConfig.destination);\n if (pairs.length > 0) {\n allowedDestinationPairs = pairs;\n prefill.destination = pairs[0];\n }\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (chainIds.length > 0) {\n allowedDestinationChains = chainIds;\n }\n } else if (rawConfig.mode === \"send\") {\n const recipient = normalizeConfiguredAddress(rawConfig.recipientAddress);\n if (recipient) {\n prefill.recipient = recipient;\n isRecipientLocked = true;\n }\n\n const hasDestinationTokens = hasConfiguredDestinationTokenList(\n rawConfig.destination\n );\n const pairs = getConfiguredDestinationPairs(rawConfig.destination);\n if (pairs.length > 0) {\n allowedDestinationPairs = pairs;\n prefill.destination ??= pairs[0];\n } else if (!hasDestinationTokens) {\n const prefillToken = getConfiguredPrefillTokenPair(rawConfig.prefill);\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (\n prefillToken &&\n (!chainIds.length || chainIds.includes(prefillToken.chain))\n ) {\n prefill.destination = prefillToken;\n }\n }\n\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (chainIds.length > 0) {\n allowedDestinationChains = chainIds;\n }\n } else if (rawConfig.mode === \"swap\") {\n const recipient = normalizeConfiguredAddress(rawConfig.recipientAddress);\n if (recipient) {\n prefill.recipient = recipient;\n }\n\n const hasDestinationTokens = hasConfiguredDestinationTokenList(\n rawConfig.destination\n );\n const pairs = getConfiguredDestinationPairs(rawConfig.destination);\n if (pairs.length > 0) {\n allowedDestinationPairs = pairs;\n prefill.destination ??= pairs[0];\n } else if (!hasDestinationTokens) {\n const prefillToken = getConfiguredPrefillTokenPair(rawConfig.prefill);\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (\n prefillToken &&\n (!chainIds.length || chainIds.includes(prefillToken.chain))\n ) {\n prefill.destination = prefillToken;\n }\n }\n\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (chainIds.length > 0) {\n allowedDestinationChains = chainIds;\n }\n }\n\n const runtimeConfig: RuntimeNexusWidgetConfig = {\n allowedDestinationChains,\n allowedDestinationPairs,\n deposit:\n activeMode === \"deposit\"\n ? depositOptions[0]\n : undefined,\n deposits: activeMode === \"deposit\" ? depositOptions : [],\n appearance,\n mode: activeMode,\n prefill,\n };\n\n return {\n activeMode,\n amountInput,\n appearance,\n config: runtimeConfig,\n depositOptions,\n isAmountFixed: false,\n isRecipientLocked,\n };\n};\n\nconst getDepositForTokenSelection = (\n deposits: NexusWidgetDepositOpportunityConfig[],\n token?: SwapTokenOption | null\n) => {\n if (!token) return undefined;\n return deposits.find(\n (deposit) =>\n deposit.chainId === token.chainId &&\n deposit.tokenAddress.toLowerCase() ===\n token.contractAddress.toLowerCase()\n );\n};\n\nconst sanitizeOpportunityForHistory = (\n opportunity?: NexusWidgetDepositOpportunityMetadata\n): NexusWidgetDepositOpportunityMetadata | undefined => {\n if (!opportunity) return undefined;\n return {\n label: opportunity.label,\n protocol: opportunity.protocol,\n logo: opportunity.logo,\n title: opportunity.title,\n subtitle: opportunity.subtitle,\n chainId: opportunity.chainId,\n tokenSymbol: opportunity.tokenSymbol,\n tokenDecimals: opportunity.tokenDecimals,\n tokenLogo: opportunity.tokenLogo,\n tokenAddress: opportunity.tokenAddress,\n apy: opportunity.apy,\n description: opportunity.description,\n };\n};\n\nconst sanitizeHistoryEntry = (entry: SwapHistoryEntry): SwapHistoryEntry => ({\n ...entry,\n createdAt: entry.createdAt ?? entry.startedAt ?? Date.now(),\n failureMessage:\n entry.status === \"timeout\" ? TIMEOUT_LABEL : entry.failureMessage,\n opportunity: sanitizeOpportunityForHistory(entry.opportunity),\n});\n\nconst sortSwapHistoryEntries = (entries: SwapHistoryEntry[]) =>\n [...entries].sort(\n (a, b) =>\n (b.createdAt ?? b.startedAt ?? 0) - (a.createdAt ?? a.startedAt ?? 0)\n );\n\nconst isStoredHistoryStatus = (value: unknown): value is SwapHistoryStatus =>\n value === \"pending\" ||\n value === \"fulfilled\" ||\n value === \"failed\" ||\n value === \"timeout\" ||\n value === \"refund-initiated\";\n\nconst isStoredMode = (value: unknown): value is NexusWidgetMode =>\n value === \"swap\" || value === \"deposit\" || value === \"send\";\n\nconst normalizeStoredHistoryEntry = (\n value: unknown\n): SwapHistoryEntry | null => {\n if (!value || typeof value !== \"object\") return null;\n const entry = value as Partial;\n const startedAt =\n typeof entry.startedAt === \"number\" && Number.isFinite(entry.startedAt)\n ? entry.startedAt\n : undefined;\n const createdAt =\n typeof entry.createdAt === \"number\" && Number.isFinite(entry.createdAt)\n ? entry.createdAt\n : startedAt;\n\n if (\n !entry.id ||\n typeof entry.id !== \"string\" ||\n !isStoredMode(entry.mode) ||\n !isStoredHistoryStatus(entry.status) ||\n !createdAt ||\n !startedAt\n ) {\n return null;\n }\n\n return {\n ...entry,\n id: entry.id,\n mode: entry.mode,\n status: entry.status,\n createdAt,\n startedAt,\n failureMessage:\n entry.status === \"timeout\" ? TIMEOUT_LABEL : entry.failureMessage,\n intentData: entry.intentData ?? null,\n fromTokens: Array.isArray(entry.fromTokens) ? entry.fromTokens : [],\n opportunity: sanitizeOpportunityForHistory(entry.opportunity),\n } as SwapHistoryEntry;\n};\n\nconst readSwapHistoryFromStorage = (storageKey: string): SwapHistoryEntry[] => {\n if (typeof window === \"undefined\") return [];\n\n try {\n const raw = window.localStorage.getItem(storageKey);\n if (!raw) return [];\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n return sortSwapHistoryEntries(\n parsed\n .map(normalizeStoredHistoryEntry)\n .filter((entry): entry is SwapHistoryEntry => Boolean(entry))\n );\n } catch {\n return [];\n }\n};\n\nconst writeSwapHistoryToStorage = (\n storageKey: string,\n entries: SwapHistoryEntry[]\n) => {\n if (typeof window === \"undefined\") return;\n\n try {\n const persistableEntries =\n sortSwapHistoryEntries(entries).map(sanitizeHistoryEntry);\n window.localStorage.setItem(\n storageKey,\n JSON.stringify(persistableEntries, (_key, value) =>\n typeof value === \"bigint\" ? value.toString() : value\n )\n );\n } catch {\n // localStorage can be unavailable or full; in-memory history still works.\n }\n};\n\nfunction QuoteRefreshCountdown({\n progress,\n isRefreshing,\n secondsRemaining,\n}: {\n progress: number;\n isRefreshing: boolean;\n secondsRemaining: number;\n}) {\n const [showTooltip, setShowTooltip] = useState(false);\n const radius = 7;\n const circumference = 2 * Math.PI * radius;\n const clampedProgress = Math.max(0, Math.min(1, progress));\n const tooltipLabel = isRefreshing\n ? \"Refreshing quotes...\"\n : `Refreshing quotes in ${Math.max(0, secondsRemaining)} second${\n secondsRemaining === 1 ? \"\" : \"s\"\n }`;\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n style={{\n alignItems: \"center\",\n backgroundColor: \"#FFFFFE\",\n borderRadius: \"999px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n height: \"22px\",\n justifyContent: \"center\",\n outline: \"1px solid #E8E8E7\",\n position: \"relative\",\n width: \"22px\",\n }}\n tabIndex={0}\n >\n {showTooltip && (\n \n {tooltipLabel}\n \n )}\n \n \n \n \n \n );\n}\n\nconst normalizeDecimalInputText = (value: unknown) => {\n const raw = String(value).trim();\n if (!raw) return \"\";\n if (SCIENTIFIC_DECIMAL_REGEX.test(raw)) return raw;\n return raw.replace(/[^0-9.-]/g, \"\");\n};\n\nconst parseDecimalLoose = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = normalizeDecimalInputText(value);\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst toViemDecimalString = (value: unknown, decimals: number) => {\n const parsed = parseDecimalLoose(value);\n if (!parsed || parsed.lte(0)) return \"0\";\n return parsed\n .toDecimalPlaces(Math.max(0, decimals), Decimal.ROUND_DOWN)\n .toFixed();\n};\n\nconst formatDecimalDisplay = (\n value: unknown,\n options: { min?: number; max?: number } = {}\n) => {\n const amount = parseDecimalLoose(value) ?? new Decimal(0);\n const max = options.max ?? 2;\n return amount.toDecimalPlaces(max).toFixed();\n};\n\nconst formatUsdDisplay = (value: unknown) => {\n const amount = parseDecimalLoose(value) ?? new Decimal(0);\n if (amount.gt(0) && amount.lt(0.01)) return \"<$0.01\";\n return `$${formatDecimalDisplay(amount, { min: 2, max: 2 })}`;\n};\n\nconst formatTokenDisplay = (value: unknown) => {\n const amount = parseDecimalLoose(value) ?? new Decimal(0);\n return formatDecimalDisplay(amount, { max: 8 });\n};\n\nconst getSwapTokenUsdValue = (token: SwapTokenOption) =>\n parseDecimalLoose(token.userAmountUsd) ??\n parseDecimalLoose(token.balanceInFiat) ??\n new Decimal(0);\n\nconst getSwapTokenBalanceUsdValue = (token: SwapTokenOption) =>\n parseDecimalLoose(token.balanceInFiat) ?? new Decimal(0);\n\nconst sortSwapTokensByUsdDesc = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const usdDelta = getSwapTokenUsdValue(b).cmp(getSwapTokenUsdValue(a));\n if (usdDelta !== 0) return usdDelta;\n return (a.symbol ?? \"\").localeCompare(b.symbol ?? \"\");\n });\n\nconst sortDisplaySourcesByBalanceUsdDesc = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const balanceUsdDelta = getSwapTokenBalanceUsdValue(b).cmp(\n getSwapTokenBalanceUsdValue(a)\n );\n if (balanceUsdDelta !== 0) return balanceUsdDelta;\n\n const spendUsdDelta = getSwapTokenUsdValue(b).cmp(getSwapTokenUsdValue(a));\n if (spendUsdDelta !== 0) return spendUsdDelta;\n\n return `${a.symbol ?? \"\"} ${a.chainName ?? \"\"}`.localeCompare(\n `${b.symbol ?? \"\"} ${b.chainName ?? \"\"}`\n );\n });\n\nconst getIntentSourceUsdValue = (source: SwapIntentData[\"sources\"][number]) =>\n parseDecimalLoose(source.value) ?? new Decimal(0);\n\nconst sortIntentSourcesByUsdDesc = (sources: SwapIntentData[\"sources\"]) =>\n [...sources].sort((a, b) => {\n const usdDelta = getIntentSourceUsdValue(b).cmp(getIntentSourceUsdValue(a));\n if (usdDelta !== 0) return usdDelta;\n return (a.token?.symbol ?? \"\").localeCompare(b.token?.symbol ?? \"\");\n });\n\nconst extractIntentIdFromUrl = (url?: string | null) => {\n if (!url) return undefined;\n const match = url.match(/(\\d+)(?:\\/)?$/);\n if (!match) return undefined;\n const parsed = Number(match[1]);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;\n};\n\nconst getNonEmptyString = (...values: unknown[]) => {\n for (const value of values) {\n if (typeof value !== \"string\") continue;\n const trimmed = value.trim();\n if (trimmed.length > 0) return trimmed;\n }\n return null;\n};\n\nconst isHttpUrl = (value?: string | null): value is string =>\n Boolean(value && /^https?:\\/\\//i.test(value));\n\nconst hasValidIntentExplorer = (\n entry: Pick\n) => isHttpUrl(entry.intentExplorerUrl);\n\nconst getHistoryExplorerUrl = (\n entry: Pick<\n SwapHistoryEntry,\n \"finalExplorerUrl\" | \"intentExplorerUrl\" | \"sourceExplorerUrl\"\n >\n) =>\n [\n entry.intentExplorerUrl,\n entry.finalExplorerUrl,\n entry.sourceExplorerUrl,\n ].find(isHttpUrl) ?? null;\n\nconst getFiniteNumber = (...values: unknown[]) => {\n for (const value of values) {\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value !== \"string\") continue;\n const parsed = Number(value);\n if (Number.isFinite(parsed)) return parsed;\n }\n return undefined;\n};\n\nconst getObjectChainId = (value: any) =>\n getFiniteNumber(\n value?.chainId,\n value?.chain?.id,\n value?.chain?.chainId,\n value?.toChainId,\n value?.destinationChainId,\n value?.data?.chainId,\n value?.data?.chain?.id,\n value?.data?.chain?.chainId,\n value?.data?.toChainId,\n value?.data?.destinationChainId,\n value?.result?.chainId,\n value?.result?.chain?.id\n );\n\nconst getExplorerBaseUrl = (chainId?: number, ...candidates: unknown[]) => {\n const directCandidates = candidates.flatMap((candidate: any) => [\n candidate?.blockExplorerUrl,\n candidate?.blockExplorerURL,\n candidate?.chainBlockExplorerUrl,\n candidate?.explorerBaseUrl,\n candidate?.explorerUrlBase,\n candidate?.blockExplorerUrls?.[0],\n candidate?.blockExplorers?.default?.url,\n candidate?.chain?.blockExplorerUrl,\n candidate?.chain?.blockExplorerURL,\n candidate?.chain?.chainBlockExplorerUrl,\n candidate?.chain?.explorerBaseUrl,\n candidate?.chain?.explorerUrlBase,\n candidate?.chain?.blockExplorerUrls?.[0],\n candidate?.chain?.blockExplorers?.default?.url,\n candidate?.data?.blockExplorerUrl,\n candidate?.data?.blockExplorerURL,\n candidate?.data?.chainBlockExplorerUrl,\n candidate?.data?.blockExplorerUrls?.[0],\n candidate?.data?.blockExplorers?.default?.url,\n candidate?.data?.chain?.blockExplorerUrl,\n candidate?.data?.chain?.blockExplorerURL,\n candidate?.data?.chain?.chainBlockExplorerUrl,\n candidate?.data?.chain?.blockExplorerUrls?.[0],\n candidate?.data?.chain?.blockExplorers?.default?.url,\n ]);\n return getNonEmptyString(\n ...directCandidates,\n chainId ? CHAIN_METADATA[chainId]?.blockExplorerUrls?.[0] : undefined,\n chainId\n ? (CHAIN_METADATA[chainId] as any)?.blockExplorers?.default?.url\n : undefined\n );\n};\n\nconst getTransactionHash = (...values: unknown[]) => {\n for (const value of values) {\n if (typeof value !== \"string\") continue;\n const trimmed = value.trim();\n if (/^0x[a-fA-F0-9]{64}$/.test(trimmed)) return trimmed;\n }\n return null;\n};\n\nconst getIntentHash = (...values: unknown[]) => getTransactionHash(...values);\n\nconst getObjectIntentHash = (value: any) =>\n getIntentHash(\n value?.intentHash,\n value?.intent_hash,\n value?.intent?.hash,\n value?.intent?.intentHash,\n value?.intent?.intent_hash,\n value?.requestHash,\n value?.request_hash,\n value?.request?.hash,\n value?.request?.requestHash,\n value?.rffHash,\n value?.rff_hash,\n value?.rff?.hash,\n value?.data?.intentHash,\n value?.data?.intent_hash,\n value?.data?.intent?.hash,\n value?.data?.intent?.intentHash,\n value?.data?.intent?.intent_hash,\n value?.data?.requestHash,\n value?.data?.request_hash,\n value?.data?.request?.hash,\n value?.data?.request?.requestHash,\n value?.data?.rffHash,\n value?.data?.rff_hash,\n value?.data?.rff?.hash,\n value?.result?.intentHash,\n value?.result?.intent_hash,\n value?.result?.intent?.hash,\n value?.result?.requestHash,\n value?.result?.request_hash,\n value?.result?.rffHash,\n value?.result?.rff_hash\n );\n\nconst getNexusExplorerNetwork = (network?: unknown) => {\n const normalized =\n typeof network === \"string\" ? network.trim().toLowerCase() : \"\";\n if (normalized === \"canary\" || normalized === \"testnet\") return normalized;\n return \"mainnet\";\n};\n\nconst getRffExplorerUrl = (network: unknown, intentHash?: string | null) =>\n intentHash\n ? `https://nexus-v2.${getNexusExplorerNetwork(network)}.avail.so/rff/${intentHash}`\n : null;\n\nconst getObjectTransactionHash = (value: any) =>\n getTransactionHash(\n value?.txHash,\n value?.transactionHash,\n value?.executeTxHash,\n value?.executeTransactionHash,\n value?.transferTransactionHash,\n value?.receipt?.transactionHash,\n value?.tx?.hash,\n value?.transaction?.hash,\n value?.data?.txHash,\n value?.data?.transactionHash,\n value?.data?.executeTxHash,\n value?.data?.executeTransactionHash,\n value?.data?.transferTransactionHash,\n value?.data?.receipt?.transactionHash,\n value?.data?.tx?.hash,\n value?.data?.transaction?.hash,\n value?.result?.txHash,\n value?.result?.transactionHash,\n value?.result?.receipt?.transactionHash\n );\n\nconst getExplorerTxUrl = (\n chainId?: number,\n txHash?: string | null,\n ...candidates: unknown[]\n) => {\n if (!chainId || !txHash) return null;\n const baseUrl = getExplorerBaseUrl(chainId, ...candidates);\n return baseUrl ? `${String(baseUrl).replace(/\\/$/, \"\")}/tx/${txHash}` : null;\n};\n\nconst getSdkSwapResult = (result: any) => {\n const candidate = result?.swapResult ?? result?.result;\n return candidate && typeof candidate === \"object\" ? candidate : null;\n};\n\nconst getSdkTransactionHash = (result: any) =>\n getObjectTransactionHash(result) ||\n getObjectTransactionHash(result?.executeResponse) ||\n getObjectTransactionHash(result?.execute) ||\n getObjectTransactionHash(result?.transfer) ||\n getObjectTransactionHash(result?.swapResult) ||\n getObjectTransactionHash(result?.result) ||\n null;\n\nconst getSdkExplorerUrl = (result: any) =>\n getNonEmptyString(\n result?.explorerUrl,\n result?.explorerURL,\n result?.txExplorerUrl,\n result?.transactionExplorerUrl,\n result?.execute?.explorerUrl,\n result?.execute?.explorerURL,\n result?.execute?.txExplorerUrl,\n result?.execute?.transactionExplorerUrl,\n result?.executeResponse?.explorerUrl,\n result?.executeResponse?.explorerURL,\n result?.executeResponse?.txExplorerUrl,\n result?.executeResponse?.transactionExplorerUrl,\n result?.executeExplorerUrl,\n result?.transferExplorerUrl,\n result?.swapResult?.explorerUrl,\n result?.swapResult?.explorerURL,\n result?.swapResult?.txExplorerUrl,\n result?.swapResult?.transactionExplorerUrl,\n result?.result?.explorerUrl,\n result?.result?.explorerURL,\n result?.result?.txExplorerUrl,\n result?.result?.transactionExplorerUrl\n );\n\nconst getSdkIntentExplorerUrl = (result: any, swapResult?: any) =>\n getNonEmptyString(\n swapResult?.intentExplorerUrl,\n swapResult?.intentExplorerURL,\n swapResult?.intentUrl,\n swapResult?.intentURL,\n swapResult?.rffUrl,\n swapResult?.rffURL,\n swapResult?.rffExplorerUrl,\n swapResult?.rffExplorerURL,\n swapResult?.explorerUrl,\n swapResult?.explorerURL,\n result?.intentExplorerUrl,\n result?.intentExplorerURL,\n result?.intentUrl,\n result?.intentURL,\n result?.rffUrl,\n result?.rffURL,\n result?.rffExplorerUrl,\n result?.rffExplorerURL,\n result?.swapResult?.intentExplorerUrl,\n result?.swapResult?.intentExplorerURL,\n result?.swapResult?.intentUrl,\n result?.swapResult?.intentURL,\n result?.swapResult?.rffUrl,\n result?.swapResult?.rffURL,\n result?.swapResult?.rffExplorerUrl,\n result?.swapResult?.rffExplorerURL,\n result?.swapResult?.explorerUrl,\n result?.swapResult?.explorerURL,\n result?.result?.intentExplorerUrl,\n result?.result?.intentExplorerURL,\n result?.result?.intentUrl,\n result?.result?.intentURL,\n result?.result?.rffUrl,\n result?.result?.rffURL,\n result?.result?.rffExplorerUrl,\n result?.result?.rffExplorerURL,\n result?.result?.explorerUrl,\n result?.result?.explorerURL\n );\n\nconst getSdkIntentExplorerUrlForNetwork = (\n network: unknown,\n result: any,\n swapResult?: any\n) =>\n getSdkIntentExplorerUrl(result, swapResult) ||\n getRffExplorerUrl(\n network,\n getObjectIntentHash(swapResult) || getObjectIntentHash(result)\n );\n\nfunction MiniLogo({\n src,\n label,\n size = 30,\n fontSize = 13,\n outline,\n style,\n}: {\n src?: string;\n label?: string;\n size?: number;\n fontSize?: number;\n outline?: string;\n style?: React.CSSProperties;\n}) {\n const [failed, setFailed] = useState(!src);\n\n useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n background: \"#FFFFFE\",\n borderRadius: \"999px\",\n height: size,\n objectFit: \"cover\",\n outline,\n width: size,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {(label || \"?\").trim().slice(0, 1).toUpperCase()}\n \n );\n}\n\nfunction TokenLogoPair({\n tokenLogo,\n chainLogo,\n tokenSymbol,\n chainName,\n tokenOutline,\n size = 34,\n}: {\n tokenLogo?: string;\n chainLogo?: string;\n tokenSymbol?: string;\n chainName?: string;\n tokenOutline?: string;\n size?: number;\n}) {\n return (\n \n \n {chainLogo && (\n \n )}\n \n );\n}\n\nfunction SourceLogoStack({\n sources,\n size = 24,\n maxVisible = 3,\n}: {\n sources: HistorySourceRow[];\n size?: number;\n maxVisible?: number;\n}) {\n const visibleSources = sources.slice(0, maxVisible);\n const hiddenCount = Math.max(0, sources.length - visibleSources.length);\n\n return (\n \n {visibleSources.map((source, index) => (\n \n \n \n ))}\n {hiddenCount > 0 && (\n \n +{hiddenCount}\n \n )}\n \n );\n}\n\nfunction TruncatedAddress({\n address,\n color = \"var(--foreground-brand)\",\n}: {\n address: string;\n color?: string;\n}) {\n const [showTooltip, setShowTooltip] = useState(false);\n const label =\n address.length > 12\n ? `${address.slice(0, 6)}...${address.slice(-4)}`\n : address;\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n style={{\n color,\n display: \"inline-flex\",\n fontFamily: uiFont,\n fontSize: \"15px\",\n fontWeight: 500,\n lineHeight: \"20px\",\n outline: \"none\",\n position: \"relative\",\n }}\n tabIndex={0}\n >\n {label}\n {showTooltip && (\n \n {address}\n \n )}\n \n );\n}\n\nconst getDisplayDestinationSourceRow = (\n entry: SwapHistoryEntry\n): HistorySourceRow | null => {\n if (entry.mode !== \"deposit\" && entry.mode !== \"send\") return null;\n if (!entry.toToken || !entry.requestedToAmount) return null;\n\n const requestedAmount = parseDecimalLoose(entry.requestedToAmount);\n const intentDestinationAmount = parseDecimalLoose(\n entry.intentData?.destination.amount\n );\n const destinationBalanceAmount = parseDecimalLoose(\n entry.toToken.balance?.replace(entry.toToken.symbol, \"\")\n );\n if (\n !requestedAmount ||\n !destinationBalanceAmount ||\n requestedAmount.lte(0) ||\n destinationBalanceAmount.lte(0)\n ) {\n return null;\n }\n\n const intentCoversAmount = intentDestinationAmount ?? new Decimal(0);\n const displayAmount = Decimal.min(\n destinationBalanceAmount,\n Decimal.max(0, requestedAmount.minus(intentCoversAmount))\n );\n if (displayAmount.lte(0)) return null;\n\n const requestedValue = parseDecimalLoose(entry.requestedToValue);\n const destinationValue = parseDecimalLoose(\n entry.intentData?.destination.value\n );\n const rate =\n requestedValue && requestedAmount.gt(0)\n ? requestedValue.div(requestedAmount)\n : destinationValue && intentCoversAmount.gt(0)\n ? destinationValue.div(intentCoversAmount)\n : undefined;\n\n return {\n key: `destination-balance-${entry.toToken.chainId}-${entry.toToken.contractAddress}`,\n chainId: entry.toToken.chainId,\n contractAddress: entry.toToken.contractAddress,\n tokenLogo: entry.intentData?.destination.token.logo || entry.toToken.logo,\n chainLogo: entry.toToken.chainLogo,\n symbol: entry.toToken.symbol,\n chainName: getShortChainName(\n entry.toToken.chainId,\n entry.toToken.chainName\n ),\n amount: displayAmount\n .toDecimalPlaces(\n Math.max(0, entry.toToken.decimals ?? 18),\n Decimal.ROUND_DOWN\n )\n .toFixed(),\n value: rate\n ? displayAmount.mul(rate).toFixed()\n : entry.toToken.balanceInFiat,\n };\n};\n\nconst getHistorySourceRowMergeKey = (row: HistorySourceRow) => {\n const chainKey = row.chainId ?? row.chainName;\n const address = row.contractAddress?.toLowerCase();\n return address\n ? `${chainKey}:${address}`\n : `${chainKey}:symbol:${row.symbol.toUpperCase()}`;\n};\n\nconst mergeHistorySourceRows = (rows: HistorySourceRow[]) => {\n const merged = new Map();\n\n for (const row of rows) {\n const key = getHistorySourceRowMergeKey(row);\n const existing = merged.get(key);\n if (!existing) {\n merged.set(key, row);\n continue;\n }\n\n merged.set(key, {\n ...existing,\n amount: sumDecimalStrings(existing.amount, row.amount) ?? existing.amount,\n chainLogo: existing.chainLogo || row.chainLogo,\n key: existing.key,\n tokenLogo: existing.tokenLogo || row.tokenLogo,\n value: sumDecimalStrings(\n String(existing.value ?? \"\"),\n String(row.value ?? \"\")\n ),\n });\n }\n\n return Array.from(merged.values());\n};\n\nconst getProgressStepType = (step?: SwapStepType | BridgeStepType | null) =>\n String((step as any)?.type ?? (step as any)?.typeID ?? \"\").toUpperCase();\n\nconst isSourceMovementProgressStep = (type: string, rawStepType: string) =>\n rawStepType === \"source_swap\" ||\n rawStepType === \"eoa_to_ephemeral_transfer\" ||\n rawStepType === \"bridge_deposit\" ||\n rawStepType === \"vault_deposit\" ||\n type.includes(\"SOURCE_SWAP\") ||\n type.includes(\"EOA_TO_EPHEMERAL_TRANSFER\") ||\n type.includes(\"BRIDGE_DEPOSIT\") ||\n type.includes(\"VAULT_DEPOSIT\");\n\nconst isBridgeRefundStepType = (type: string) =>\n type.includes(\"BRIDGE_INTENT_SUBMISSION\") || type.includes(\"BRIDGE_DEPOSIT\");\n\nconst isSwapSkippedStepType = (type: string) => type.includes(\"SWAP_SKIPPED\");\n\nconst isAutoRefundAvailableProgressEvent = (event?: NexusWidgetProgressEvent) =>\n event?.name === PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS &&\n isBridgeRefundStepType(getProgressStepType(event.step));\n\nconst normalizeBridgeProvider = (\n value: unknown\n): BridgeProvider | undefined => {\n if (value === \"nexus\" || value === \"mayan\" || value === null) {\n return value;\n }\n return undefined;\n};\n\nconst normalizePlanStepType = (stepType: unknown, state?: unknown) => {\n const normalized = String(stepType ?? \"\").toLowerCase();\n const normalizedState = String(state ?? \"\").toLowerCase();\n\n if (normalized === \"execute_transaction\") {\n return normalizedState === \"confirmed\" || normalizedState === \"completed\"\n ? \"TRANSACTION_CONFIRMED\"\n : \"TRANSACTION_SENT\";\n }\n\n const mapped: Record = {\n allowance_approval: \"APPROVAL\",\n bridge_deposit: \"BRIDGE_DEPOSIT\",\n bridge_fill: \"BRIDGE_FILL\",\n bridge_intent_submission: \"BRIDGE_INTENT_SUBMISSION\",\n destination_swap: \"DESTINATION_SWAP\",\n eoa_to_ephemeral_transfer: \"EOA_TO_EPHEMERAL_TRANSFER\",\n execute_approval: \"APPROVAL\",\n request_signing: \"REQUEST_SIGNING\",\n request_submission: \"REQUEST_SUBMISSION\",\n source_swap: \"SOURCE_SWAP\",\n vault_deposit: \"BRIDGE_DEPOSIT\",\n };\n\n return mapped[normalized] ?? normalized.toUpperCase();\n};\n\nconst normalizePlanStep = (\n stepLike: unknown,\n fallbackStepType?: unknown,\n state?: unknown,\n completed?: boolean\n): SwapStepType | BridgeStepType => {\n const source =\n stepLike && typeof stepLike === \"object\" ? (stepLike as any) : {};\n const rawStepType = fallbackStepType ?? source.stepType ?? source.type;\n const progressType = normalizePlanStepType(\n rawStepType ?? source.typeID,\n state\n );\n const progressKey =\n source.id ?? source.stepId ?? source.typeID ?? progressType;\n\n return {\n ...source,\n completed,\n rawType: rawStepType,\n type: progressType,\n typeID: String(progressKey),\n } as SwapStepType | BridgeStepType;\n};\n\nconst getPlanStepChainId = (event: any, step: any) =>\n getObjectChainId(event) ?? getObjectChainId(step);\n\nconst getPlanStepTransactionHash = (event: any, step: any) =>\n getObjectTransactionHash(event) ?? getObjectTransactionHash(step);\n\nconst getPlanStepExplorerUrl = (event: any, step: any) => {\n const directExplorerUrl = getNonEmptyString(\n event?.explorerUrl,\n event?.explorerURL,\n event?.txExplorerUrl,\n event?.transactionExplorerUrl,\n step?.explorerUrl,\n step?.explorerURL,\n step?.txExplorerUrl,\n step?.transactionExplorerUrl,\n step?.data?.explorerUrl,\n step?.data?.explorerURL,\n step?.data?.txExplorerUrl,\n step?.data?.transactionExplorerUrl\n );\n if (directExplorerUrl) return directExplorerUrl;\n\n return getExplorerTxUrl(\n getPlanStepChainId(event, step),\n getPlanStepTransactionHash(event, step),\n event,\n step\n );\n};\n\nconst getPlanStepIntentExplorerUrl = (event: any, step: any) =>\n getNonEmptyString(\n event?.intentExplorerUrl,\n event?.intentExplorerURL,\n event?.intentUrl,\n event?.intentURL,\n event?.rffUrl,\n event?.rffURL,\n event?.rffExplorerUrl,\n event?.rffExplorerURL,\n step?.intentExplorerUrl,\n step?.intentExplorerURL,\n step?.intentUrl,\n step?.intentURL,\n step?.rffUrl,\n step?.rffURL,\n step?.rffExplorerUrl,\n step?.rffExplorerURL,\n step?.data?.intentExplorerUrl,\n step?.data?.intentExplorerURL,\n step?.data?.intentUrl,\n step?.data?.intentURL,\n step?.data?.rffUrl,\n step?.data?.rffURL,\n step?.data?.rffExplorerUrl,\n step?.data?.rffExplorerURL\n );\n\nconst isIntentSubmissionLikeEvent = (event: any, step?: any) => {\n const text = [\n event?.type,\n event?.event,\n event?.name,\n event?.status,\n event?.stepType,\n step?.type,\n step?.typeID,\n step?.rawType,\n ]\n .filter((value) => value !== null && value !== undefined)\n .join(\" \")\n .toLowerCase();\n\n return (\n text.includes(\"intent\") ||\n text.includes(\"request_submission\") ||\n text.includes(\"rff\")\n );\n};\n\nconst getGenericEventHash = (event: any, step?: any) =>\n getTransactionHash(\n event?.hash,\n event?.data?.hash,\n event?.result?.hash,\n step?.hash,\n step?.data?.hash,\n step?.result?.hash\n );\n\nconst getEventIntentExplorerUrl = (\n network: unknown,\n event: any,\n step?: any\n) => {\n const directUrl = step\n ? getPlanStepIntentExplorerUrl(event, step)\n : getNonEmptyString(\n event?.intentExplorerUrl,\n event?.intentExplorerURL,\n event?.intentUrl,\n event?.intentURL,\n event?.rffUrl,\n event?.rffURL,\n event?.rffExplorerUrl,\n event?.rffExplorerURL,\n event?.data?.intentExplorerUrl,\n event?.data?.intentExplorerURL,\n event?.data?.intentUrl,\n event?.data?.intentURL,\n event?.data?.rffUrl,\n event?.data?.rffURL,\n event?.data?.rffExplorerUrl,\n event?.data?.rffExplorerURL\n );\n if (directUrl) return directUrl;\n\n return getRffExplorerUrl(\n network,\n getObjectIntentHash(event) ||\n getObjectIntentHash(step) ||\n (isIntentSubmissionLikeEvent(event, step)\n ? getGenericEventHash(event, step)\n : null)\n );\n};\n\nconst isTimeoutLikeError = (error: unknown) => {\n const err = error as {\n code?: unknown;\n name?: unknown;\n message?: unknown;\n shortMessage?: unknown;\n };\n const text = [\n err?.code,\n err?.name,\n err?.message,\n err?.shortMessage,\n typeof error === \"string\" ? error : undefined,\n ]\n .filter((value) => value !== null && value !== undefined)\n .join(\" \");\n\n return /timeout|timed out|time out|deadline exceeded|expired while waiting|wait.*expired|poll.*expired/i.test(\n text\n );\n};\n\nconst getSdkEventType = (event: any) =>\n event?.type ?? event?.name ?? event?.event ?? \"unknown\";\n\nconst summarizeSdkProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n index?: number\n) => ({\n completed: (step as any)?.completed,\n index,\n rawType: (step as any)?.rawType ?? (step as any)?.stepType,\n state: (step as any)?.state,\n type: getProgressStepType(step),\n});\n\nconst logSdkSwapEvent = (\n label: string,\n event: any,\n meta?: Record\n) => {\n console.log(`[NexusWidget SDK][swap] ${label}`, {\n event,\n eventType: getSdkEventType(event),\n ...meta,\n });\n};\n\nconst logSdkIntentEvent = (\n label: string,\n data: any,\n meta?: Record\n) => {\n console.log(`[NexusWidget SDK][intent] ${label}`, {\n hasAllow: typeof data?.allow === \"function\",\n hasDeny: typeof data?.deny === \"function\",\n hasRefresh: typeof data?.refresh === \"function\",\n intent: data?.intent,\n raw: data,\n ...meta,\n });\n};\n\nconst logSdkIntentInput = (\n operation: string,\n input: unknown,\n meta?: Record\n) => {\n console.log(`[NexusWidget SDK][intent input] ${operation}`, {\n input,\n ...meta,\n });\n};\n\nconst normalizeSdkIntentString = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n return String(value);\n};\n\nconst normalizeSdkIntentAmount = (\n value: unknown,\n fallback: string = \"0\"\n) => normalizeSdkIntentString(value) ?? fallback;\n\nconst getLogoFromMetadata = (metadata: any) =>\n metadata?.logo ??\n metadata?.logoURI ??\n metadata?.logoUri ??\n metadata?.logoUrl ??\n metadata?.icon ??\n \"\";\n\nconst normalizeSdkIntentChain = (chain: any) => {\n const id = Number(chain?.id ?? chain?.chainId);\n if (!Number.isFinite(id)) return undefined;\n const chainMeta = CHAIN_METADATA[id];\n return {\n id,\n logo: getLogoFromMetadata(chain) || chainMeta?.logo || \"\",\n name: chain?.name ?? chainMeta?.name ?? \"\",\n };\n};\n\nconst normalizeSdkIntentToken = (token: any, chainId?: number) => {\n const chainMeta = chainId ? CHAIN_METADATA[chainId] : undefined;\n const decimals = Number(token?.decimals ?? chainMeta?.nativeCurrency.decimals);\n return {\n contractAddress:\n token?.contractAddress ??\n token?.address ??\n token?.tokenAddress ??\n zeroAddress,\n decimals: Number.isFinite(decimals) ? decimals : 18,\n logo: getLogoFromMetadata(token) || undefined,\n symbol: token?.symbol ?? token?.tokenSymbol ?? \"\",\n };\n};\n\nconst normalizeSdkIntentGas = (\n gas: any,\n chainId?: number\n): SwapIntentDestination[\"gas\"] => ({\n amount: normalizeSdkIntentAmount(gas?.amount),\n value: normalizeSdkIntentString(gas?.value),\n token: normalizeSdkIntentToken(gas?.token ?? gas, chainId),\n});\n\nconst normalizeSdkIntentSource = (\n source: any\n): SwapIntentSource | undefined => {\n const chain = normalizeSdkIntentChain(source?.chain);\n if (!chain) return undefined;\n return {\n ...source,\n amount: normalizeSdkIntentAmount(source?.amount),\n chain,\n token: normalizeSdkIntentToken(source?.token, chain.id),\n value: normalizeSdkIntentString(source?.value),\n };\n};\n\nconst normalizeSdkIntentDestination = (\n destination: any\n): SwapIntentDestination | undefined => {\n const chain = normalizeSdkIntentChain(destination?.chain);\n if (!chain) return undefined;\n return {\n ...destination,\n amount: normalizeSdkIntentAmount(destination?.amount),\n chain,\n gas: normalizeSdkIntentGas(destination?.gas, chain.id),\n token: normalizeSdkIntentToken(destination?.token, chain.id),\n value: normalizeSdkIntentString(destination?.value),\n };\n};\n\nconst normalizeSwapIntentData = (intent: any): SwapIntentData | null => {\n const destination = normalizeSdkIntentDestination(intent?.destination);\n if (!destination) return null;\n\n return {\n ...intent,\n destination,\n sources: Array.isArray(intent?.sources)\n ? intent.sources\n .map(normalizeSdkIntentSource)\n .filter(\n (source: SwapIntentSource | undefined): source is SwapIntentSource =>\n Boolean(source)\n )\n : [],\n };\n};\n\nconst normalizeSwapAndExecuteRequirementIntent = (\n intent: any\n): SwapIntentData | null => {\n const requirement = intent?.executeRequirement ?? intent?.executionRequirement;\n if (!requirement) return null;\n const destination = normalizeSdkIntentDestination({\n amount: requirement?.token?.amount,\n chain: requirement?.chain,\n gas: requirement?.gas,\n token: requirement?.token,\n value: requirement?.token?.value,\n });\n if (!destination) return null;\n\n return {\n ...intent,\n bridgeProvider: normalizeBridgeProvider(intent?.bridgeProvider),\n destination,\n feesAndBuffer: intent?.feesAndBuffer,\n sources: [],\n };\n};\n\nconst normalizeRenderableSwapIntentData = (\n rawIntent: any,\n bridgeProvider?: BridgeProvider\n): SwapIntentData | null => {\n const direct = normalizeSwapIntentData(rawIntent);\n const normalizedIntent = direct\n ? null\n : normalizeSwapIntentData(rawIntent?.normalizedIntent);\n const nestedSwap =\n direct || normalizedIntent ? null : normalizeSwapIntentData(rawIntent?.swap);\n const requirement = direct || normalizedIntent || nestedSwap\n ? null\n : normalizeSwapAndExecuteRequirementIntent(rawIntent);\n const normalized = direct ?? normalizedIntent ?? nestedSwap ?? requirement;\n if (!normalized) return null;\n\n return bridgeProvider === undefined\n ? normalized\n : { ...normalized, bridgeProvider };\n};\n\nconst logSwapPlanSteps = (\n eventType: \"plan_preview\" | \"plan_confirmed\",\n stepList: Array,\n rawSteps: unknown\n) => {\n console.log(`[NexusWidget SDK][swap] ${eventType} step list`, {\n count: stepList.length,\n eventType,\n rawSteps,\n steps: stepList.map((step, index) => summarizeSdkProgressStep(step, index)),\n });\n};\n\nconst logSwapPlanProgress = (\n event: any,\n step: SwapStepType | BridgeStepType,\n eventName: string,\n completed: boolean\n) => {\n console.log(\"[NexusWidget SDK][swap] plan_progress\", {\n completed,\n eventName,\n eventType: getSdkEventType(event),\n normalizedStep: summarizeSdkProgressStep(step),\n rawEvent: event,\n rawStep: event?.step,\n state: event?.state,\n stepType: event?.stepType,\n });\n};\n\nconst getFailureMessageForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n mode: NexusWidgetMode,\n autoRefundAvailable = false\n) => {\n if (autoRefundAvailable) {\n return \"Swap Failed. Refund Initiated\";\n }\n\n const type = getProgressStepType(step);\n if (\n type.includes(\"CREATE_PERMIT_FOR_SOURCE_SWAP\") ||\n type.includes(\"CREATE_PERMIT_EOA_TO_EPHEMERAL\") ||\n type.includes(\"EOA_EXECUTE_CALL\") ||\n type.includes(\"SOURCE_SWAP\") ||\n type.includes(\"COLLECTION\")\n ) {\n return \"Collection Failed\";\n }\n if (type.includes(\"DESTINATION_SWAP\") || type.includes(\"FULFIL\")) {\n return \"Destination Swap Failed\";\n }\n if (\n type.includes(\"TRANSACTION\") ||\n type.includes(\"APPROVAL\") ||\n type.includes(\"DEPOSIT\")\n ) {\n return mode === \"send\"\n ? \"Send failed. Funds are in your wallet\"\n : mode === \"deposit\"\n ? \"Deposit failed. Funds are in your wallet\"\n : \"Swap Failed\";\n }\n if (\n type.includes(\"SWAP\") ||\n type.includes(\"BRIDGE\") ||\n type.includes(\"INTENT\") ||\n type.includes(\"DETERMINING\")\n ) {\n return \"Swap Failed\";\n }\n return mode === \"send\"\n ? \"Send failed. Funds are in your wallet\"\n : mode === \"deposit\"\n ? \"Deposit failed. Funds are in your wallet\"\n : \"Swap Failed\";\n};\n\nconst getBridgeTokenSymbolForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined\n) => {\n const rawStep = step as any;\n return (\n getNonEmptyString(\n rawStep?.bridgeToken?.symbol,\n rawStep?.data?.bridgeToken?.symbol,\n rawStep?.bridgeTokenSymbol,\n rawStep?.data?.bridgeTokenSymbol,\n rawStep?.swaps?.[0]?.input?.symbol,\n rawStep?.data?.swaps?.[0]?.input?.symbol,\n rawStep?.input?.symbol,\n rawStep?.data?.input?.symbol,\n rawStep?.asset?.symbol,\n rawStep?.data?.asset?.symbol\n ) ?? \"USDC\"\n );\n};\n\nconst getFailureDescriptionForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n autoRefundAvailable = false\n) => {\n if (autoRefundAvailable) return undefined;\n const type = getProgressStepType(step);\n if (!type.includes(\"DESTINATION_SWAP\")) return undefined;\n const bridgeTokenSymbol = getBridgeTokenSymbolForProgressStep(step);\n return `${bridgeTokenSymbol} has been bridged and you have those funds in your wallet.`;\n};\n\nconst getSourceRows = (entry: SwapHistoryEntry): HistorySourceRow[] => {\n const sources = entry.intentData?.sources ?? [];\n const displayDestinationSourceRow = getDisplayDestinationSourceRow(entry);\n if (sources.length > 0) {\n const sourceRows = sources.map((source, index) => {\n const fallback = entry.fromTokens.find(\n (token) =>\n token.chainId === source.chain.id &&\n (token.contractAddress?.toLowerCase() ===\n source.token.contractAddress?.toLowerCase() ||\n token.symbol === source.token.symbol)\n );\n\n return {\n key: `${source.chain.id}-${source.token.contractAddress}-${index}`,\n chainId: source.chain.id,\n contractAddress: source.token.contractAddress,\n tokenLogo: source.token.logo || fallback?.logo,\n chainLogo: source.chain.logo || fallback?.chainLogo,\n symbol: source.token.symbol,\n chainName: getShortChainName(source.chain.id, source.chain.name),\n amount: source.amount,\n value: source.value,\n };\n });\n\n return mergeHistorySourceRows(\n displayDestinationSourceRow\n ? [displayDestinationSourceRow, ...sourceRows]\n : sourceRows\n );\n }\n\n const fallbackRows = entry.fromTokens.map((token, index) => ({\n key: `${token.chainId}-${token.contractAddress}-${index}`,\n chainId: token.chainId,\n contractAddress: token.contractAddress,\n tokenLogo: token.logo,\n chainLogo: token.chainLogo,\n symbol: token.symbol,\n chainName: getShortChainName(token.chainId, token.chainName),\n amount: token.userAmount || \"0\",\n value: token.balanceInFiat,\n }));\n\n return mergeHistorySourceRows(\n displayDestinationSourceRow\n ? [displayDestinationSourceRow, ...fallbackRows]\n : fallbackRows\n );\n};\n\nfunction SourceRowsList({\n entry,\n maxHeight = 236,\n borderTopFirst = true,\n scrollAfterRows = 4,\n}: {\n entry: SwapHistoryEntry;\n maxHeight?: number;\n borderTopFirst?: boolean;\n scrollAfterRows?: number;\n}) {\n const rows = getSourceRows(entry);\n const shouldScroll = rows.length > scrollAfterRows;\n const scrollRef = useRef(null);\n\n return (\n
\n \n {rows.map((row, index) => (\n 0 ? \"1px solid #E8E8E7\" : \"none\",\n display: \"flex\",\n justifyContent: \"space-between\",\n minHeight: \"64px\",\n padding: \"10px 20px\",\n }}\n >\n \n \n
\n \n {row.symbol}\n \n \n on {row.chainName || \"Unknown chain\"}\n \n
\n
\n \n \n {formatTokenDisplay(row.amount)} {row.symbol}\n \n \n {formatUsdDisplay(row.value)}\n \n \n \n ))}\n \n {shouldScroll && (\n \n scrollRef.current?.scrollBy({ top: 72, behavior: \"smooth\" })\n }\n style={{\n alignItems: \"center\",\n background: \"#FFFFFE\",\n border: \"1px solid #E8E8E7\",\n borderRadius: \"999px\",\n bottom: \"6px\",\n boxShadow: \"0 2px 8px rgba(22,22,21,0.08)\",\n display: \"flex\",\n height: \"22px\",\n justifyContent: \"center\",\n left: \"50%\",\n padding: 0,\n position: \"absolute\",\n transform: \"translateX(-50%)\",\n width: \"22px\",\n }}\n type=\"button\"\n >\n \n \n )}\n \n );\n}\n\nfunction SwapReceiptPanel({\n entry,\n onDone,\n}: {\n entry: SwapHistoryEntry;\n onDone: () => void;\n}) {\n const [showSourceDetails, setShowSourceDetails] = useState(false);\n const destination = entry.intentData?.destination;\n const isFailed = entry.status === \"failed\";\n const isTimeout = entry.status === \"timeout\";\n const isDeposit = entry.mode === \"deposit\";\n const isSend = entry.mode === \"send\";\n const isRecipientTransfer = isSend || Boolean(entry.recipientAddress);\n const tokenSymbol = destination?.token.symbol || entry.toToken?.symbol || \"\";\n const chainName = getShortChainName(\n destination?.chain.id ?? entry.toToken?.chainId,\n destination?.chain.name || entry.toToken?.chainName || \"\"\n );\n const depositVenue =\n entry.opportunity?.title || entry.opportunity?.protocol || chainName;\n const amount = destination?.amount || \"\";\n const destinationLogo =\n destination?.token.logo ||\n entry.toToken?.logo ||\n entry.opportunity?.tokenLogo ||\n (isDeposit ? entry.opportunity?.logo : undefined);\n const requestedExactOutAmount =\n (isDeposit || isSend) && entry.requestedToAmount\n ? entry.requestedToAmount\n : undefined;\n const requestedExactOutValue =\n (isDeposit || isSend) && entry.requestedToValue\n ? entry.requestedToValue\n : undefined;\n const value = requestedExactOutValue || destination?.value;\n const displayAmount = requestedExactOutAmount || amount;\n const showIntentExplorer = hasValidIntentExplorer(entry);\n const intentLabel = entry.intentId\n ? `Intent #${entry.intentId}`\n : \"View Explorer\";\n const sourceRows = getSourceRows(entry);\n const sourceCount = sourceRows.length;\n const sourceTotalUsd = sourceRows.reduce(\n (sum, source) => sum.plus(parseDecimalLoose(source.value) ?? 0),\n new Decimal(0)\n );\n const defaultSwapFailureHeadline = entry.autoRefundAvailable\n ? \"Swap Failed. Refund Initiated\"\n : \"Swap Failed\";\n const entryFailureMessage =\n entry.status === \"timeout\" ? TIMEOUT_LABEL : entry.failureMessage;\n const storedFailureMessage =\n !entry.autoRefundAvailable && entryFailureMessage?.includes(\"Refund\")\n ? undefined\n : entryFailureMessage;\n const failureHeadline =\n storedFailureMessage ||\n (isDeposit\n ? \"Deposit failed. Funds are in your wallet\"\n : isRecipientTransfer\n ? \"Send failed. Funds are in your wallet\"\n : defaultSwapFailureHeadline);\n const failureDescription = isFailed ? entry.failureDescription : undefined;\n const timeoutHeadline = TIMEOUT_LABEL;\n const timeoutDescription = isTimeout\n ? entry.failureDescription ||\n \"This transaction is still pending. Check the intent explorer for the latest status.\"\n : undefined;\n const receiptLocation = isDeposit ? depositVenue : chainName;\n const receiptSummary = receiptLocation ? `on ${receiptLocation}` : \"\";\n\n return (\n \n \n \n \n \n {isFailed ? \"x\" : isTimeout ? \"!\" : \"✓\"}\n \n \n
\n {isTimeout\n ? timeoutHeadline\n : isFailed\n ? failureHeadline\n : isDeposit\n ? \"You deposited\"\n : isRecipientTransfer\n ? \"You sent\"\n : \"You received\"}\n
\n {(failureDescription || timeoutDescription) && (\n \n {failureDescription || timeoutDescription}\n \n )}\n \n {displayAmount ? formatTokenDisplay(displayAmount) : \"--\"}\n \n {tokenSymbol}\n \n \n
\n ≈ {formatUsdDisplay(value)}\n
\n {receiptSummary && (\n \n {receiptSummary}\n \n )}\n \n\n \n \n \n {isDeposit || isSend ? \"You Paid\" : \"You Swapped\"}\n \n \n \n {formatUsdDisplay(sourceTotalUsd)}\n \n setShowSourceDetails((current) => !current)}\n style={{\n alignItems: \"center\",\n background: \"transparent\",\n border: \"none\",\n color: \"var(--foreground-brand)\",\n cursor: \"pointer\",\n display: \"inline-flex\",\n fontFamily: uiFont,\n fontSize: \"12px\",\n gap: \"4px\",\n padding: 0,\n }}\n type=\"button\"\n >\n {showSourceDetails\n ? \"Hide Details\"\n : `${sourceCount} asset${sourceCount === 1 ? \"\" : \"s\"}`}\n \n \n \n \n \n
\n \n
\n \n {isRecipientTransfer && entry.recipientAddress && (\n \n \n Recipient\n \n \n \n )}\n {showIntentExplorer && (\n \n \n Intent Explorer\n \n \n {intentLabel} ↗\n \n \n )}\n {entry.finalExplorerUrl && (\n \n \n Final Transaction\n \n \n View Explorer ↗\n \n \n )}\n \n \n Total Fees\n \n \n {formatUsdDisplay(entry.feeUsd)}\n \n \n \n\n \n Done\n \n \n );\n}\n\nconst getRelativeTime = (time: number, now: number) => {\n const seconds = Math.max(1, Math.floor((now - time) / 1000));\n if (seconds < 60) return `${seconds} second${seconds === 1 ? \"\" : \"s\"} ago`;\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes} minute${minutes === 1 ? \"\" : \"s\"} ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours} hour${hours === 1 ? \"\" : \"s\"} ago`;\n const days = Math.floor(hours / 24);\n return `${days} day${days === 1 ? \"\" : \"s\"} ago`;\n};\n\nfunction HistoryStatusPill({ status }: { status: SwapHistoryStatus }) {\n const config =\n status === \"fulfilled\"\n ? { label: \"Fulfilled\", bg: \"#E8F6EF\", fg: \"#168A47\" }\n : status === \"pending\"\n ? { label: \"Pending\", bg: \"#FFF3DE\", fg: \"#B7791F\" }\n : status === \"timeout\"\n ? { label: TIMEOUT_LABEL, bg: \"#FFF3DE\", fg: \"#B7791F\" }\n : status === \"refund-initiated\"\n ? { label: \"Refund Initiated\", bg: \"#FFF3DE\", fg: \"#B7791F\" }\n : { label: \"Failed\", bg: \"#FFE6EA\", fg: \"#E92C2C\" };\n\n return (\n \n {config.label}\n \n );\n}\n\nfunction SwapHistoryPanel({\n entries,\n now,\n}: {\n entries: SwapHistoryEntry[];\n now: number;\n}) {\n if (entries.length === 0) {\n return (\n \n \n \n ↻\n \n \n \n No transactions yet\n \n \n Your transaction history will appear here once you make your first\n swap, deposit, or send.\n \n \n );\n }\n\n const sortedEntries = sortSwapHistoryEntries(entries);\n const shouldScroll = sortedEntries.length > 5;\n\n return (\n \n {sortedEntries.map((entry) => {\n const destination = entry.intentData?.destination;\n const destinationLogo =\n destination?.token.logo ||\n entry.toToken?.logo ||\n entry.opportunity?.tokenLogo ||\n \"\";\n const destinationChainLogo =\n destination?.chain.logo || entry.toToken?.chainLogo || \"\";\n const destinationChainName = getShortChainName(\n destination?.chain.id ?? entry.toToken?.chainId,\n destination?.chain.name || entry.toToken?.chainName || \"\"\n );\n const destinationSymbol =\n destination?.token.symbol || entry.toToken?.symbol || \"\";\n const destinationValue =\n (entry.mode === \"deposit\" || entry.mode === \"send\") &&\n entry.requestedToValue\n ? entry.requestedToValue\n : destination?.value;\n const destinationAmount =\n (entry.mode === \"deposit\" || entry.mode === \"send\") &&\n entry.requestedToAmount\n ? entry.requestedToAmount\n : destination?.amount || \"\";\n const canShowRefund =\n entry.status === \"failed\" && Boolean(entry.autoRefundAvailable);\n const status = canShowRefund ? \"refund-initiated\" : entry.status;\n const sourceRows = getSourceRows(entry);\n const historyExplorerUrl = getHistoryExplorerUrl(entry);\n\n return (\n \n \n \n \n
\n \n {destinationAmount\n ? formatTokenDisplay(destinationAmount)\n : \"--\"}\n \n {destinationSymbol}\n \n
\n \n ≈ {formatUsdDisplay(destinationValue)}\n \n \n \n \n \n \n {getRelativeTime(entry.createdAt ?? entry.startedAt, now)}\n \n \n \n\n {canShowRefund && (\n \n \n Refund Initiated\n \n \n )}\n\n \n \n {sourceRows.length > 0 && (\n \n )}\n \n →\n \n \n \n {historyExplorerUrl && (\n \n \n View\n \n \n \n \n \n \n )}\n \n \n );\n })}\n \n );\n}\n\n// ---------------------------------------------------------------------------\n// NexusWidget\n// ---------------------------------------------------------------------------\n\nexport function NexusWidget(props: NexusWidgetProps) {\n return (\n \n
\n Something went wrong\n
\n \n An unexpected error occurred. Please refresh the page or try\n resetting the widget.\n \n window.location.reload()}\n style={{\n backgroundColor: \"var(--foreground-brand)\",\n border: \"none\",\n borderRadius: \"8px\",\n color: \"#FFFFFE\",\n cursor: \"pointer\",\n fontSize: \"15px\",\n fontWeight: 500,\n padding: \"8px 16px\",\n transition: \"background-color 0.15s ease-out\",\n }}\n type=\"button\"\n >\n Reload Page\n \n \n }\n >\n \n \n );\n}\n\nfunction NexusWidgetInner({\n config: rawConfig,\n embed = true,\n className,\n connectedAddress,\n open: controlledOpen,\n onOpenChange,\n defaultOpen = false,\n onComplete,\n onConnectClick,\n onStart,\n onError,\n onReceiveAssetChange,\n onClose,\n onConnectWallet,\n}: NexusWidgetProps) {\n const {\n nexusSDK,\n bridgableBalance,\n swapBalance,\n swapBalanceLoading,\n getFiatValue,\n resolveTokenUsdRate,\n swapSupportedChainsAndTokens,\n supportedChainsAndTokens,\n fetchSwapBalance,\n handleInit,\n swapIntent: providerSwapIntent,\n network,\n loading: nexusLoading,\n } = useNexus();\n const appConfig = useMemo(\n () => ({\n nexusNetwork: network ?? \"mainnet\",\n }),\n [network]\n );\n const isSwapBalancePending =\n Boolean(nexusSDK) && (swapBalanceLoading || swapBalance === null);\n\n const normalizedWidgetConfig = useMemo(\n () => normalizeNexusWidgetConfig(rawConfig),\n [rawConfig]\n );\n const config = normalizedWidgetConfig.config;\n const activeMode = normalizedWidgetConfig.activeMode;\n const amountInputConfig = normalizedWidgetConfig.amountInput;\n const appearanceConfig = normalizedWidgetConfig.appearance;\n const configuredDepositOptions = normalizedWidgetConfig.depositOptions;\n const isConfiguredAmountFixed = normalizedWidgetConfig.isAmountFixed;\n const isConfiguredRecipientLocked =\n normalizedWidgetConfig.isRecipientLocked;\n const primaryColor = normalizeNexusWidgetPrimaryColor(\n appearanceConfig?.primaryColor\n );\n const primaryButtonBackground =\n primaryColor ?? NEXUS_WIDGET_DEFAULT_PRIMARY_COLOR;\n const primaryButtonForeground = getReadableTextColor(\n primaryButtonBackground\n );\n const theme = useMemo(\n () => ({\n ...nexusWidgetTheme,\n colors: {\n ...nexusWidgetTheme.colors,\n primary: \"var(--foreground-brand)\",\n primaryText: \"var(--foreground-brand)\",\n },\n }),\n [primaryColor]\n );\n const configuredDeposit = getConfiguredDeposit(config);\n const configuredDepositIdentity = getDepositConfigIdentity(configuredDeposit);\n const configuredDestinationTokenOptions = useMemo(() => {\n const destination = rawConfig.destination;\n const destinationChainId = getConfiguredDestinationChainId(destination);\n const destinationTokens =\n rawConfig.mode === \"deposit\"\n ? configuredDepositOptions.map(toSwapTokenOptionFromDepositConfig)\n : destination?.tokens && destinationChainId\n ? destination.tokens\n .map((token) =>\n toSwapTokenOptionFromConfiguredDestinationToken(\n token,\n destinationChainId\n )\n )\n .filter((token): token is SwapTokenOption => Boolean(token))\n : [];\n\n const tokensByKey = new Map();\n for (const token of destinationTokens) {\n tokensByKey.set(getTokenSelectionKey(token), token);\n }\n return Array.from(tokensByKey.values());\n }, [configuredDepositOptions, rawConfig.destination]);\n if (activeMode === \"deposit\" && !configuredDeposit) {\n throw new Error(\n \"NexusWidget deposit mode requires destination.chain, at least one destination token, depositAddress, and executeDeposit.\"\n );\n }\n const showCloseButton = !embed && Boolean(onClose);\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const isControlledOpen = controlledOpen !== undefined;\n const isModalOpen = isControlledOpen ? controlledOpen : internalOpen;\n\n // Preload receive tokens once SDK is available\n useEffect(() => {\n if (nexusSDK) {\n console.log(\n \"[preloadReceiveTokens] Calling preloadReceiveTokens from NexusWidget useEffect (nexusSDK available)\"\n );\n preloadReceiveTokens();\n }\n }, [nexusSDK]);\n\n const {\n address: accountAddress,\n connector,\n status: walletStatus,\n } = useAccount();\n const {\n connectors,\n connectAsync,\n isPending: isWalletConnectPending,\n } = useConnect();\n const { data: walletClient } = useWalletClient();\n const { data: connectorClient } = useConnectorClient();\n const publicClient = usePublicClient();\n const walletClientAddress = walletClient?.account?.address;\n const ownerAddress =\n connectedAddress &&\n isAddress(connectedAddress) &&\n connectedAddress.toLowerCase() !== zeroAddress\n ? connectedAddress\n : accountAddress &&\n isAddress(accountAddress) &&\n accountAddress.toLowerCase() !== zeroAddress\n ? accountAddress\n : walletClientAddress &&\n isAddress(walletClientAddress) &&\n walletClientAddress.toLowerCase() !== zeroAddress\n ? walletClientAddress\n : undefined;\n const getEffectiveWalletProvider = useCallback(\n async (activeConnector = connector) => {\n let connectorProvider: unknown;\n try {\n connectorProvider = await activeConnector?.getProvider();\n } catch {\n connectorProvider = undefined;\n }\n\n const connectorClientProvider = connectorClient\n ? {\n request: (args: unknown) => connectorClient.request(args as any),\n }\n : undefined;\n const walletClientProvider = walletClient\n ? {\n request: (args: unknown) => walletClient.request(args as any),\n }\n : undefined;\n const windowProvider =\n typeof window !== \"undefined\"\n ? (window as Window & { ethereum?: EthereumProvider }).ethereum\n : undefined;\n const effectiveProvider =\n connectorProvider &&\n typeof (connectorProvider as EthereumProvider).request === \"function\"\n ? (connectorProvider as EthereumProvider)\n : (connectorClientProvider ?? walletClientProvider ?? windowProvider);\n\n return effectiveProvider &&\n typeof effectiveProvider.request === \"function\"\n ? (effectiveProvider as EthereumProvider)\n : undefined;\n },\n [connector, connectorClient, walletClient]\n );\n const historyStorageKey = getSwapHistoryStorageKey(ownerAddress);\n\n useEffect(() => {\n if (walletStatus !== \"connected\" || !ownerAddress) {\n return;\n }\n\n let cancelled = false;\n void (async () => {\n const effectiveProvider = await getEffectiveWalletProvider();\n if (cancelled || !effectiveProvider) {\n return;\n }\n await handleInit(effectiveProvider, ownerAddress);\n })().catch((error) => {\n if (!cancelled) {\n console.error(\"Unable to initialize Nexus for wallet address:\", error);\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [getEffectiveWalletProvider, handleInit, ownerAddress, walletStatus]);\n\n // Global form state\n const [amount, setAmount] = useState(\"\");\n const [recipientAddress, setRecipientAddress] = useState(\"\");\n const [editingAssetIndex, setEditingAssetIndex] = useState(\n null\n );\n const [txError, setTxError] = useState(null);\n const [walletActionPending, setWalletActionPending] = useState(false);\n const defaultRecipientAddress = ownerAddress ?? \"\";\n const effectiveRecipientAddress =\n activeMode === \"swap\"\n ? recipientAddress || defaultRecipientAddress\n : recipientAddress;\n const hasSameOwnerSendRecipient =\n activeMode === \"send\" &&\n Boolean(\n ownerAddress &&\n recipientAddress &&\n isAddress(recipientAddress) &&\n recipientAddress.toLowerCase() === ownerAddress.toLowerCase()\n );\n const hasCustomSwapRecipient =\n activeMode === \"swap\" &&\n Boolean(\n recipientAddress &&\n (!defaultRecipientAddress ||\n recipientAddress.toLowerCase() !==\n defaultRecipientAddress.toLowerCase())\n );\n const transferRecipientAddress =\n activeMode === \"send\"\n ? recipientAddress\n : hasCustomSwapRecipient\n ? recipientAddress\n : undefined;\n\n // Swap-specific\n const [swapType, setSwapType] = useState(\"exactIn\");\n const [swapStep, setSwapStep] = useState(\"idle\");\n const drawerCloseTimerRef = useRef | null>(\n null\n );\n const terminalBalanceRefreshTimerRef = useRef | null>(null);\n const [closingDrawerStep, setClosingDrawerStep] = useState(\n null\n );\n const rootContentRef = useRef(null);\n const [rootContentHeight, setRootContentHeight] = useState(\n null\n );\n const rootContentHeightRef = useRef(null);\n const [hasMeasuredRootContent, setHasMeasuredRootContent] = useState(false);\n const [shouldAnimateRootHeight, setShouldAnimateRootHeight] = useState(false);\n const [isPreviewTransitioning, setIsPreviewTransitioning] = useState(false);\n const rootHeightTransitionTimerRef = useRef | null>(null);\n const [fromTokens, setFromTokens] = useState([]);\n const [sourceSelectionTouched, setSourceSelectionTouched] = useState(false);\n const [sourceSelectionRevision, setSourceSelectionRevision] = useState(0);\n const [\n sourceSelectionRequiredUsdDisplay,\n setSourceSelectionRequiredUsdDisplay,\n ] = useState(undefined);\n const [sourcePickerDraftTokens, setSourcePickerDraftTokens] = useState<\n SwapTokenOption[] | null\n >(null);\n const sourcePickerDraftTokensRef = useRef(null);\n const sourcePickerDraftDepositFilterRef = useRef(\"all\");\n const sourcePickerDraftTouchedRef = useRef(false);\n const sourcePickerDraftModeRef = useRef<\"all\" | \"selected\">(\"all\");\n const [exactOutQuoteSourceMode, setExactOutQuoteSourceMode] = useState<\n \"all\" | \"selected\"\n >(\"all\");\n const exactOutQuoteSourceModeRef = useRef<\"all\" | \"selected\">(\"all\");\n const [toToken, setToToken] = useState(\n undefined\n );\n const [fromTokensQuoteKey, setFromTokensQuoteKey] = useState(\"\");\n\n useEffect(() => {\n const key = getSourceTokensQuoteKey(\n activeMode === \"swap\" && swapType === \"exactIn\"\n ? getReadyExactInSourceTokens(fromTokens)\n : fromTokens\n );\n setFromTokensQuoteKey(key);\n }, [activeMode, swapType, fromTokens]);\n const toTokenQuoteKey = getTokenQuoteKey(toToken);\n const appliedTokenPrefillRef = useRef(null);\n\n useEffect(() => {\n if (!toToken?.chainId || !toToken.contractAddress) return;\n\n let active = true;\n const selectedTokenKey = getTokenSelectionKey(toToken);\n const applyLoadedReceiveToken = () => {\n if (!active) return;\n const loadedToken = getCachedReceiveTokenMatch(toToken);\n if (!loadedToken) return;\n\n setToToken((current) => {\n if (!current || getTokenSelectionKey(current) !== selectedTokenKey) {\n return current;\n }\n\n const chainMeta = current.chainId\n ? CHAIN_METADATA[current.chainId]\n : undefined;\n const next = {\n ...current,\n chainLogo:\n loadedToken.chainLogo || current.chainLogo || chainMeta?.logo,\n chainName: getShortChainName(\n current.chainId,\n loadedToken.chainName || current.chainName || chainMeta?.name\n ),\n decimals: loadedToken.decimals ?? current.decimals,\n logo: loadedToken.logo || current.logo,\n name: loadedToken.name || current.name,\n priceUSD: loadedToken.priceUSD ?? current.priceUSD,\n symbol: loadedToken.symbol || current.symbol,\n };\n\n if (\n current.decimals === next.decimals &&\n current.chainLogo === next.chainLogo &&\n current.chainName === next.chainName &&\n current.logo === next.logo &&\n current.name === next.name &&\n current.priceUSD === next.priceUSD &&\n current.symbol === next.symbol\n ) {\n return current;\n }\n\n return next;\n });\n };\n\n applyLoadedReceiveToken();\n const receiveTokensPromise = preloadReceiveTokens();\n receiveTokensPromise?.then(applyLoadedReceiveToken).catch((error) => {\n if (active) {\n console.warn(\"Unable to refresh receive token metadata\", error);\n }\n });\n\n return () => {\n active = false;\n };\n }, [\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n const setExactOutQuoteSourceModeValue = useCallback(\n (mode: \"all\" | \"selected\") => {\n exactOutQuoteSourceModeRef.current = mode;\n setExactOutQuoteSourceMode(mode);\n },\n []\n );\n\n useEffect(() => {\n if (!nexusSDK) return;\n void fetchSwapBalance();\n }, [fetchSwapBalance, nexusSDK]);\n\n useEffect(() => {\n setSourceSelectionTouched(false);\n setExactOutQuoteSourceModeValue(\"all\");\n }, [activeMode, setExactOutQuoteSourceModeValue]);\n\n useEffect(() => {\n if (activeMode !== \"swap\") return;\n\n setRecipientAddress((current) => {\n if (!defaultRecipientAddress) return current ? \"\" : current;\n return current.toLowerCase() === defaultRecipientAddress.toLowerCase()\n ? current\n : defaultRecipientAddress;\n });\n }, [activeMode, defaultRecipientAddress]);\n\n const {\n steps,\n seed,\n onStepsList,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n const [progressEvents, setProgressEvents] = useState(\n []\n );\n const progressEventsRef = useRef([]);\n const swapStepsListRef = useRef([]);\n const [failedProgressStep, setFailedProgressStep] = useState<\n SwapStepType | BridgeStepType | null\n >(null);\n const [explorerUrls, setExplorerUrls] = useState<{\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n }>({ sourceExplorerUrl: null, destinationExplorerUrl: null });\n const swapRunIdRef = useRef(0);\n\n const widgetSessionIdRef = useRef(\n typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`\n );\n const widgetAttemptIdRef = useRef(null);\n const widgetOpenedTsRef = useRef(Date.now());\n const previewViewedTsRef = useRef(null);\n const previewConfirmedTsRef = useRef(null);\n const attemptCountRef = useRef(0);\n const fundsMovedRef = useRef(false);\n const intentUrlRef = useRef(null);\n const hadSimulationSuccessRef = useRef(false);\n const hadPreviewViewedRef = useRef(false);\n const widgetOpenedFiredRef = useRef(false);\n const reachedTerminalRef = useRef(false);\n const lastIntentSourceTokensRef = useRef([]);\n const lastAutoIntentSourceTokensRef = useRef([]);\n const immediateQuoteAfterSourceEditRef = useRef(false);\n const amountEnteredLastValueRef = useRef(\"\");\n const lastInputMethodRef = useRef<\n | \"typed\"\n | \"percent_20\"\n | \"percent_25\"\n | \"percent_50\"\n | \"percent_75\"\n | \"percent_max\"\n >(\"typed\");\n const prevSourceTouchedRef = useRef(false);\n const previousAutoSourceCountRef = useRef(0);\n const analyticsRef = useRef<{\n track: (event: string, properties?: Record) => void;\n } | null>(null);\n const selectedOpportunityRef = useRef(\n undefined\n );\n\n const newAttemptId = useCallback(() => {\n return typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`;\n }, []);\n\n const rotateAttempt = useCallback(() => {\n widgetAttemptIdRef.current = newAttemptId();\n previewViewedTsRef.current = null;\n previewConfirmedTsRef.current = null;\n fundsMovedRef.current = false;\n intentUrlRef.current = null;\n hadSimulationSuccessRef.current = false;\n hadPreviewViewedRef.current = false;\n reachedTerminalRef.current = false;\n }, [newAttemptId]);\n const [intentToAmount, setIntentToAmount] = useState(\n undefined\n );\n const [intentFeeUsd, setIntentFeeUsd] = useState(\n undefined\n );\n const [intentLoading, setIntentLoading] = useState(false);\n const [quoteRefreshing, setQuoteRefreshing] = useState(false);\n const [receiveMaxCalculating, setReceiveMaxCalculating] = useState(false);\n const [maxCalculationPercent, setMaxCalculationPercent] = useState<\n number | null\n >(null);\n const intentDestinationUsdRateCacheRef = useRef<\n Record\n >({});\n const intentSymbolUsdRateCacheRef = useRef<\n Record\n >({});\n const predictiveQuoteCacheRef = useRef<\n Record\n >({});\n const predictiveQuoteRunRef = useRef(0);\n const latestIntentFetchRunIdRef = useRef(0);\n const intentFetchTimingRef = useRef(null);\n const intentUiRenderLoggedRunIdRef = useRef(null);\n const [intentFetchTiming, setIntentFetchTiming] =\n useState(null);\n const [predictiveQuote, setPredictiveQuote] =\n useState(null);\n const maxPercentRunRef = useRef(0);\n const [previewQuoteRefreshing, setPreviewQuoteRefreshing] = useState(false);\n const [quoteRefreshProgress, setQuoteRefreshProgress] = useState(0);\n const [quoteRefreshSecondsRemaining, setQuoteRefreshSecondsRemaining] =\n useState(0);\n const [intentData, setIntentData] = useState(null);\n const [swapQuoteIssue, setSwapQuoteIssue] = useState(\n null\n );\n const [receiveAmountIssue, setReceiveAmountIssue] =\n useState(null);\n const receiveAmountIssueRef = useRef(null);\n const receiveAmountIssueKeyRef = useRef(\"\");\n const [transferExplorerUrl, setTransferExplorerUrl] = useState(\n null\n );\n const swapStepRef = useRef(swapStep);\n const syncingIntentSourcesRef = useRef(false);\n const lastSwapIntentRefreshAtRef = useRef(0);\n const [destinationBalance, setDestinationBalance] = useState(\n null\n );\n const [swapHistory, setSwapHistory] = useState(() =>\n readSwapHistoryFromStorage(historyStorageKey)\n );\n const [currentSwapId, setCurrentSwapId] = useState(null);\n const [historyNow, setHistoryNow] = useState(() => Date.now());\n const currentSwapIdRef = useRef(null);\n const currentSwapStartedAtRef = useRef(0);\n const historyStorageKeyRef = useRef(historyStorageKey);\n const skipNextHistoryPersistRef = useRef(false);\n const explorerUrlsRef = useRef<{\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n }>({ sourceExplorerUrl: null, destinationExplorerUrl: null });\n const activeQuoteInputKeyRef = useRef(\"\");\n\n // Ref to store swap intent hook allow/deny callbacks\n const swapIntentRef = useRef<{\n intent?: SwapIntentData;\n allow: () => void;\n deny: () => void;\n refresh: () => Promise;\n runId?: number;\n quoteInputKey?: string;\n } | null>(null);\n\n const startIntentFetchTiming = useCallback(\n ({\n background,\n operation,\n quoteInputKey,\n runId,\n }: {\n background: boolean;\n operation: string;\n quoteInputKey: string;\n runId: number;\n }) => {\n const startTimeMs = Date.now();\n const timing: IntentFetchTiming = {\n background,\n operation,\n quoteInputKey,\n runId,\n startTimeMs,\n walletAddress: ownerAddress ?? null,\n };\n\n latestIntentFetchRunIdRef.current = runId;\n intentFetchTimingRef.current = timing;\n intentUiRenderLoggedRunIdRef.current = null;\n setIntentFetchTiming(timing);\n console.log(\"[NexusWidget] intent fetch start\", {\n runId,\n walletAddress: timing.walletAddress,\n operation,\n quoteInputKey,\n background,\n dateNow: startTimeMs,\n date: new Date(startTimeMs),\n });\n return timing;\n },\n [ownerAddress]\n );\n\n const finishIntentFetchTiming = useCallback(\n (runId: number, status: \"resolved\" | \"failed\" = \"resolved\") => {\n const timing = intentFetchTimingRef.current;\n if (!timing || timing.runId !== runId) return;\n if (timing.endTimeMs !== undefined) return;\n\n const endTimeMs = Date.now();\n if (latestIntentFetchRunIdRef.current !== runId) {\n return;\n }\n\n const fetchDurationMs = endTimeMs - timing.startTimeMs;\n const completedTiming: IntentFetchTiming = {\n ...timing,\n endTimeMs,\n fetchDurationMs,\n };\n intentFetchTimingRef.current = completedTiming;\n setIntentFetchTiming(completedTiming);\n console.log(\"[NexusWidget] intent fetch end\", {\n runId,\n walletAddress: timing.walletAddress,\n operation: timing.operation,\n quoteInputKey: timing.quoteInputKey,\n status,\n dateNow: endTimeMs,\n date: new Date(endTimeMs),\n });\n console.log(\"[NexusWidget] intent fetch total\", {\n runId,\n walletAddress: timing.walletAddress,\n operation: timing.operation,\n quoteInputKey: timing.quoteInputKey,\n totalTimeSeconds: fetchDurationMs / 1000,\n elapsedMs: fetchDurationMs,\n dateNow: endTimeMs,\n });\n },\n []\n );\n\n useEffect(() => {\n swapStepRef.current = swapStep;\n }, [swapStep]);\n\n useEffect(() => {\n return () => {\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n }\n if (terminalBalanceRefreshTimerRef.current) {\n clearTimeout(terminalBalanceRefreshTimerRef.current);\n }\n if (rootHeightTransitionTimerRef.current) {\n clearTimeout(rootHeightTransitionTimerRef.current);\n }\n };\n }, []);\n\n const isQuoteEditLocked = useCallback(\n () => swapStepRef.current === \"choose-swap-asset\",\n []\n );\n\n const getQuoteRequestDelay = useCallback(() => {\n if (immediateQuoteAfterSourceEditRef.current) {\n immediateQuoteAfterSourceEditRef.current = false;\n return 0;\n }\n return EXACT_OUT_INPUT_DEBOUNCE_MS;\n }, []);\n\n const startRootHeightTransition = useCallback(() => {\n setShouldAnimateRootHeight(true);\n if (rootHeightTransitionTimerRef.current) {\n clearTimeout(rootHeightTransitionTimerRef.current);\n }\n rootHeightTransitionTimerRef.current = setTimeout(() => {\n setShouldAnimateRootHeight(false);\n rootHeightTransitionTimerRef.current = null;\n }, ROOT_HEIGHT_TRANSITION_MS);\n }, []);\n\n const closeDrawerToIdle = useCallback(() => {\n const isDrawerStep =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n swapStep === \"enter-recipient\";\n\n if (!isDrawerStep) {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n return;\n }\n\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n }\n\n setClosingDrawerStep(swapStep);\n drawerCloseTimerRef.current = setTimeout(() => {\n if (\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\"\n ) {\n startRootHeightTransition();\n }\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n setClosingDrawerStep(null);\n drawerCloseTimerRef.current = null;\n }, DRAWER_CLOSE_MS);\n }, [startRootHeightTransition, swapStep]);\n\n const openDrawerStep = useCallback((nextStep: SwapStep) => {\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n drawerCloseTimerRef.current = null;\n }\n if (\n nextStep === \"choose-swap-asset\" ||\n nextStep === \"choose-receive-asset\"\n ) {\n startRootHeightTransition();\n }\n setClosingDrawerStep(null);\n swapStepRef.current = nextStep;\n setSwapStep(nextStep);\n }, [startRootHeightTransition]);\n\n const syncRootContentHeight = useCallback((animate = false) => {\n const element = rootContentRef.current;\n if (!element) return;\n\n const nextHeight = Math.ceil(\n Math.max(element.getBoundingClientRect().height, element.scrollHeight)\n );\n if (nextHeight <= 0) return;\n\n if (rootContentHeightRef.current === nextHeight) {\n setHasMeasuredRootContent(true);\n return;\n }\n\n rootContentHeightRef.current = nextHeight;\n setShouldAnimateRootHeight(animate);\n if (rootHeightTransitionTimerRef.current) {\n clearTimeout(rootHeightTransitionTimerRef.current);\n rootHeightTransitionTimerRef.current = null;\n }\n if (animate) {\n rootHeightTransitionTimerRef.current = setTimeout(() => {\n setShouldAnimateRootHeight(false);\n rootHeightTransitionTimerRef.current = null;\n }, ROOT_HEIGHT_TRANSITION_MS);\n }\n setRootContentHeight(nextHeight);\n setHasMeasuredRootContent(true);\n }, []);\n\n useLayoutEffect(() => {\n syncRootContentHeight(true);\n\n const element = rootContentRef.current;\n if (!element || typeof ResizeObserver === \"undefined\") return;\n\n let frame = 0;\n const observer = new ResizeObserver(() => {\n if (frame) {\n window.cancelAnimationFrame(frame);\n }\n frame = window.requestAnimationFrame(() => {\n syncRootContentHeight(false);\n });\n });\n\n observer.observe(element);\n\n return () => {\n if (frame) {\n window.cancelAnimationFrame(frame);\n }\n observer.disconnect();\n };\n }, [activeMode, swapStep, syncRootContentHeight]);\n\n useEffect(() => {\n currentSwapIdRef.current = currentSwapId;\n }, [currentSwapId]);\n\n useEffect(() => {\n if (historyStorageKeyRef.current === historyStorageKey) return;\n historyStorageKeyRef.current = historyStorageKey;\n skipNextHistoryPersistRef.current = true;\n setSwapHistory(readSwapHistoryFromStorage(historyStorageKey));\n }, [historyStorageKey]);\n\n useEffect(() => {\n if (skipNextHistoryPersistRef.current) {\n skipNextHistoryPersistRef.current = false;\n return;\n }\n\n writeSwapHistoryToStorage(historyStorageKey, swapHistory);\n }, [historyStorageKey, swapHistory]);\n\n useEffect(() => {\n if (swapStep !== \"history\") return;\n const timer = window.setInterval(() => setHistoryNow(Date.now()), 30000);\n return () => window.clearInterval(timer);\n }, [swapStep]);\n\n const normalizeAddress = (value?: string | null) =>\n (value ?? \"\").toLowerCase();\n\n const getTokenLookupAddress = (value?: string | null) => {\n if (!value) return \"\";\n return isNativeTokenAddress(value) ? zeroAddress : value.toLowerCase();\n };\n\n const findBalanceTokenLogo = (\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId) return \"\";\n const lookupAddress = getTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n let symbolMatchLogo = \"\";\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== chainId) continue;\n const breakdownAddress = getTokenLookupAddress(\n breakdown.contractAddress\n );\n const addressMatches =\n lookupAddress && breakdownAddress === lookupAddress;\n const symbolMatches =\n lookupSymbol &&\n [breakdown.symbol, asset.symbol]\n .filter(Boolean)\n .some((candidate) => candidate.toUpperCase() === lookupSymbol);\n const logo = getLogoFromMetadata(breakdown) || getLogoFromMetadata(asset);\n\n if (addressMatches && logo) return logo;\n if (!symbolMatchLogo && symbolMatches && logo) {\n symbolMatchLogo = logo;\n }\n }\n }\n\n return symbolMatchLogo;\n };\n\n const findSupportedTokenLogo = (\n chains: any[] | null | undefined,\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId) return \"\";\n const chain = chains?.find(\n (item: any) => Number(item?.id ?? item?.chainId) === chainId\n );\n const tokens = chain?.tokens ?? chain?.assets ?? [];\n const lookupAddress = getTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n\n const addressMatch =\n lookupAddress &&\n tokens.find((token: any) => {\n const tokenAddress = getTokenLookupAddress(\n token?.contractAddress ?? token?.address ?? token?.tokenAddress\n );\n return tokenAddress === lookupAddress;\n });\n const symbolMatch =\n lookupSymbol &&\n tokens.find((token: any) => {\n const tokenSymbol = (token?.symbol ?? token?.tokenSymbol ?? \"\")\n .toString()\n .toUpperCase();\n return tokenSymbol === lookupSymbol;\n });\n\n return getLogoFromMetadata(addressMatch) || getLogoFromMetadata(symbolMatch);\n };\n\n const resolveTokenLogo = (\n token:\n | {\n address?: string;\n contractAddress?: string;\n logo?: string;\n symbol?: string;\n tokenAddress?: string;\n tokenSymbol?: string;\n }\n | undefined,\n chainId?: number\n ) => {\n const explicitLogo = getLogoFromMetadata(token);\n if (explicitLogo) return explicitLogo;\n\n const contractAddress =\n token?.contractAddress ?? token?.address ?? token?.tokenAddress;\n const symbol = token?.symbol ?? token?.tokenSymbol;\n const citreaToken = findCitreaReceiveToken({\n address: contractAddress,\n chainId,\n symbol,\n });\n const tokenMeta = symbol\n ? TOKEN_METADATA[symbol as keyof typeof TOKEN_METADATA]\n : undefined;\n\n return (\n findBalanceTokenLogo(chainId, contractAddress, symbol) ||\n findSupportedTokenLogo(\n swapSupportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol\n ) ||\n findSupportedTokenLogo(\n supportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol\n ) ||\n citreaToken?.logo ||\n tokenMeta?.logo ||\n (contractAddress && isNativeTokenAddress(contractAddress)\n ? CHAIN_METADATA[chainId ?? 0]?.logo\n : \"\") ||\n \"\"\n );\n };\n\n const enrichIntentToken = <\n T extends {\n address?: string;\n contractAddress?: string;\n logo?: string;\n symbol?: string;\n tokenAddress?: string;\n tokenSymbol?: string;\n },\n >(\n token: T,\n chainId?: number\n ): T => {\n const logo = resolveTokenLogo(token, chainId);\n return logo && logo !== token.logo ? { ...token, logo } : token;\n };\n\n const enrichTokenOptionLogo = (\n token?: SwapTokenOption\n ): SwapTokenOption | undefined => {\n if (!token) return undefined;\n\n const chainMeta = token.chainId ? CHAIN_METADATA[token.chainId] : undefined;\n const logo = resolveTokenLogo(token, token.chainId);\n const next = {\n ...token,\n chainLogo: token.chainLogo || chainMeta?.logo,\n logo: token.logo || logo,\n };\n\n if (next.chainLogo === token.chainLogo && next.logo === token.logo) {\n return token;\n }\n\n return next;\n };\n\n const enrichSwapIntentTokenMetadata = (\n intent: SwapIntentData\n ): SwapIntentData => ({\n ...intent,\n destination: {\n ...intent.destination,\n token: enrichIntentToken(\n intent.destination.token,\n intent.destination.chain.id\n ),\n },\n sources: (intent.sources ?? []).map((source) => ({\n ...source,\n token: enrichIntentToken(source.token, source.chain.id),\n })),\n });\n\n const buildIntentSourceToken = (\n source: SwapIntentData[\"sources\"][number]\n ): SwapTokenOption => {\n let matchedAsset: any;\n let matchedBreakdown: any;\n const sourceAddress = normalizeAddress(source.token.contractAddress);\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const addressMatches =\n normalizeAddress(breakdown.contractAddress) === sourceAddress;\n const symbolMatches =\n breakdown.symbol === source.token.symbol ||\n asset.symbol === source.token.symbol;\n if (\n breakdown.chain?.id === source.chain.id &&\n (addressMatches || symbolMatches)\n ) {\n matchedAsset = asset;\n matchedBreakdown = breakdown;\n break;\n }\n }\n if (matchedBreakdown) break;\n }\n\n const chainMeta = CHAIN_METADATA[source.chain.id];\n const sourceValue = Number((source as any).value ?? 0);\n const isNativeSource = isNativeTokenAddress(source.token.contractAddress);\n const nativeCurrency = chainMeta?.nativeCurrency;\n const sourceSymbol =\n isNativeSource && (!source.token.symbol || !matchedAsset?.logo)\n ? nativeCurrency?.symbol || source.token.symbol\n : source.token.symbol || nativeCurrency?.symbol || \"\";\n const sourceDecimals =\n isNativeSource && nativeCurrency?.decimals !== undefined\n ? nativeCurrency.decimals\n : source.token.decimals;\n const sourceLogo = resolveTokenLogo(source.token, source.chain.id);\n\n return {\n contractAddress: source.token.contractAddress,\n symbol: sourceSymbol,\n name: sourceSymbol,\n logo: sourceLogo ?? \"\",\n decimals: sourceDecimals,\n balance: matchedBreakdown?.balance\n ? `${matchedBreakdown.balance} ${sourceSymbol}`\n : `${source.amount} ${sourceSymbol}`,\n balanceInFiat:\n matchedBreakdown?.balanceInFiat != null\n ? `$${Number(matchedBreakdown.balanceInFiat).toFixed(2)}`\n : Number.isFinite(sourceValue)\n ? `$${sourceValue.toFixed(2)}`\n : \"$0.00\",\n chainId: source.chain.id,\n chainName: getShortChainName(\n source.chain.id,\n chainMeta?.name ?? source.chain.name\n ),\n chainLogo: chainMeta?.logo ?? source.chain.logo,\n userAmount: source.amount,\n userAmountUsd: Number.isFinite(sourceValue) ? source.value : undefined,\n userAmountMode: \"token\",\n };\n };\n\n const clearPendingSwapIntent = (\n clearQuote = true,\n options: { keepQuoteRefreshing?: boolean } = {}\n ) => {\n swapRunIdRef.current += 1;\n swapIntentRef.current?.deny();\n swapIntentRef.current = null;\n setIntentLoading(false);\n setTxError(null);\n if (!options.keepQuoteRefreshing) {\n setQuoteRefreshing(false);\n }\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setSwapQuoteIssue(null);\n resetProgressEvents();\n if (swapStepsListRef.current.length > 0 || steps.length > 0) {\n swapStepsListRef.current = [];\n resetSteps();\n } else {\n swapStepsListRef.current = [];\n }\n if (clearQuote) {\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n if (!options.keepQuoteRefreshing) {\n setPredictiveQuote(null);\n }\n }\n };\n\n const clearSelectedSources = () => {\n setFromTokens((current) => (current.length === 0 ? current : []));\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setExactOutQuoteSourceModeValue(\"all\");\n };\n\n const resetExactOutSourcesToAuto = () => {\n setFromTokens((current) => (current.length === 0 ? current : []));\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setExactOutQuoteSourceModeValue(\"all\");\n setSourceSelectionRevision((current) => current + 1);\n };\n\n const getSourceAmountInput = (tokens: SwapTokenOption[]) => {\n const total = tokens.reduce(\n (sum, token) => sum + Number(token.userAmount || 0),\n 0\n );\n return total > 0 ? String(total) : \"\";\n };\n\n const parseFiatNumber = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = normalizeDecimalInputText(value);\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n };\n\n const minimumSourceUsd = new Decimal(1);\n const hasMinimumSourceUsdValue = (value: unknown) =>\n (parseFiatNumber(value) ?? new Decimal(0)).gte(minimumSourceUsd);\n const hasMinimumSourceUsdBalance = (\n token: Pick\n ) => hasMinimumSourceUsdValue(token.balanceInFiat);\n const filterMinimumSourceUsdTokens = (tokens: SwapTokenOption[]) =>\n tokens.filter(hasMinimumSourceUsdBalance);\n\n const getTokenUsdRateCacheKeyFromParts = (\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId || !symbol) return \"\";\n return [\n chainId,\n (contractAddress || zeroAddress).toLowerCase(),\n symbol.toUpperCase(),\n ].join(\":\");\n };\n\n const getTokenUsdRateCacheKey = (\n token?: Pick\n ) =>\n getTokenUsdRateCacheKeyFromParts(\n token?.chainId,\n token?.contractAddress,\n token?.symbol\n );\n\n const getSymbolUsdRateCacheKey = (symbol?: string) =>\n symbol ? symbol.trim().toUpperCase() : \"\";\n\n const getCachedIntentUsdRate = (\n token?: Pick\n ) => {\n const tokenKey = getTokenUsdRateCacheKey(token);\n const cached = tokenKey\n ? intentDestinationUsdRateCacheRef.current[tokenKey]\n : undefined;\n const rate = parseFiatNumber(cached?.rate);\n return rate && rate.gt(0) ? rate : undefined;\n };\n\n const cacheDestinationUsdRateFromIntent = (\n intent?: SwapIntentData | null\n ) => {\n const destination = intent?.destination;\n const amount = parseFiatNumber(destination?.amount);\n const value = parseFiatNumber(destination?.value);\n const chainId = destination?.chain?.id;\n const symbol = destination?.token?.symbol;\n\n if (\n !amount ||\n !value ||\n amount.lte(0) ||\n value.lte(0) ||\n !chainId ||\n !symbol\n ) {\n return;\n }\n\n const rate = value.div(amount);\n if (!rate.isFinite() || rate.lte(0)) return;\n\n const cached: CachedIntentUsdRate = {\n amount: amount.toFixed(),\n rate: rate.toDecimalPlaces(18).toFixed(),\n updatedAt: Date.now(),\n value: value.toFixed(),\n };\n const tokenKey = getTokenUsdRateCacheKeyFromParts(\n chainId,\n destination?.token?.contractAddress,\n symbol\n );\n if (tokenKey) {\n intentDestinationUsdRateCacheRef.current[tokenKey] = cached;\n }\n\n const symbolKey = getSymbolUsdRateCacheKey(symbol);\n if (symbolKey) {\n intentSymbolUsdRateCacheRef.current[symbolKey] = cached;\n }\n };\n\n const getSwapBalanceTotalUsd = () =>\n (swapBalance ?? []).reduce((sum, asset) => {\n const breakdown = asset.breakdown ?? [];\n if (breakdown.length > 0) {\n return sum.plus(\n breakdown.reduce((breakdownSum, item) => {\n const value = parseFiatNumber(item.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd)\n ? breakdownSum.plus(value)\n : breakdownSum;\n }, new Decimal(0))\n );\n }\n\n const value = parseFiatNumber(asset.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd) ? sum.plus(value) : sum;\n }, new Decimal(0));\n\n const getTokenUsdRate = (token: SwapTokenOption) => {\n const tokenBalance = parseFiatNumber(token.balance) ?? new Decimal(0);\n const fiatBalance = parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n if (tokenBalance.gt(0) && fiatBalance.gt(0)) {\n return fiatBalance.div(tokenBalance);\n }\n\n const priceUsd = parseFiatNumber(token.priceUSD);\n if (priceUsd && priceUsd.gt(0)) {\n return priceUsd;\n }\n\n const fallbackRate = getFiatValue(1, token.symbol);\n if (Number.isFinite(fallbackRate) && fallbackRate > 0) {\n return new Decimal(fallbackRate);\n }\n\n return getCachedIntentUsdRate(token) ?? new Decimal(0);\n };\n const getUsdRateForSymbol = (symbol?: string) => {\n if (!symbol) return new Decimal(0);\n const fiat = getFiatValue(1, symbol);\n if (Number.isFinite(fiat) && fiat > 0) {\n return new Decimal(fiat);\n }\n\n const cached =\n intentSymbolUsdRateCacheRef.current[getSymbolUsdRateCacheKey(symbol)];\n const rate = parseFiatNumber(cached?.rate);\n return rate && rate.gt(0) ? rate : new Decimal(0);\n };\n\n const getFeeTokenLookupAddress = (value?: string | null) => {\n const lower = (value ?? \"\").toLowerCase();\n if (!lower || lower === \"0x\" || isNativeTokenAddress(lower)) {\n return zeroAddress;\n }\n return lower;\n };\n\n const getTokenUsdRateFromBalances = (\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId) return undefined;\n const lookupAddress = getFeeTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== chainId) continue;\n const breakdownAddress = getFeeTokenLookupAddress(\n breakdown.contractAddress\n );\n const addressMatches =\n lookupAddress && breakdownAddress === lookupAddress;\n const symbolMatches =\n lookupSymbol &&\n [breakdown.symbol, asset.symbol]\n .filter(Boolean)\n .some((candidate) => candidate.toUpperCase() === lookupSymbol);\n if (!addressMatches && !symbolMatches) continue;\n\n const balance = parseFiatNumber(breakdown.balance);\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n if (balance && fiatBalance && balance.gt(0) && fiatBalance.gt(0)) {\n return fiatBalance.div(balance);\n }\n }\n }\n\n return undefined;\n };\n\n const getTokenUsdRateFromSupportedChains = (\n chains: any[] | null | undefined,\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId) return undefined;\n const chain = chains?.find(\n (item: any) => Number(item?.id ?? item?.chainId) === chainId\n );\n const tokens = chain?.tokens ?? chain?.assets ?? [];\n const lookupAddress = getFeeTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n const matchedToken = tokens.find((token: any) => {\n const tokenAddress = getFeeTokenLookupAddress(\n token?.contractAddress ?? token?.address ?? token?.tokenAddress\n );\n const addressMatches = lookupAddress && tokenAddress === lookupAddress;\n const tokenSymbol = (token?.symbol ?? token?.tokenSymbol ?? \"\")\n .toString()\n .toUpperCase();\n const symbolMatches = lookupSymbol && tokenSymbol === lookupSymbol;\n return addressMatches || symbolMatches;\n });\n const priceUsd = parseFiatNumber(\n matchedToken?.priceUSD ??\n matchedToken?.priceUsd ??\n matchedToken?.usdPrice ??\n matchedToken?.price\n );\n return priceUsd && priceUsd.gt(0) ? priceUsd : undefined;\n };\n\n const getIntentDestinationGasUsdValue = (\n intent?: SwapIntentData | null\n ) => {\n const gas = intent?.destination?.gas;\n const explicitValue = parseFiatNumber(gas?.value);\n if (explicitValue && explicitValue.gt(0)) return explicitValue;\n\n const amount = parseFiatNumber(gas?.amount);\n if (!amount || amount.lte(0)) return undefined;\n\n const chainId = intent?.destination?.chain?.id;\n const contractAddress = gas?.token?.contractAddress;\n const symbol = gas?.token?.symbol;\n const rate =\n getTokenUsdRateFromBalances(chainId, contractAddress, symbol) ??\n getTokenUsdRateFromSupportedChains(\n swapSupportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol\n ) ??\n getTokenUsdRateFromSupportedChains(\n supportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol\n ) ??\n getUsdRateForSymbol(symbol);\n\n return rate.gt(0) ? amount.mul(rate) : undefined;\n };\n\n const getCachedDestinationUsdRate = (token?: SwapTokenOption) => {\n return getCachedIntentUsdRate(token);\n };\n\n const resolveUsdRateForSymbol = async (symbol?: string) => {\n if (!symbol) return new Decimal(0);\n\n const localRate = getUsdRateForSymbol(symbol);\n if (localRate.gt(0)) return localRate;\n\n try {\n const resolvedRate = await resolveTokenUsdRate(symbol);\n return resolvedRate && resolvedRate > 0\n ? new Decimal(resolvedRate)\n : new Decimal(0);\n } catch {\n return new Decimal(0);\n }\n };\n\n const getTokenUsdValue = (\n token: SwapTokenOption,\n fallbackAmount?: string\n ) => {\n const amountNumber =\n parseFiatNumber(token.userAmount || fallbackAmount) ?? new Decimal(0);\n if (amountNumber.lte(0)) return new Decimal(0);\n const quotedUsd = parseFiatNumber(token.userAmountUsd);\n if (quotedUsd && quotedUsd.gte(0)) return quotedUsd;\n if (token.userAmountMode === \"usd\") return amountNumber;\n\n const rate = getTokenUsdRate(token);\n return rate.gt(0) ? amountNumber.mul(rate) : new Decimal(0);\n };\n\n const getTokenBalanceAmount = (token: SwapTokenOption) =>\n parseFiatNumber(token.balance) ?? new Decimal(0);\n\n const getTokenBalanceUsd = (token: SwapTokenOption) =>\n parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n\n const getSdkExactOutSourcePriority = (token: SwapTokenOption) => {\n const isSameChain =\n Boolean(token.chainId && toToken?.chainId) &&\n token.chainId === toToken?.chainId;\n const isSameToken =\n Boolean(token.contractAddress && toToken?.contractAddress) &&\n ((isNativeTokenAddress(token.contractAddress) &&\n isNativeTokenAddress(toToken?.contractAddress)) ||\n token.contractAddress.toLowerCase() ===\n toToken?.contractAddress.toLowerCase());\n const isStable = isSdkExactOutStableSymbol(token.symbol);\n const isGas = isNativeTokenAddress(token.contractAddress);\n const isEthereum = token.chainId === ETHEREUM_MAINNET_CHAIN_ID;\n\n if (isSameChain) {\n if (isSameToken) return 1;\n if (isStable) return 2;\n if (isGas) return 3;\n return 4;\n }\n\n if (isEthereum) {\n if (isSameToken) return 8;\n if (isStable) return 9;\n if (isGas) return 10;\n return 11;\n }\n\n if (isSameToken) return 5;\n if (isStable) return 6;\n return 7;\n };\n\n const sortExactOutSourcesBySdkPriority = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const priorityDiff =\n getSdkExactOutSourcePriority(a) - getSdkExactOutSourcePriority(b);\n if (priorityDiff !== 0) return priorityDiff;\n\n const usdDiff = getTokenBalanceUsd(b).cmp(getTokenBalanceUsd(a));\n if (usdDiff !== 0) return usdDiff;\n\n return `${a.symbol} ${a.chainName ?? \"\"}`.localeCompare(\n `${b.symbol} ${b.chainName ?? \"\"}`\n );\n });\n\n const getTokenAmountForUsd = (token: SwapTokenOption, usdAmount: Decimal) => {\n const rate = getTokenUsdRate(token);\n if (rate.lte(0) || usdAmount.lte(0)) return new Decimal(0);\n return usdAmount.div(rate);\n };\n\n const getUsdForTokenAmount = (\n token: SwapTokenOption,\n tokenAmount: Decimal\n ) => {\n const rate = getTokenUsdRate(token);\n if (rate.lte(0) || tokenAmount.lte(0)) return new Decimal(0);\n return tokenAmount.mul(rate);\n };\n\n const getExactOutDestinationBalanceCoverage = ({\n requestedAmount,\n requestedUsd,\n producedAmount,\n producedUsd,\n token = toToken,\n }: {\n requestedAmount?: Decimal;\n requestedUsd?: Decimal;\n producedAmount?: Decimal;\n producedUsd?: Decimal;\n token?: SwapTokenOption;\n }) => {\n if (\n (activeMode !== \"deposit\" && activeMode !== \"send\") ||\n !token ||\n !requestedAmount ||\n requestedAmount.lte(0)\n ) {\n return null;\n }\n\n const balanceAmount =\n parseFiatNumber(destinationBalance) ??\n parseFiatNumber(token.balance) ??\n new Decimal(0);\n if (balanceAmount.lte(0)) return null;\n\n const externalAmount =\n producedAmount && producedAmount.gt(0) ? producedAmount : new Decimal(0);\n const uncoveredAmount = Decimal.max(\n requestedAmount.minus(externalAmount),\n new Decimal(0)\n );\n const coveredAmount = Decimal.min(balanceAmount, uncoveredAmount);\n if (coveredAmount.lte(0)) return null;\n\n const requestedRate =\n requestedUsd && requestedUsd.gt(0)\n ? requestedUsd.div(requestedAmount)\n : undefined;\n const producedRate =\n producedUsd && producedUsd.gt(0) && producedAmount && producedAmount.gt(0)\n ? producedUsd.div(producedAmount)\n : undefined;\n const fallbackRate = getTokenUsdRate(token);\n const usdRate =\n requestedRate && requestedRate.gt(0)\n ? requestedRate\n : producedRate && producedRate.gt(0)\n ? producedRate\n : fallbackRate.gt(0)\n ? fallbackRate\n : undefined;\n\n return {\n amount: coveredAmount,\n usd: usdRate ? coveredAmount.mul(usdRate) : undefined,\n };\n };\n\n const buildDestinationBalanceDisplayToken = (\n coverage: ReturnType,\n token?: SwapTokenOption\n ): SwapTokenOption | null => {\n if (!coverage || !token || coverage.amount.lte(0)) return null;\n\n const amount = coverage.amount\n .toDecimalPlaces(Math.max(0, token.decimals ?? 18), Decimal.ROUND_DOWN)\n .toFixed();\n const usd = coverage.usd?.toDecimalPlaces(6, Decimal.ROUND_DOWN).toFixed();\n const balanceUsd = coverage.usd\n ? `$${coverage.usd.toDecimalPlaces(2, Decimal.ROUND_DOWN).toFixed()}`\n : token.balanceInFiat || \"$0.00\";\n\n return {\n ...token,\n balance: `${amount} ${token.symbol}`,\n balanceInFiat: balanceUsd,\n userAmount: amount,\n userAmountMode: \"token\",\n userAmountUsd: usd,\n };\n };\n\n const cacheSymbolUsdRate = (symbol: string | undefined, rate: Decimal) => {\n const symbolKey = getSymbolUsdRateCacheKey(symbol);\n if (!symbolKey || rate.lte(0)) return;\n\n intentSymbolUsdRateCacheRef.current[symbolKey] = {\n amount: \"1\",\n rate: rate.toDecimalPlaces(18).toFixed(),\n updatedAt: Date.now(),\n value: rate.toFixed(),\n };\n };\n\n const getPredictiveDestinationKey = (token?: SwapTokenOption) => {\n const tokenKey = getTokenUsdRateCacheKey(token);\n return tokenKey ? `destination:${tokenKey}` : \"\";\n };\n\n const getPredictiveSourceKey = (token: SwapTokenOption) =>\n [\n token.chainId ?? \"unknown\",\n (token.contractAddress || zeroAddress).toLowerCase(),\n token.symbol.toUpperCase(),\n ].join(\":\");\n\n const getPredictiveQuoteCacheKey = (\n mode = activeMode,\n type = swapType,\n destination = toToken,\n sources = fromTokens\n ) => {\n const destinationKey = getPredictiveDestinationKey(destination);\n if (!destinationKey) return \"\";\n if (mode !== \"swap\" || type !== \"exactIn\") {\n return `exactOut:${destinationKey}`;\n }\n\n const sourceKey = getExpandedSourceTokens(sources)\n .map(getPredictiveSourceKey)\n .sort()\n .join(\"+\");\n return sourceKey ? `exactIn:${sourceKey}->${destinationKey}` : \"\";\n };\n\n const getPredictiveDisplayAmount = (\n amount: Decimal,\n token?: Pick\n ) => {\n const decimals = Math.min(\n PREDICTIVE_QUOTE_DISPLAY_DECIMALS,\n Math.max(0, token?.decimals ?? 18)\n );\n return amount.toDecimalPlaces(decimals, Decimal.ROUND_DOWN).toFixed();\n };\n\n const resolveUsdRateForToken = async (token?: SwapTokenOption) => {\n if (!token?.symbol) return new Decimal(0);\n\n const localRate = getTokenUsdRate(token);\n if (localRate.gt(0)) return localRate;\n\n const resolvedRate = await resolveUsdRateForSymbol(token.symbol);\n if (resolvedRate.gt(0)) {\n cacheSymbolUsdRate(token.symbol, resolvedRate);\n }\n return resolvedRate;\n };\n\n const getDestinationReceiveLimitUsd = (token?: SwapTokenOption) => {\n if (!token?.chainId) return undefined;\n const limit = DESTINATION_RECEIVE_LIMIT_USD_BY_CHAIN_ID[token.chainId];\n return limit ? new Decimal(limit) : undefined;\n };\n\n const getImmediateDestinationReceiveUsdRate = (token?: SwapTokenOption) => {\n const priceUsd = parseFiatNumber(token?.priceUSD);\n if (priceUsd && priceUsd.gt(0)) return priceUsd;\n\n const cachedRate = getCachedDestinationUsdRate(token);\n if (cachedRate && cachedRate.gt(0)) return cachedRate;\n\n if (!token) return undefined;\n const localRate = getTokenUsdRate(token);\n return localRate.gt(0) ? localRate : undefined;\n };\n\n const getExactInSourceUsdForReceiveLimit = (\n sourceTokens: SwapTokenOption[],\n inputAmount: string\n ) => {\n if (sourceTokens.length === 0) return undefined;\n let hasPositiveSourceAmount = false;\n let totalUsd = new Decimal(0);\n\n for (const token of sourceTokens) {\n const fallbackAmount =\n sourceTokens.length === 1 ? inputAmount : undefined;\n const sourceAmount = parseFiatNumber(token.userAmount || fallbackAmount);\n if (!sourceAmount || sourceAmount.lte(0)) continue;\n\n hasPositiveSourceAmount = true;\n const sourceUsd = getTokenUsdValue(token, fallbackAmount);\n if (sourceUsd.lte(0)) return undefined;\n totalUsd = totalUsd.plus(sourceUsd);\n }\n\n return hasPositiveSourceAmount ? totalUsd : undefined;\n };\n\n const buildReceiveAmountIssue = ({\n destinationRate,\n destinationToken = toToken,\n inputAmount = amount,\n mode = activeMode,\n sourceTokens = fromTokens,\n type = swapType,\n }: {\n destinationRate?: Decimal;\n destinationToken?: SwapTokenOption;\n inputAmount?: string;\n mode?: NexusWidgetMode;\n sourceTokens?: SwapTokenOption[];\n type?: SwapType;\n } = {}): ReceiveAmountIssue | null => {\n const limit = getDestinationReceiveLimitUsd(destinationToken);\n if (!limit || !destinationToken) return null;\n\n const parsedAmount = parseFiatNumber(inputAmount);\n if (!parsedAmount || parsedAmount.lte(0)) return null;\n if (mode === \"swap\" && type === \"exactIn\" && sourceTokens.length === 0) {\n return null;\n }\n\n const chainName = getShortChainName(\n destinationToken.chainId,\n destinationToken.chainName\n );\n const resolvedDestinationRate =\n destinationRate ??\n getImmediateDestinationReceiveUsdRate(destinationToken);\n\n if (!resolvedDestinationRate || resolvedDestinationRate.lte(0)) {\n return {\n ctaLabel: \"Price unavailable\",\n message: `Unable to price ${destinationToken.symbol} on ${chainName}. Select another receive token.`,\n type: \"unpricedReceiveToken\",\n };\n }\n\n let receiveUsd: Decimal | undefined;\n if (mode === \"swap\" && type === \"exactIn\") {\n receiveUsd = getExactInSourceUsdForReceiveLimit(\n sourceTokens,\n inputAmount\n );\n if (!receiveUsd || receiveUsd.lte(0)) {\n return {\n ctaLabel: \"Price unavailable\",\n message: `Unable to price selected assets for ${chainName}'s receive limit.`,\n type: \"unpricedReceiveToken\",\n };\n }\n } else if (mode === \"deposit\" && depositAmountMode === \"usd\") {\n receiveUsd = parsedAmount;\n } else {\n receiveUsd = parsedAmount.mul(resolvedDestinationRate);\n }\n\n if (receiveUsd.gt(limit)) {\n return {\n ctaLabel: \"Receive limit exceeded\",\n message: `Maximum receive amount on ${chainName} is ${formatUsdDisplay(limit)}.`,\n type: \"receiveLimitExceeded\",\n };\n }\n\n return null;\n };\n\n const buildConfiguredAmountIssue = (\n inputAmount = amount\n ): ReceiveAmountIssue | null => {\n if (!amountInputConfig || !inputAmount) return null;\n const parsedAmount = parseFiatNumber(inputAmount);\n if (!parsedAmount || parsedAmount.lte(0)) return null;\n\n const minAmount = parseFiatNumber(amountInputConfig.min);\n if (minAmount && parsedAmount.lt(minAmount)) {\n return {\n ctaLabel: \"Below minimum\",\n message: `Minimum amount is ${minAmount.toFixed()}.`,\n type: \"configuredAmountLimit\",\n };\n }\n\n const maxAmount = parseFiatNumber(amountInputConfig.max);\n if (maxAmount && parsedAmount.gt(maxAmount)) {\n return {\n ctaLabel: \"Above maximum\",\n message: `Maximum amount is ${maxAmount.toFixed()}.`,\n type: \"configuredAmountLimit\",\n };\n }\n\n return null;\n };\n\n const clearPreviewForBlockingAmountIssue = () => {\n clearPendingSwapIntent(true);\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setTxError(null);\n setSwapQuoteIssue(null);\n };\n\n const applyReceiveAmountIssue = (issue: ReceiveAmountIssue | null) => {\n const key = issue ? `${issue.type}:${issue.message}` : \"\";\n receiveAmountIssueRef.current = issue;\n if (receiveAmountIssueKeyRef.current !== key) {\n receiveAmountIssueKeyRef.current = key;\n setReceiveAmountIssue(issue);\n }\n if (!issue) return;\n\n clearPendingSwapIntent(true);\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setTxError(null);\n };\n\n const getPredictiveExactInSourceTokens = () => {\n const expanded = getExpandedSourceTokens(fromTokens);\n if (expanded.length === 0) return [];\n\n return expanded\n .map((token) => {\n const userAmount =\n token.userAmount ||\n (expanded.length === 1 && hasPositiveDecimalInput(amount)\n ? amount\n : \"\");\n return { ...token, userAmount };\n })\n .filter((token) => hasPositiveDecimalInput(token.userAmount));\n };\n\n const sortUnifiedSourceTokens = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const fiatDiff = getTokenBalanceUsd(b).cmp(getTokenBalanceUsd(a));\n if (fiatDiff !== 0) return fiatDiff;\n return getTokenBalanceAmount(b).cmp(getTokenBalanceAmount(a));\n });\n\n const allocateUnifiedExactInToken = (\n token: SwapTokenOption,\n fallbackAmount?: string\n ) => {\n if (!token.isUnified || !token.sourceTokens?.length) return [token];\n\n const rawAmount =\n parseFiatNumber(token.userAmount || fallbackAmount) ?? new Decimal(0);\n if (rawAmount.lte(0)) return [];\n\n const sortedSources = sortUnifiedSourceTokens(token.sourceTokens).filter(\n (source) =>\n source.chainId &&\n source.contractAddress &&\n getTokenBalanceAmount(source).gt(0) &&\n hasMinimumSourceUsdBalance(source)\n );\n const allocated: SwapTokenOption[] = [];\n\n if (token.userAmountMode === \"usd\") {\n let remainingUsd = rawAmount;\n\n for (const source of sortedSources) {\n if (remainingUsd.lte(0)) break;\n\n const availableUsd = getTokenBalanceUsd(source);\n if (availableUsd.lte(0)) continue;\n\n const targetUsd = Decimal.min(remainingUsd, availableUsd);\n const tokenAmount = getTokenAmountForUsd(\n source,\n targetUsd\n ).toDecimalPlaces(\n Math.max(0, source.decimals || 18),\n Decimal.ROUND_DOWN\n );\n if (tokenAmount.lte(0)) continue;\n\n const actualUsd = getUsdForTokenAmount(source, tokenAmount);\n allocated.push({\n ...source,\n userAmount: tokenAmount.toFixed(),\n userAmountMode: \"token\",\n userAmountUsd: actualUsd\n .toDecimalPlaces(6, Decimal.ROUND_DOWN)\n .toFixed(),\n });\n remainingUsd = remainingUsd.minus(targetUsd);\n }\n\n return allocated;\n }\n\n let remainingTokenAmount = rawAmount;\n\n for (const source of sortedSources) {\n if (remainingTokenAmount.lte(0)) break;\n\n const availableTokenAmount = getTokenBalanceAmount(source);\n if (availableTokenAmount.lte(0)) continue;\n\n const tokenAmount = Decimal.min(\n remainingTokenAmount,\n availableTokenAmount\n ).toDecimalPlaces(Math.max(0, source.decimals || 18), Decimal.ROUND_DOWN);\n if (tokenAmount.lte(0)) continue;\n\n const actualUsd = getUsdForTokenAmount(source, tokenAmount);\n allocated.push({\n ...source,\n userAmount: tokenAmount.toFixed(),\n userAmountMode: \"token\",\n userAmountUsd: actualUsd\n .toDecimalPlaces(6, Decimal.ROUND_DOWN)\n .toFixed(),\n });\n remainingTokenAmount = remainingTokenAmount.minus(tokenAmount);\n }\n\n return allocated;\n };\n\n const getExactInSourceTokens = (\n tokens: SwapTokenOption[],\n fallbackAmount?: string\n ) =>\n tokens\n .flatMap((token) =>\n token.isUnified\n ? allocateUnifiedExactInToken(token, fallbackAmount)\n : [token]\n )\n .filter(hasMinimumSourceUsdBalance);\n\n const hasPositiveDecimalInput = (value: unknown) =>\n Boolean(parseFiatNumber(value)?.gt(0));\n\n const hasPositiveSourceSpend = (token: SwapTokenOption) =>\n hasPositiveDecimalInput(token.userAmount) ||\n hasPositiveDecimalInput(token.userAmountUsd);\n\n const getReadyExactInSourceTokens = (tokens: SwapTokenOption[]) =>\n getExactInSourceTokens(tokens).filter(\n (token) =>\n Boolean(token.chainId && token.contractAddress) &&\n hasPositiveDecimalInput(token.userAmount)\n );\n\n const hasReadyExactInSwapInput = (\n tokens: SwapTokenOption[],\n destination?: SwapTokenOption\n ) =>\n Boolean(\n destination?.chainId &&\n destination.contractAddress &&\n getReadyExactInSourceTokens(tokens).length > 0\n );\n\n const getExpandedSourceTokens = (tokens: SwapTokenOption[]) => {\n const expanded = tokens.flatMap((token) =>\n token.isUnified && token.sourceTokens?.length\n ? token.sourceTokens\n : [token]\n );\n const seen = new Set();\n return expanded.filter((token) => {\n if (!token.chainId || !token.contractAddress) return false;\n if (\n !isSwapSupportedBySdkChainList(\n token.chainId,\n swapSupportedChainsAndTokens\n )\n ) {\n return false;\n }\n const key = `${token.chainId}-${token.contractAddress.toLowerCase()}`;\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n });\n };\n\n const getNativeGasBalanceForChain = (chainId: number) => {\n const nativeSymbol =\n CHAIN_METADATA[chainId]?.nativeCurrency?.symbol?.toUpperCase();\n let balance = new Decimal(0);\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== chainId) continue;\n const breakdownSymbol = (\n breakdown.symbol ??\n asset.symbol ??\n \"\"\n ).toUpperCase();\n const assetSymbol = (asset.symbol ?? \"\").toUpperCase();\n const isNativeBalance =\n isNativeTokenAddress(breakdown.contractAddress) ||\n Boolean(\n nativeSymbol &&\n (breakdownSymbol === nativeSymbol || assetSymbol === nativeSymbol)\n );\n\n if (!isNativeBalance) continue;\n balance = balance.plus(\n parseFiatNumber(breakdown.balance) ?? new Decimal(0)\n );\n }\n }\n\n return balance;\n };\n\n const hasGasForSource = (token: SwapTokenOption) => {\n if (!token.chainId || !token.contractAddress) return false;\n const tokenBalance = parseFiatNumber(token.balance) ?? new Decimal(0);\n if (tokenBalance.lte(0)) return false;\n if (isNativeTokenAddress(token.contractAddress)) return true;\n return getNativeGasBalanceForChain(token.chainId).gt(0);\n };\n\n const getGasCapableBalanceSourceTokens = () => {\n const tokens: SwapTokenOption[] = [];\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const chainId = breakdown.chain?.id;\n const contractAddress = breakdown.contractAddress;\n const balance = parseFiatNumber(breakdown.balance) ?? new Decimal(0);\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n if (\n !chainId ||\n !contractAddress ||\n balance.lte(0) ||\n !fiatBalance ||\n fiatBalance.lt(minimumSourceUsd)\n )\n continue;\n\n const chainMeta = CHAIN_METADATA[chainId];\n const symbol = breakdown.symbol ?? asset.symbol;\n tokens.push({\n chainId,\n chainLogo: chainMeta?.logo ?? breakdown.chain?.logo,\n chainName: getShortChainName(\n chainId,\n chainMeta?.name ?? breakdown.chain?.name\n ),\n contractAddress,\n decimals: breakdown.decimals ?? asset.decimals ?? 18,\n logo: asset.logo ?? \"\",\n name: symbol,\n symbol,\n balance: `${breakdown.balance} ${symbol}`,\n balanceInFiat:\n fiatBalance !== undefined\n ? `$${fiatBalance.toDecimalPlaces(2).toFixed()}`\n : \"$0.00\",\n });\n }\n }\n\n return getExpandedSourceTokens(tokens).filter(hasGasForSource);\n };\n\n const getMinimumBalanceSourceTokens = () =>\n filterMinimumSourceUsdTokens(\n getExpandedSourceTokens(\n swapBalance\n ? deriveTokenOptions(swapBalance, swapSupportedChainsAndTokens)\n : []\n )\n );\n const getHeldDestinationTokenOption = () => {\n if (!toToken?.chainId || !toToken.contractAddress) return undefined;\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const chainId = breakdown.chain?.id;\n if (chainId !== toToken.chainId) continue;\n\n const breakdownAddress = breakdown.contractAddress;\n const addressMatches =\n breakdownAddress &&\n (breakdownAddress.toLowerCase() ===\n toToken.contractAddress.toLowerCase() ||\n (isNativeTokenAddress(breakdownAddress) &&\n isNativeTokenAddress(toToken.contractAddress)));\n const symbolMatches =\n (breakdown.symbol ?? asset.symbol ?? \"\").toUpperCase() ===\n toToken.symbol.toUpperCase();\n\n if (!addressMatches && !symbolMatches) continue;\n\n const balanceAmount = parseFiatNumber(breakdown.balance);\n if (!balanceAmount || balanceAmount.lte(0)) continue;\n\n const chainMeta = CHAIN_METADATA[chainId];\n const symbol = breakdown.symbol ?? asset.symbol ?? toToken.symbol;\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n\n return {\n chainId,\n chainLogo:\n chainMeta?.logo ?? breakdown.chain?.logo ?? toToken.chainLogo,\n chainName: getShortChainName(\n chainId,\n chainMeta?.name ?? breakdown.chain?.name ?? toToken.chainName\n ),\n contractAddress: breakdown.contractAddress ?? toToken.contractAddress,\n decimals:\n breakdown.decimals ?? asset.decimals ?? toToken.decimals ?? 18,\n logo: asset.logo ?? toToken.logo,\n name: symbol,\n symbol,\n balance: `${breakdown.balance} ${symbol}`,\n balanceInFiat:\n fiatBalance !== undefined\n ? `$${fiatBalance.toDecimalPlaces(2).toFixed()}`\n : \"$0.00\",\n } satisfies SwapTokenOption;\n }\n }\n\n return undefined;\n };\n\n const getDepositDestinationForSourceSelection = () => {\n const destination =\n activeMode === \"deposit\" ? selectedOpportunity : toToken;\n const chainId = destination?.chainId;\n const tokenAddress =\n activeMode === \"deposit\"\n ? selectedOpportunity?.tokenAddress\n : toToken?.contractAddress;\n const tokenSymbol =\n activeMode === \"deposit\"\n ? selectedOpportunity?.tokenSymbol\n : toToken?.symbol;\n\n if (!chainId || !tokenAddress || !tokenSymbol) return undefined;\n\n return {\n chainId,\n tokenAddress: tokenAddress as `0x${string}`,\n tokenSymbol,\n };\n };\n const getDestinationSourceIdForDeposit = () => {\n const destination = getDepositDestinationForSourceSelection();\n return destination\n ? getDepositSourceId(destination.tokenAddress, destination.chainId)\n : undefined;\n };\n const getDepositSourceTargetUsd = () => {\n if (activeMode !== \"deposit\") return undefined;\n const requestedUsd = depositUsdDecimal;\n if (!requestedUsd || requestedUsd.lte(0)) return undefined;\n\n const coverage = getExactOutDestinationBalanceCoverage({\n requestedAmount: depositTokenAmountForQuote,\n requestedUsd,\n token: toToken,\n });\n return Decimal.max(\n requestedUsd.minus(coverage?.usd ?? new Decimal(0)),\n new Decimal(0)\n );\n };\n\n const getDepositSourceIdsFromTokens = (tokens: SwapTokenOption[]) =>\n getExpandedSourceTokens(tokens)\n .filter((token) => token.chainId && token.contractAddress)\n .map((token) =>\n getDepositSourceId(token.contractAddress, token.chainId!)\n );\n\n const getDepositTokenOptionsBySourceId = () => {\n const map = new Map();\n const sourceTokens = [\n ...(swapBalance\n ? deriveTokenOptions(swapBalance, swapSupportedChainsAndTokens)\n : []),\n ...fromTokens,\n ];\n\n for (const token of getExpandedSourceTokens(sourceTokens)) {\n if (!token.chainId || !token.contractAddress) continue;\n const id = getDepositSourceId(token.contractAddress, token.chainId);\n if (!map.has(id)) {\n map.set(id, {\n ...token,\n userAmount: \"\",\n });\n }\n }\n\n return map;\n };\n\n const getDepositSourceTokensForIds = (sourceIds: string[]) => {\n const tokenBySourceId = getDepositTokenOptionsBySourceId();\n return sourceIds\n .map((sourceId) => tokenBySourceId.get(sourceId))\n .filter((token): token is SwapTokenOption => Boolean(token))\n .map((token) => ({ ...token, userAmount: \"\" }));\n };\n\n const getResolvedDepositSourceSelection = (options?: {\n filter?: DepositSourceFilter;\n selectedTokens?: SwapTokenOption[];\n isManualSelection?: boolean;\n targetAmountUsd?: Decimal;\n }) => {\n const destination = getDepositDestinationForSourceSelection();\n if (!destination) {\n return { sourcePoolIds: [], selectedSourceIds: [], fromSources: [] };\n }\n\n const manualSelection =\n options?.isManualSelection ?? sourceSelectionTouched;\n const selectedTokensForResolution = options?.selectedTokens ?? fromTokens;\n const selectedSourceIds = getDepositSourceIdsFromTokens(\n selectedTokensForResolution\n );\n const destinationSourceId = getDestinationSourceIdForDeposit();\n const targetAmountUsd =\n options?.targetAmountUsd ??\n (activeMode === \"deposit\"\n ? getDepositSourceTargetUsd()\n : activeMode === \"send\"\n ? new Decimal(sendAmountUsd || 0)\n : undefined);\n\n return resolveDepositSourceSelection({\n swapBalance,\n destination,\n filter: manualSelection\n ? \"custom\"\n : (options?.filter ?? depositSourceFilter),\n selectedSourceIds,\n isManualSelection: manualSelection,\n minimumBalanceUsd: minimumSourceUsd.toNumber(),\n targetAmountUsd: targetAmountUsd?.toNumber(),\n excludedSourceIds: destinationSourceId ? [destinationSourceId] : [],\n });\n };\n\n const getExactOutSourceTokens = (\n mode: \"all\" | \"selected\" = exactOutQuoteSourceModeRef.current,\n targetAmountUsd?: Decimal\n ) => {\n if (activeMode === \"deposit\") {\n const selection = getResolvedDepositSourceSelection({ targetAmountUsd });\n return getDepositSourceTokensForIds(\n mode === \"all\" ? selection.sourcePoolIds : selection.selectedSourceIds\n );\n }\n\n if (activeMode === \"send\" && mode === \"selected\" && fromTokens.length > 0) {\n return filterMinimumSourceUsdTokens(\n getExpandedSourceTokens(fromTokens)\n ).filter(hasGasForSource);\n }\n\n return getGasCapableBalanceSourceTokens();\n };\n\n const buildExplicitSourcesPayload = (tokens: SwapTokenOption[]) => {\n const eligibleTokens = filterMinimumSourceUsdTokens(tokens).filter(\n (token) => token.chainId && token.contractAddress\n );\n return {\n sources: eligibleTokens.map((token) => ({\n chainId: token.chainId!,\n tokenAddress: token.contractAddress as `0x${string}`,\n })),\n };\n };\n\n const getSdkSourceKey = (source: {\n chainId: number;\n tokenAddress: `0x${string}`;\n }) => {\n const normalizedAddress = isNativeTokenAddress(source.tokenAddress)\n ? zeroAddress\n : source.tokenAddress.toLowerCase();\n return `${source.chainId}:${normalizedAddress}`;\n };\n\n const dedupeSdkSources = (\n sources: Array<\n | {\n chainId: number;\n tokenAddress: `0x${string}`;\n }\n | undefined\n >\n ) => {\n const seen = new Set();\n return sources.filter(\n (\n source\n ): source is {\n chainId: number;\n tokenAddress: `0x${string}`;\n } => {\n if (!source) return false;\n const key = getSdkSourceKey(source);\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n }\n );\n };\n\n const getHeldDestinationTokenSource = () => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n if (\n !heldDestinationToken?.chainId ||\n !heldDestinationToken.contractAddress\n ) {\n return undefined;\n }\n\n return {\n chainId: heldDestinationToken.chainId,\n tokenAddress: heldDestinationToken.contractAddress as `0x${string}`,\n };\n };\n\n const shouldSendExactOutSourceAllowlist = () => {\n if (activeMode === \"deposit\") {\n return sourceSelectionTouched || depositSourceFilter !== \"all\";\n }\n\n if (activeMode === \"send\") {\n return exactOutQuoteSourceModeRef.current === \"selected\";\n }\n\n return true;\n };\n\n const buildExactOutSourcesPayload = (tokens: SwapTokenOption[]) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n return buildExplicitSourcesPayload(tokens);\n }\n\n const sourceTokens = shouldSendExactOutSourceAllowlist()\n ? tokens\n : getExactOutSourceTokens(\"all\");\n const explicitSources = buildExplicitSourcesPayload(sourceTokens).sources;\n const sources = dedupeSdkSources([\n ...explicitSources,\n getHeldDestinationTokenSource(),\n ]);\n\n return sources.length > 0 ? { sources } : {};\n };\n\n const buildPredictiveExactOutSources = async (requiredSourceUsd: Decimal) => {\n if (requiredSourceUsd.lte(0)) return [];\n\n const destinationKey = getTokenSelectionKey(toToken);\n const candidates = sortExactOutSourcesBySdkPriority(\n getExactOutSourceTokens(\n exactOutQuoteSourceModeRef.current,\n requiredSourceUsd\n )\n .filter((token) => getTokenSelectionKey(token) !== destinationKey)\n .filter(hasMinimumSourceUsdBalance)\n );\n const sources: SwapTokenOption[] = [];\n let remainingUsd = requiredSourceUsd;\n\n for (const token of candidates) {\n if (remainingUsd.lte(0)) break;\n\n const availableUsd = getTokenBalanceUsd(token);\n if (availableUsd.lt(minimumSourceUsd)) continue;\n\n const rate = await resolveUsdRateForToken(token);\n if (rate.lte(0)) continue;\n\n const targetUsd = Decimal.min(remainingUsd, availableUsd);\n const tokenAmount = targetUsd\n .div(rate)\n .toDecimalPlaces(Math.max(0, token.decimals || 18), Decimal.ROUND_DOWN);\n if (tokenAmount.lte(0)) continue;\n\n sources.push({\n ...token,\n userAmount: tokenAmount.toFixed(),\n userAmountMode: \"token\",\n userAmountUsd: targetUsd\n .toDecimalPlaces(6, Decimal.ROUND_DOWN)\n .toFixed(),\n });\n remainingUsd = remainingUsd.minus(targetUsd);\n }\n\n return remainingUsd.gt(0.01) ? [] : sources;\n };\n\n const getErrorText = (error: unknown) => {\n const err = error as any;\n const parts = [\n err?.message,\n typeof error === \"string\" ? error : undefined,\n err?.code,\n ];\n\n try {\n if (err?.data) parts.push(JSON.stringify(err.data));\n } catch {\n // Ignore non-serializable SDK error metadata.\n }\n\n return parts.filter(Boolean).join(\" \");\n };\n\n const isInsufficientSourcesError = (error: unknown) => {\n const err = error as any;\n const message = getErrorText(error).toLowerCase();\n\n return (\n err?.code === ERROR_CODES.INSUFFICIENT_BALANCE ||\n message.includes(\"insufficient balance\") ||\n message.includes(\"sources are not enough\") ||\n (message.includes(\"source\") && message.includes(\"not enough\"))\n );\n };\n\n const isViemInvalidDecimalError = (error: unknown) =>\n getErrorText(error).toLowerCase().includes(\"not a valid decimal number\");\n\n const parseLabeledErrorDecimal = (text: string, label: string) => {\n const match = text.match(\n new RegExp(`${label}\\\\s*:\\\\s*\\\\$?\\\\s*([0-9][0-9,]*(?:\\\\.[0-9]+)?)`, \"i\")\n );\n return match ? parseFiatNumber(match[1]) : undefined;\n };\n\n const getExactOutRequestedUsd = () => {\n const amountNumber = parseFiatNumber(amount);\n if (!amountNumber || amountNumber.lte(0) || !toToken?.symbol) {\n return undefined;\n }\n\n if (activeMode === \"deposit\" && depositAmountMode === \"usd\") {\n return amountNumber;\n }\n\n const fiatValue = getFiatValue(amountNumber.toNumber(), toToken.symbol);\n return Number.isFinite(fiatValue) && fiatValue > 0\n ? new Decimal(fiatValue)\n : undefined;\n };\n\n const getExactOutAvailableSourceUsd = (\n sourceTokensOverride?: SwapTokenOption[]\n ): Decimal => {\n const sumTokensWithDestinationCredit = (\n tokens: SwapTokenOption[]\n ): Decimal => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n const heldDestinationKey = getTokenSelectionKey(heldDestinationToken);\n let hasHeldDestinationToken = false;\n\n const sourceTotal = tokens.reduce((sum, token) => {\n const value = parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n const isHeldDestinationToken =\n heldDestinationKey &&\n getTokenSelectionKey(token) === heldDestinationKey;\n if (isHeldDestinationToken) {\n hasHeldDestinationToken = true;\n }\n\n return value.gt(0) &&\n (isHeldDestinationToken || value.gte(minimumSourceUsd))\n ? sum.plus(value)\n : sum;\n }, new Decimal(0));\n\n if (!heldDestinationToken || hasHeldDestinationToken) {\n return sourceTotal;\n }\n\n const heldDestinationUsd =\n parseFiatNumber(heldDestinationToken.balanceInFiat) ?? new Decimal(0);\n return heldDestinationUsd.gt(0)\n ? sourceTotal.plus(heldDestinationUsd)\n : sourceTotal;\n };\n\n if (\n sourceTokensOverride ||\n exactOutQuoteSourceModeRef.current === \"selected\"\n ) {\n const selectedSourceTokens = sourceTokensOverride ?? fromTokens;\n return sumTokensWithDestinationCredit(selectedSourceTokens);\n }\n\n return getExactOutTotalSourceBalanceUsd();\n };\n const getExactOutTotalSourceBalanceUsd = (): Decimal => {\n const allSourceTotal: Decimal = getExactOutAvailableSourceUsd(\n getMinimumBalanceSourceTokens()\n );\n\n return allSourceTotal.gt(0) ? allSourceTotal : getSwapBalanceTotalUsd();\n };\n\n const getExactOutIntentSourceUsd = () => {\n const intentSourceUsd = (intentData?.sources ?? []).reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0)\n );\n return intentSourceUsd.gt(0) ? intentSourceUsd : undefined;\n };\n\n const getExactOutHeldDestinationUsd = () => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n const value = parseFiatNumber(heldDestinationToken?.balanceInFiat);\n return value && value.gt(0) ? value : new Decimal(0);\n };\n\n const exactOutIntentIncludesHeldDestination = () => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n const heldDestinationKey = getTokenSelectionKey(heldDestinationToken);\n if (!heldDestinationKey) return false;\n\n return (intentData?.sources ?? []).some((source) => {\n const chainId = source.chain?.id;\n const tokenAddress = source.token?.contractAddress;\n if (!chainId || !tokenAddress) return false;\n\n return (\n getTokenSelectionKey({\n chainId,\n contractAddress: tokenAddress,\n } as SwapTokenOption) === heldDestinationKey\n );\n });\n };\n\n const getExactOutRequiredSourceUsd = () => {\n const intentSourceUsd = getExactOutIntentSourceUsd();\n if (intentSourceUsd) return intentSourceUsd;\n return undefined;\n };\n\n const getExactOutRequiredFundingUsd = () => {\n const sourceRequiredUsd = getExactOutRequiredSourceUsd();\n if (sourceRequiredUsd) {\n const destinationCreditUsd = exactOutIntentIncludesHeldDestination()\n ? new Decimal(0)\n : getExactOutHeldDestinationUsd();\n return sourceRequiredUsd.plus(destinationCreditUsd);\n }\n\n return getExactOutRequestedUsd();\n };\n\n const getExactInSourceDeficitUsd = () => {\n if (swapType !== \"exactIn\" || fromTokens.length === 0) return undefined;\n\n return fromTokens.reduce((sum, token) => {\n const requestedAmount = parseFiatNumber(token.userAmount);\n if (!requestedAmount || requestedAmount.lte(0)) return sum;\n\n if (token.userAmountMode === \"usd\") {\n const availableUsd = parseFiatNumber(token.balanceInFiat);\n if (!availableUsd || requestedAmount.lte(availableUsd)) return sum;\n return sum.plus(requestedAmount.minus(availableUsd));\n }\n\n const availableTokenAmount = parseFiatNumber(token.balance);\n if (!availableTokenAmount || requestedAmount.lte(availableTokenAmount)) {\n return sum;\n }\n\n const missingTokenAmount = requestedAmount.minus(availableTokenAmount);\n const fiatBalance = parseFiatNumber(token.balanceInFiat);\n if (fiatBalance && availableTokenAmount.gt(0)) {\n return sum.plus(\n missingTokenAmount.mul(fiatBalance.div(availableTokenAmount))\n );\n }\n\n return sum;\n }, new Decimal(0));\n };\n\n const buildInsufficientSourcesIssue = (error: unknown): SwapQuoteIssue => {\n const errorText = getErrorText(error);\n const details =\n (error as any)?.data?.details ?? (error as any)?.details ?? {};\n const requiredFromError =\n parseFiatNumber(\n details.requiredUsd ??\n details.requiredUSD ??\n details.requiredAmountUsd ??\n details.requiredAmount ??\n details.required\n ) ?? parseLabeledErrorDecimal(errorText, \"required\");\n const availableFromError =\n parseFiatNumber(\n details.availableUsd ??\n details.availableUSD ??\n details.availableAmountUsd ??\n details.availableAmount ??\n details.available\n ) ?? parseLabeledErrorDecimal(errorText, \"available\");\n const requestedUsd = getExactOutRequiredFundingUsd();\n const availableUsd = getExactOutAvailableSourceUsd();\n const exactInSourceDeficitUsd = getExactInSourceDeficitUsd();\n\n let missingUsd =\n exactInSourceDeficitUsd && exactInSourceDeficitUsd.gt(0)\n ? exactInSourceDeficitUsd\n : requiredFromError && availableFromError\n ? requiredFromError.minus(availableFromError)\n : undefined;\n\n if (\n requestedUsd &&\n (!missingUsd || missingUsd.lte(0) || missingUsd.gt(requestedUsd.mul(5)))\n ) {\n missingUsd = requestedUsd.minus(availableUsd);\n }\n\n if (missingUsd && missingUsd.gt(0)) {\n const formattedMissing =\n missingUsd.gt(0) && missingUsd.lt(0.01)\n ? \"<$0.01\"\n : formatUsdDisplay(missingUsd);\n\n return {\n type: \"insufficientSources\",\n missingUsd: missingUsd.toDecimalPlaces(2).toFixed(),\n message: `Need ${formattedMissing} more across your assets`,\n };\n }\n\n return {\n type: \"insufficientSources\",\n message: \"Add more source balance across your assets\",\n };\n };\n\n const buildExactOutSourceBalanceIssue = (\n sourceTokensOverride?: SwapTokenOption[]\n ): SwapQuoteIssue | null => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return null;\n const requestedUsd = getExactOutRequiredFundingUsd();\n const availableUsd = getExactOutAvailableSourceUsd(sourceTokensOverride);\n if (!requestedUsd || requestedUsd.lte(0) || !availableUsd) return null;\n\n const missingUsd = requestedUsd.minus(availableUsd);\n if (missingUsd.lte(0.01)) return null;\n\n const formattedMissing =\n missingUsd.gt(0) && missingUsd.lt(0.01)\n ? \"<$0.01\"\n : formatUsdDisplay(missingUsd);\n\n return {\n type: \"insufficientSources\",\n missingUsd: missingUsd.toDecimalPlaces(2).toFixed(),\n message: `Need ${formattedMissing} more across your assets`,\n };\n };\n\n const isNativeTokenAddress = (address?: string) =>\n !address ||\n address.toLowerCase() === \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\" ||\n address.toLowerCase() === \"0x0000000000000000000000000000000000000000\";\n\n const formatReadableTokenAmount = (rawAmount: bigint, decimals: number) =>\n new Decimal(rawAmount.toString())\n .div(new Decimal(10).pow(decimals))\n .toFixed();\n\n const formatReadableTokenBalanceAmount = (\n rawAmount: bigint,\n decimals: number\n ) =>\n new Decimal(rawAmount.toString())\n .div(new Decimal(10).pow(decimals))\n .toDecimalPlaces(6)\n .toFixed();\n\n const trimDecimalString = (value: string) =>\n value.replace(/(\\.\\d*?)0+$/, \"$1\").replace(/\\.$/, \"\");\n\n const buildSourceTokenSnapshotMap = useCallback(\n (balances: UserAsset[] | null | undefined) => {\n const snapshots = new Map();\n\n for (const asset of balances ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const chainId = breakdown.chain?.id;\n const contractAddress = breakdown.contractAddress;\n const symbol = breakdown.symbol ?? asset.symbol;\n\n if (\n !chainId ||\n !contractAddress ||\n !symbol ||\n !isSwapSupportedBySdkChainList(\n chainId,\n swapSupportedChainsAndTokens\n )\n ) {\n continue;\n }\n\n const chainMeta = CHAIN_METADATA[chainId];\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n const snapshot: SwapTokenOption = {\n balance: `${breakdown.balance ?? \"0\"} ${symbol}`,\n balanceInFiat: fiatBalance\n ? formatUsdDisplay(fiatBalance)\n : \"$0.00\",\n chainId,\n chainLogo: chainMeta?.logo ?? breakdown.chain?.logo,\n chainName: getShortChainName(\n chainId,\n chainMeta?.name ?? breakdown.chain?.name\n ),\n contractAddress,\n decimals: breakdown.decimals ?? asset.decimals ?? 18,\n logo: asset.logo ?? \"\",\n name: symbol,\n symbol,\n };\n snapshots.set(getTokenSelectionKey(snapshot), snapshot);\n }\n }\n\n return snapshots;\n },\n [swapSupportedChainsAndTokens]\n );\n\n const patchSourceTokensWithBalances = useCallback(\n (tokens: SwapTokenOption[], balances: UserAsset[]) => {\n const snapshots = buildSourceTokenSnapshotMap(balances);\n\n const updateToken = (token: SwapTokenOption): SwapTokenOption => {\n const preservedAmounts = {\n userAmount: token.userAmount,\n userAmountMode: token.userAmountMode,\n userAmountUsd: token.userAmountUsd,\n };\n\n if (token.isUnified) {\n const sourceTokens = (token.sourceTokens ?? []).map(updateToken);\n const totalBalance = sourceTokens.reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber(source.balance) ?? new Decimal(0)),\n new Decimal(0)\n );\n const totalFiat = sourceTokens.reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber(source.balanceInFiat) ?? new Decimal(0)),\n new Decimal(0)\n );\n\n return {\n ...token,\n ...preservedAmounts,\n balance: totalBalance.toDecimalPlaces(8).toFixed(),\n balanceInFiat: formatUsdDisplay(totalFiat),\n sourceTokens,\n };\n }\n\n const snapshot = snapshots.get(getTokenSelectionKey(token));\n if (!snapshot) {\n return {\n ...token,\n ...preservedAmounts,\n balance: `0 ${token.symbol}`,\n balanceInFiat: \"$0.00\",\n chainLogo:\n token.chainLogo ??\n (token.chainId ? CHAIN_METADATA[token.chainId]?.logo : undefined),\n chainName: getShortChainName(token.chainId, token.chainName),\n };\n }\n\n return {\n ...token,\n ...snapshot,\n ...preservedAmounts,\n };\n };\n\n return tokens.map(updateToken);\n },\n [buildSourceTokenSnapshotMap]\n );\n\n const refreshSelectedSourceBalances = useCallback(async () => {\n const refreshedBalance = await fetchSwapBalance();\n const balances = refreshedBalance ?? swapBalance;\n if (!balances) return;\n\n setFromTokens((current) =>\n current.length === 0\n ? current\n : patchSourceTokensWithBalances(current, balances)\n );\n setSourceSelectionRevision((current) => current + 1);\n }, [fetchSwapBalance, patchSourceTokensWithBalances, swapBalance]);\n\n const receiveMaxSafetyMultiplier = new Decimal(\"0.9\");\n const currentSwapEntry =\n currentSwapId !== null\n ? swapHistory.find((entry) => entry.id === currentSwapId)\n : undefined;\n\n const scheduleTerminalBalanceRefresh = () => {\n if (terminalBalanceRefreshTimerRef.current) {\n clearTimeout(terminalBalanceRefreshTimerRef.current);\n }\n\n terminalBalanceRefreshTimerRef.current = setTimeout(() => {\n terminalBalanceRefreshTimerRef.current = null;\n void fetchSwapBalance();\n }, BALANCE_REFRESH_AFTER_TERMINAL_MS);\n };\n\n const patchSwapHistoryEntry = (\n id: string | null | undefined,\n patch: Partial\n ) => {\n if (!id) return;\n setSwapHistory((prev) =>\n sortSwapHistoryEntries(\n prev.map((entry) => (entry.id === id ? { ...entry, ...patch } : entry))\n )\n );\n };\n\n const patchCurrentSwapHistoryEntry = (patch: Partial) => {\n patchSwapHistoryEntry(currentSwapIdRef.current, patch);\n };\n\n const patchCurrentIntentExplorerUrl = (url?: string | null) => {\n if (!isHttpUrl(url)) return;\n if (intentUrlRef.current === url) return;\n\n intentUrlRef.current = url;\n const intentId = extractIntentIdFromUrl(url);\n patchCurrentSwapHistoryEntry({\n intentExplorerUrl: url,\n ...(intentId ? { intentId } : {}),\n });\n };\n\n const resetExplorerUrls = () => {\n const next = { sourceExplorerUrl: null, destinationExplorerUrl: null };\n explorerUrlsRef.current = next;\n setExplorerUrls(next);\n };\n\n const mergeExplorerUrls = (\n patch: Partial<{\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n }>\n ) => {\n const next = { ...explorerUrlsRef.current, ...patch };\n explorerUrlsRef.current = next;\n setExplorerUrls(next);\n patchCurrentSwapHistoryEntry({\n sourceExplorerUrl: next.sourceExplorerUrl,\n finalExplorerUrl: next.destinationExplorerUrl ?? next.sourceExplorerUrl,\n });\n };\n\n const resetProgressEvents = () => {\n progressEventsRef.current = [];\n setProgressEvents((current) => (current.length === 0 ? current : []));\n setFailedProgressStep((current) => (current === null ? current : null));\n };\n\n const appendProgressEvent = (\n name: string,\n step: SwapStepType | BridgeStepType | undefined,\n defaultCompleted: boolean,\n event?: unknown\n ) => {\n if (!step) return;\n const completed =\n typeof (step as any).completed === \"boolean\"\n ? Boolean((step as any).completed)\n : defaultCompleted;\n\n setProgressEvents((prev) => {\n const next = [\n ...prev,\n {\n id: `${Date.now()}-${prev.length}-${(step as any).typeID ?? (step as any).type ?? name}`,\n name,\n completed,\n event,\n step,\n },\n ];\n progressEventsRef.current = next;\n return next;\n });\n };\n\n const appendProgressListEvent = (\n name: string,\n stepList: Array\n ) => {\n if (stepList.length === 0) return;\n\n setProgressEvents((prev) => {\n const next = [\n ...prev,\n {\n id: `${Date.now()}-${prev.length}-${name}`,\n name,\n completed: false,\n step: stepList[0],\n steps: stepList,\n },\n ];\n progressEventsRef.current = next;\n return next;\n });\n };\n\n const startSwapHistoryEntry = () => {\n const id = `${Date.now()}-${swapRunIdRef.current}`;\n const now = Date.now();\n const resolvedToToken =\n toToken && destinationBalance\n ? { ...toToken, balance: destinationBalance }\n : toToken;\n const entry: SwapHistoryEntry = {\n id,\n mode: activeMode,\n status: \"pending\",\n createdAt: now,\n startedAt: now,\n intentData,\n fromTokens,\n toToken: resolvedToToken,\n requestedToAmount:\n activeMode === \"deposit\" || activeMode === \"send\"\n ? previewDestinationAmount\n : undefined,\n requestedToValue:\n activeMode === \"deposit\" || activeMode === \"send\"\n ? previewToAmountUsd\n : undefined,\n recipientAddress: transferRecipientAddress,\n opportunity: selectedOpportunity,\n feeUsd: intentFeeUsd,\n sourceExplorerUrl: null,\n finalExplorerUrl: null,\n intentExplorerUrl: isHttpUrl(intentUrlRef.current)\n ? intentUrlRef.current\n : null,\n intentId: extractIntentIdFromUrl(intentUrlRef.current),\n autoRefundAvailable: false,\n };\n\n currentSwapStartedAtRef.current = 0;\n currentSwapIdRef.current = id;\n setCurrentSwapId(id);\n setSwapHistory((prev) => sortSwapHistoryEntries([entry, ...prev]));\n return id;\n };\n\n const finishCurrentSwapHistoryEntry = (\n status: \"fulfilled\" | \"failed\" | \"timeout\",\n patch: Partial = {}\n ) => {\n const now = Date.now();\n const startedAt = currentSwapStartedAtRef.current || now;\n patchSwapHistoryEntry(currentSwapIdRef.current, {\n status,\n endedAt: now,\n durationSeconds: Math.max(1, Math.round((now - startedAt) / 1000)),\n sourceExplorerUrl: explorerUrlsRef.current.sourceExplorerUrl,\n finalExplorerUrl:\n explorerUrlsRef.current.destinationExplorerUrl ??\n explorerUrlsRef.current.sourceExplorerUrl,\n ...patch,\n });\n scheduleTerminalBalanceRefresh();\n };\n\n const markSwapExecutionStarted = () => {\n if (currentSwapStartedAtRef.current > 0) return;\n const now = Date.now();\n currentSwapStartedAtRef.current = now;\n patchCurrentSwapHistoryEntry({ startedAt: now });\n };\n\n const enterSkippedSwapProgress = () => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n\n const shouldInitializeProgress = swapStepRef.current !== \"progress\";\n if (!currentSwapIdRef.current) {\n onStart?.();\n startSwapHistoryEntry();\n }\n\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setPreviewQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setSwapQuoteIssue(null);\n\n if (shouldInitializeProgress) {\n resetProgressEvents();\n swapStepsListRef.current = [];\n resetSteps();\n swapStepRef.current = \"progress\";\n setSwapStep(\"progress\");\n }\n };\n\n const cachePredictiveBaselineFromIntent = (intent: SwapIntentData) => {\n const destinationAmount = parseFiatNumber(intent.destination?.amount);\n const destinationValue = parseFiatNumber(intent.destination?.value);\n const sourceUsd = (intent.sources ?? []).reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0)\n );\n\n if (!destinationAmount || destinationAmount.lte(0)) return;\n\n const destinationUsdRate =\n destinationValue && destinationValue.gt(0)\n ? destinationValue.div(destinationAmount)\n : getUsdRateForSymbol(intent.destination?.token?.symbol);\n if (destinationUsdRate.lte(0)) return;\n\n cacheSymbolUsdRate(intent.destination?.token?.symbol, destinationUsdRate);\n\n const key = getPredictiveQuoteCacheKey();\n if (!key) return;\n\n const baseline: PredictiveQuoteBaseline = {\n destinationUsdRate: destinationUsdRate.toDecimalPlaces(18).toFixed(),\n updatedAt: Date.now(),\n };\n\n if (activeMode === \"swap\" && swapType === \"exactIn\" && sourceUsd.gt(0)) {\n baseline.exactInDestinationAmountPerSourceUsd = destinationAmount\n .div(sourceUsd)\n .toDecimalPlaces(18)\n .toFixed();\n }\n\n const resolvedDestinationValue =\n destinationValue && destinationValue.gt(0)\n ? destinationValue\n : destinationAmount.mul(destinationUsdRate);\n if (\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n resolvedDestinationValue.gt(0) &&\n sourceUsd.gt(0)\n ) {\n baseline.exactOutSourceUsdPerDestinationUsd = sourceUsd\n .div(resolvedDestinationValue)\n .toDecimalPlaces(18)\n .toFixed();\n }\n\n predictiveQuoteCacheRef.current[key] = baseline;\n };\n\n const applySwapIntent = useCallback(\n (intent: SwapIntentData) => {\n const sortedIntent = {\n ...intent,\n sources: sortIntentSourcesByUsdDesc(intent.sources ?? []),\n };\n const enrichedIntent = enrichSwapIntentTokenMetadata(sortedIntent);\n const sortedIntentSourceTokens = sortSwapTokensByUsdDesc(\n (enrichedIntent.sources ?? []).map(buildIntentSourceToken)\n );\n\n lastSwapIntentRefreshAtRef.current = Date.now();\n lastIntentSourceTokensRef.current = sortedIntentSourceTokens;\n if (\n !sourceSelectionTouched &&\n (activeMode === \"send\" ||\n (activeMode === \"deposit\" && swapType === \"exactOut\"))\n ) {\n lastAutoIntentSourceTokensRef.current = sortedIntentSourceTokens;\n }\n cacheDestinationUsdRateFromIntent(enrichedIntent);\n cachePredictiveBaselineFromIntent(enrichedIntent);\n setIntentData(enrichedIntent);\n setIntentToAmount(enrichedIntent.destination?.amount || undefined);\n setSwapQuoteIssue(null);\n\n if (\n !sourceSelectionTouched &&\n swapStepRef.current !== \"choose-swap-asset\" &&\n (activeMode === \"send\" ||\n (activeMode === \"deposit\" && swapType === \"exactOut\"))\n ) {\n syncingIntentSourcesRef.current = true;\n setFromTokens(sortedIntentSourceTokens);\n }\n\n try {\n const bridgeFees = enrichedIntent.feesAndBuffer?.bridge;\n const bridgeFeeData =\n bridgeFees && typeof bridgeFees === \"object\" ? bridgeFees : undefined;\n const collectionFee = parseFiatNumber(bridgeFeeData?.collection);\n const fulfilmentFee = parseFiatNumber(bridgeFeeData?.fulfilment);\n const executionGasFee =\n parseFiatNumber(bridgeFeeData?.caGas) ??\n (collectionFee !== undefined || fulfilmentFee !== undefined\n ? (collectionFee ?? new Decimal(0)).plus(\n fulfilmentFee ?? new Decimal(0)\n )\n : undefined);\n const bridgeGasSuppliedFee = parseFiatNumber(\n bridgeFeeData?.gasSupplied\n );\n const destinationGasSuppliedFee =\n getIntentDestinationGasUsdValue(enrichedIntent);\n const gasSuppliedFee =\n bridgeGasSuppliedFee ?? destinationGasSuppliedFee;\n const bridgeComponentsTotal = bridgeFeeData\n ? [\n executionGasFee,\n parseFiatNumber(bridgeFeeData.protocol),\n parseFiatNumber(bridgeFeeData.solver),\n gasSuppliedFee,\n ].reduce(\n (sum, value) => sum.plus(value ?? new Decimal(0)),\n new Decimal(0)\n )\n : undefined;\n const rawBridgeTotal =\n typeof bridgeFees === \"string\"\n ? parseFiatNumber(bridgeFees)\n : parseFiatNumber(bridgeFeeData?.total);\n const bridgeTotal =\n rawBridgeTotal &&\n !bridgeGasSuppliedFee &&\n destinationGasSuppliedFee &&\n destinationGasSuppliedFee.gt(0)\n ? rawBridgeTotal.plus(destinationGasSuppliedFee)\n : (rawBridgeTotal ??\n (bridgeComponentsTotal && bridgeComponentsTotal.gt(0)\n ? bridgeComponentsTotal\n : destinationGasSuppliedFee));\n\n if (bridgeTotal !== undefined) {\n setIntentFeeUsd(\n bridgeTotal.gt(0) ? bridgeTotal.toDecimalPlaces(6).toFixed() : \"0\"\n );\n } else {\n setIntentFeeUsd(undefined);\n }\n } catch (err) {\n console.warn(\"Could not resolve bridge fee total\", err);\n setIntentFeeUsd(undefined);\n }\n },\n [\n activeMode,\n fromTokens,\n getFiatValue,\n sourceSelectionTouched,\n swapType,\n swapBalance,\n swapSupportedChainsAndTokens,\n supportedChainsAndTokens,\n toToken,\n ]\n );\n\n const handleSwapIntentCallback = useCallback(\n (data: any, runId: number, quoteInputKey: string) => {\n const { intent, allow, deny, refresh } = data;\n const bridgeProvider = normalizeBridgeProvider(\n data?.bridgeProvider ??\n intent?.bridgeProvider ??\n intent?.normalizedIntent?.bridgeProvider ??\n intent?.swap?.bridgeProvider\n );\n const intentWithBridgeProvider = normalizeRenderableSwapIntentData(\n intent,\n bridgeProvider\n );\n logSdkIntentEvent(\"onIntent\", data, {\n bridgeProvider,\n currentRunId: swapRunIdRef.current,\n normalizedIntent: intentWithBridgeProvider,\n isCurrentRun: swapRunIdRef.current === runId,\n quoteInputKey,\n runId,\n });\n if (swapRunIdRef.current !== runId) {\n logSdkIntentEvent(\"ignored stale onIntent\", data, {\n currentRunId: swapRunIdRef.current,\n quoteInputKey,\n runId,\n });\n deny();\n return;\n }\n if (quoteInputKey && activeQuoteInputKeyRef.current !== quoteInputKey) {\n logSdkIntentEvent(\"ignored stale quote input onIntent\", data, {\n currentQuoteInputKey: activeQuoteInputKeyRef.current,\n quoteInputKey,\n runId,\n });\n deny();\n return;\n }\n if (!intentWithBridgeProvider) {\n console.warn(\"[NexusWidget SDK][intent] Unsupported intent shape\", {\n intent,\n raw: data,\n });\n finishIntentFetchTiming(runId, \"failed\");\n deny();\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setTxError(\"Quote unavailable\");\n return;\n }\n const resolvedQuoteInputKey = quoteInputKey;\n const normalizedRefresh =\n typeof refresh === \"function\"\n ? async (...args: unknown[]) => {\n const refreshed = await refresh(...args);\n const refreshedBridgeProvider = normalizeBridgeProvider(\n refreshed?.bridgeProvider ??\n refreshed?.normalizedIntent?.bridgeProvider ??\n refreshed?.swap?.bridgeProvider ??\n bridgeProvider\n );\n return (\n normalizeRenderableSwapIntentData(\n refreshed,\n refreshedBridgeProvider\n ) ?? refreshed\n );\n }\n : refresh;\n finishIntentFetchTiming(runId);\n providerSwapIntent.current = {\n intent: intentWithBridgeProvider as any,\n allow,\n deny,\n refresh: normalizedRefresh,\n };\n swapIntentRef.current = {\n intent: intentWithBridgeProvider,\n allow,\n deny,\n refresh: normalizedRefresh,\n runId,\n quoteInputKey: resolvedQuoteInputKey,\n };\n flushSync(() => {\n applySwapIntent(intentWithBridgeProvider);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n });\n },\n [applySwapIntent, finishIntentFetchTiming, providerSwapIntent]\n );\n\n // Deposit-specific\n const [selectedOpportunity, setSelectedOpportunity] = useState<\n NexusWidgetDepositOpportunityConfig | undefined\n >(() => (activeMode === \"deposit\" ? configuredDeposit : undefined));\n const selectedOpportunityIdentity =\n getDepositConfigIdentity(selectedOpportunity);\n const [depositAmountMode, setDepositAmountMode] = useState<\"token\" | \"usd\">(\n \"token\"\n );\n const [depositSourceFilter, setDepositSourceFilter] =\n useState(\"all\");\n\n useEffect(() => {\n const immediateIssue = buildReceiveAmountIssue();\n applyReceiveAmountIssue(immediateIssue);\n\n if (!toToken || !getDestinationReceiveLimitUsd(toToken)) return;\n if (!parseFiatNumber(amount)?.gt(0)) return;\n if (getImmediateDestinationReceiveUsdRate(toToken)?.gt(0)) return;\n\n let cancelled = false;\n void resolveUsdRateForToken(toToken)\n .then((resolvedRate) => {\n if (cancelled) return;\n const issue = buildReceiveAmountIssue({\n destinationRate: resolvedRate.gt(0) ? resolvedRate : undefined,\n });\n applyReceiveAmountIssue(issue);\n })\n .catch(() => {\n if (!cancelled) {\n applyReceiveAmountIssue(buildReceiveAmountIssue());\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [\n activeMode,\n amount,\n depositAmountMode,\n fromTokens,\n swapType,\n toToken?.chainId,\n toToken?.chainName,\n toToken?.contractAddress,\n toToken?.priceUSD,\n toToken?.symbol,\n ]);\n\n const trackDeposit = useCallback(\n (event: string, props?: Record) => {\n const analytics = nexusSDK?.analytics;\n if (!analytics) return;\n analytics.track(event, {\n widgetSessionId: widgetSessionIdRef.current,\n widgetAttemptId: widgetAttemptIdRef.current,\n opportunityProtocol: selectedOpportunity?.protocol ?? null,\n destinationChainId: selectedOpportunity?.chainId ?? null,\n destinationToken: selectedOpportunity?.tokenSymbol ?? null,\n ...props,\n });\n },\n [nexusSDK, selectedOpportunity]\n );\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (!nexusSDK?.analytics) return;\n if (widgetOpenedFiredRef.current) return;\n widgetOpenedFiredRef.current = true;\n widgetOpenedTsRef.current = Date.now();\n rotateAttempt();\n trackDeposit(\"deposit_widget_opened\", {\n embed: Boolean(embed),\n depositConfigured: Boolean(configuredDeposit),\n prefillAmountPresent: Boolean(config.prefill?.amount),\n });\n }, [\n activeMode,\n nexusSDK,\n embed,\n configuredDeposit,\n config.prefill,\n rotateAttempt,\n trackDeposit,\n ]);\n\n useEffect(() => {\n analyticsRef.current = nexusSDK?.analytics ?? null;\n }, [nexusSDK]);\n\n useEffect(() => {\n selectedOpportunityRef.current = selectedOpportunity;\n }, [selectedOpportunity]);\n\n useEffect(() => {\n return () => {\n if (!widgetOpenedFiredRef.current) return;\n const analytics = analyticsRef.current;\n if (!analytics) return;\n const opp = selectedOpportunityRef.current;\n analytics.track(\"deposit_widget_closed\", {\n widgetSessionId: widgetSessionIdRef.current,\n widgetAttemptId: widgetAttemptIdRef.current,\n opportunityProtocol: opp?.protocol ?? null,\n lastStep: swapStepRef.current,\n reachedTerminal: reachedTerminalRef.current,\n hadSimulationSuccess: hadSimulationSuccessRef.current,\n hadPreviewViewed: hadPreviewViewedRef.current,\n timeInWidgetMs: Date.now() - widgetOpenedTsRef.current,\n });\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const toTokenFromOpportunity = (\n opp: NexusWidgetDepositOpportunityMetadata\n ): SwapTokenOption => {\n const citreaToken = findCitreaReceiveToken({\n address: opp.tokenAddress,\n chainId: opp.chainId,\n symbol: opp.tokenSymbol,\n });\n const chainTokens = supportedChainsAndTokens?.find(\n (chain) => chain.id === opp.chainId\n )?.tokens;\n const matchedToken = chainTokens?.find(\n (token) =>\n token.contractAddress.toLowerCase() ===\n opp.tokenAddress.toLowerCase() || token.symbol === opp.tokenSymbol\n );\n const tokenSymbol =\n citreaToken?.symbol ?? matchedToken?.symbol ?? opp.tokenSymbol;\n const tokenMeta =\n TOKEN_METADATA[tokenSymbol as keyof typeof TOKEN_METADATA];\n\n return {\n chainId: opp.chainId,\n contractAddress: citreaToken?.contractAddress ?? opp.tokenAddress,\n symbol: tokenSymbol,\n name: matchedToken?.name || citreaToken?.name || tokenSymbol,\n balance: \"0\",\n balanceInFiat: \"$0.00\",\n decimals:\n matchedToken?.decimals ??\n citreaToken?.decimals ??\n opp.tokenDecimals ??\n tokenMeta?.decimals ??\n 18,\n logo:\n opp.tokenLogo ||\n matchedToken?.logo ||\n citreaToken?.logo ||\n tokenMeta?.logo,\n chainName: getShortChainName(\n opp.chainId,\n CHAIN_METADATA[opp.chainId]?.name ?? citreaToken?.chainName\n ),\n chainLogo: CHAIN_METADATA[opp.chainId]?.logo ?? citreaToken?.chainLogo,\n };\n };\n\n const getDestinationBalanceFromSwapBalances = (token?: SwapTokenOption) => {\n if (!token?.chainId || !token.contractAddress) return null;\n\n const targetAddress = token.contractAddress.toLowerCase();\n const targetSymbol = token.symbol.toUpperCase();\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== token.chainId) continue;\n\n const breakdownAddress = breakdown.contractAddress?.toLowerCase();\n const addressMatches =\n (breakdownAddress && breakdownAddress === targetAddress) ||\n (isNativeTokenAddress(breakdownAddress) &&\n isNativeTokenAddress(targetAddress));\n const symbolMatches =\n (breakdown.symbol ?? asset.symbol ?? \"\").toUpperCase() ===\n targetSymbol;\n\n if (!addressMatches && !symbolMatches) continue;\n\n const balance = parseFiatNumber(breakdown.balance);\n if (!balance) return null;\n\n return `${balance.toDecimalPlaces(6).toFixed()} ${token.symbol}`;\n }\n }\n\n return null;\n };\n\n const resolvePrefillToken = useCallback(\n (pair?: RuntimeDestinationPair) => {\n if (!pair?.token || !pair.chain) return undefined;\n\n const normalizeAddress = (address?: string) => {\n if (!address) return \"\";\n return isNativeTokenAddress(address)\n ? zeroAddress\n : address.toLowerCase();\n };\n const targetAddress = normalizeAddress(pair.token);\n\n const balanceToken = deriveTokenOptions(\n swapBalance ?? [],\n swapSupportedChainsAndTokens\n ).find(\n (token) =>\n token.chainId === pair.chain &&\n normalizeAddress(token.contractAddress) === targetAddress\n );\n if (balanceToken) return balanceToken;\n\n const chain = supportedChainsAndTokens?.find(\n (item) => item.id === pair.chain\n );\n const matchedToken = chain?.tokens?.find(\n (token) => normalizeAddress(token.contractAddress) === targetAddress\n );\n const citreaToken = findCitreaReceiveToken({\n address: pair.token,\n chainId: pair.chain,\n });\n const tokenAddressSymbol = Object.entries(\n TOKEN_CONTRACT_ADDRESSES as Record>\n ).find(\n ([, addresses]) =>\n normalizeAddress(addresses[pair.chain]) === targetAddress\n )?.[0];\n const chainMeta = CHAIN_METADATA[pair.chain];\n const isNativePrefill = isNativeTokenAddress(pair.token);\n const tokenSymbol =\n matchedToken?.symbol ??\n citreaToken?.symbol ??\n pair.symbol ??\n tokenAddressSymbol ??\n (isNativePrefill ? chainMeta?.nativeCurrency?.symbol : undefined) ??\n \"Token\";\n const tokenMeta =\n TOKEN_METADATA[tokenSymbol as keyof typeof TOKEN_METADATA];\n\n if (\n !chain &&\n !matchedToken &&\n !citreaToken &&\n !tokenAddressSymbol &&\n !pair.symbol &&\n !isNativePrefill\n ) {\n return undefined;\n }\n\n return {\n chainId: pair.chain,\n contractAddress: citreaToken?.contractAddress ?? pair.token,\n symbol: tokenSymbol,\n name: matchedToken?.name || citreaToken?.name || tokenSymbol,\n balance: `0 ${tokenSymbol}`,\n balanceInFiat: \"$0.00\",\n decimals:\n matchedToken?.decimals ??\n citreaToken?.decimals ??\n pair.decimals ??\n tokenMeta?.decimals ??\n (isNativePrefill ? chainMeta?.nativeCurrency?.decimals : undefined) ??\n 18,\n logo:\n matchedToken?.logo ||\n citreaToken?.logo ||\n pair.logo ||\n tokenMeta?.logo,\n chainName: getShortChainName(\n pair.chain,\n chain?.name ?? chainMeta?.name ?? citreaToken?.chainName\n ),\n chainLogo: chainMeta?.logo ?? chain?.logo ?? citreaToken?.chainLogo,\n } satisfies SwapTokenOption;\n },\n [supportedChainsAndTokens, swapBalance]\n );\n\n useEffect(() => {\n if (activeMode !== \"swap\") return;\n\n const sourcePrefill = config.prefill?.source;\n const destinationPrefill = config.prefill?.destination;\n if (!sourcePrefill && !destinationPrefill) return;\n\n const prefillKey = [\n sourcePrefill\n ? `source:${sourcePrefill.chain}:${sourcePrefill.token.toLowerCase()}`\n : \"\",\n destinationPrefill\n ? `destination:${destinationPrefill.chain}:${destinationPrefill.token.toLowerCase()}`\n : \"\",\n config.prefill?.amount ? `amount:${config.prefill.amount}` : \"\",\n ].join(\"|\");\n\n if (appliedTokenPrefillRef.current === prefillKey) return;\n\n const sourceToken = resolvePrefillToken(sourcePrefill);\n const destinationToken = resolvePrefillToken(destinationPrefill);\n\n if (sourcePrefill && !sourceToken) return;\n if (destinationPrefill && !destinationToken) return;\n\n if (sourceToken) {\n setFromTokens((current) => {\n const nextSourceToken = {\n ...sourceToken,\n userAmount: config.prefill?.amount ?? \"\",\n };\n const currentSourceToken = current[0];\n if (\n current.length === 1 &&\n isSameTokenSelection(currentSourceToken, nextSourceToken) &&\n currentSourceToken.userAmount === nextSourceToken.userAmount\n ) {\n return current;\n }\n return [nextSourceToken];\n });\n setSourceSelectionTouched(true);\n }\n if (destinationToken) {\n setToToken((current) =>\n isSameTokenSelection(current, destinationToken)\n ? current\n : destinationToken\n );\n }\n setSwapType(\"exactIn\");\n appliedTokenPrefillRef.current = prefillKey;\n }, [\n activeMode,\n config.prefill?.amount,\n config.prefill?.destination?.chain,\n config.prefill?.destination?.decimals,\n config.prefill?.destination?.logo,\n config.prefill?.destination?.symbol,\n config.prefill?.destination?.token,\n config.prefill?.source?.chain,\n config.prefill?.source?.decimals,\n config.prefill?.source?.logo,\n config.prefill?.source?.symbol,\n config.prefill?.source?.token,\n resolvePrefillToken,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"send\") return;\n\n const sendPrefill =\n config.prefill?.token && config.prefill?.chain\n ? {\n token: config.prefill.token,\n chain: config.prefill.chain,\n }\n : config.prefill?.destination;\n if (!sendPrefill) return;\n\n const prefillKey = `send:${sendPrefill.chain}:${sendPrefill.token.toLowerCase()}`;\n if (appliedTokenPrefillRef.current === prefillKey) return;\n\n const token = resolvePrefillToken(sendPrefill);\n if (!token) return;\n\n setToToken(token);\n setSwapType(\"exactOut\");\n appliedTokenPrefillRef.current = prefillKey;\n }, [\n activeMode,\n config.prefill?.chain,\n config.prefill?.destination?.chain,\n config.prefill?.destination?.decimals,\n config.prefill?.destination?.logo,\n config.prefill?.destination?.symbol,\n config.prefill?.destination?.token,\n config.prefill?.token,\n resolvePrefillToken,\n ]);\n\n useEffect(() => {\n if (config.prefill?.amount) setAmount(config.prefill.amount);\n if (config.prefill?.recipient)\n setRecipientAddress(config.prefill.recipient);\n }, [config.prefill?.amount, config.prefill?.recipient]);\n\n useEffect(() => {\n setDestinationBalance(null);\n\n const balanceToken =\n toToken ??\n (activeMode === \"deposit\" && selectedOpportunity\n ? toTokenFromOpportunity(selectedOpportunity)\n : undefined);\n\n if (!balanceToken?.chainId || !ownerAddress) return;\n\n const swapBalanceValue =\n getDestinationBalanceFromSwapBalances(balanceToken);\n if (swapBalanceValue) {\n setDestinationBalance(swapBalanceValue);\n }\n }, [\n activeMode,\n ownerAddress,\n selectedOpportunity?.chainId,\n selectedOpportunity?.tokenAddress,\n selectedOpportunity?.tokenLogo,\n selectedOpportunity?.tokenSymbol,\n swapBalance,\n toToken?.chainId,\n toToken?.chainName,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || !configuredDeposit) return;\n setSelectedOpportunity((current) =>\n isSameDepositConfig(current, configuredDeposit)\n ? current\n : configuredDeposit\n );\n setSwapType(\"exactOut\");\n setToToken((current) => {\n const next = {\n ...toTokenFromOpportunity(configuredDeposit),\n balance: current?.balance ?? \"0\",\n balanceInFiat: current?.balanceInFiat ?? \"$0.00\",\n };\n if (\n current &&\n current.chainId === next.chainId &&\n current.contractAddress.toLowerCase() ===\n next.contractAddress.toLowerCase() &&\n current.symbol === next.symbol &&\n current.decimals === next.decimals &&\n current.logo === next.logo &&\n current.chainLogo === next.chainLogo &&\n current.chainName === next.chainName &&\n current.balance === next.balance &&\n current.balanceInFiat === next.balanceInFiat\n ) {\n return current;\n }\n return next;\n });\n }, [\n activeMode,\n configuredDepositIdentity,\n configuredDeposit?.chainId,\n configuredDeposit?.tokenAddress,\n configuredDeposit?.tokenLogo,\n configuredDeposit?.tokenSymbol,\n supportedChainsAndTokens,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || !selectedOpportunity) return;\n setToToken((current) => {\n const next = {\n ...toTokenFromOpportunity(selectedOpportunity),\n balance: current?.balance ?? \"0\",\n balanceInFiat: current?.balanceInFiat ?? \"$0.00\",\n };\n if (\n current &&\n current.chainId === next.chainId &&\n current.contractAddress.toLowerCase() ===\n next.contractAddress.toLowerCase() &&\n current.symbol === next.symbol &&\n current.decimals === next.decimals &&\n current.logo === next.logo &&\n current.chainLogo === next.chainLogo &&\n current.chainName === next.chainName &&\n current.balance === next.balance &&\n current.balanceInFiat === next.balanceInFiat\n ) {\n return current;\n }\n return next;\n });\n }, [activeMode, selectedOpportunity, supportedChainsAndTokens]);\n\n useEffect(() => {\n if (activeMode !== \"send\") return;\n setSwapType(\"exactOut\");\n }, [activeMode]);\n\n useEffect(() => {\n if (activeMode === \"swap\" && swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n }, [activeMode, swapType]);\n\n useEffect(() => {\n if (!toToken?.symbol) return;\n if (getFiatValue(1, toToken.symbol) > 0) return;\n\n let cancelled = false;\n void resolveTokenUsdRate(toToken.symbol).catch((error) => {\n if (!cancelled) {\n console.warn(\"Unable to resolve Nexus Widget token USD rate\", {\n symbol: toToken.symbol,\n error,\n });\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [activeMode, getFiatValue, resolveTokenUsdRate, toToken?.symbol]);\n\n // Balance helpers\n const activeBalanceArray = swapBalance;\n const selectedToken = config.prefill?.token ?? \"USDC\";\n const currentAsset =\n activeBalanceArray?.find((a) => a.symbol === selectedToken) ||\n activeBalanceArray?.[0];\n const maxBalance = currentAsset?.balance\n ? String(currentAsset.balance)\n : undefined;\n const usdValue = getFiatValue(\n Number(amount) || 0,\n currentAsset?.symbol || \"USDC\"\n );\n const getDepositTokenUsdRate = () => {\n if (selectedOpportunity?.tokenSymbol) {\n const fiat = getFiatValue(1, selectedOpportunity.tokenSymbol);\n if (Number.isFinite(fiat) && fiat > 0) {\n return new Decimal(fiat);\n }\n }\n\n if (toToken) {\n const tokenRate = getTokenUsdRate(toToken);\n if (tokenRate.gt(0)) return tokenRate;\n }\n\n return getCachedDestinationUsdRate(toToken) ?? new Decimal(0);\n };\n const getDepositTokenAmountForQuote = () => {\n const parsedAmount = parseFiatNumber(amount) ?? new Decimal(0);\n if (parsedAmount.lte(0)) return undefined;\n if (depositAmountMode === \"token\") return parsedAmount;\n\n const rate = getDepositTokenUsdRate();\n if (rate.lte(0)) return undefined;\n return parsedAmount.div(rate);\n };\n const depositTokenAmountForQuote = getDepositTokenAmountForQuote();\n const depositQuoteAmountKey = depositTokenAmountForQuote?.toFixed() ?? \"\";\n const depositUsdDecimal =\n depositAmountMode === \"usd\"\n ? (parseFiatNumber(amount) ?? new Decimal(0))\n : depositTokenAmountForQuote\n ? depositTokenAmountForQuote.mul(getDepositTokenUsdRate())\n : new Decimal(0);\n const depositUsdDisplay = depositUsdDecimal.toDecimalPlaces(2).toFixed();\n const depositTokenDisplay =\n depositTokenAmountForQuote\n ?.toDecimalPlaces(\n getCappedTokenDisplayDecimals(toToken?.decimals),\n Decimal.ROUND_DOWN\n )\n .toFixed() ?? \"0\";\n const isExactOutMode = activeMode === \"deposit\" || activeMode === \"send\";\n const getActiveTotalBalanceUsd = () =>\n isExactOutMode\n ? getExactOutTotalSourceBalanceUsd()\n : getSwapBalanceTotalUsd();\n const getTotalBalancePercentUsdAmount = (pct: number) => {\n const ratio =\n isExactOutMode && pct === 100\n ? receiveMaxSafetyMultiplier\n : new Decimal(pct).div(100);\n return getActiveTotalBalanceUsd().mul(ratio);\n };\n const getExactOutDestinationUsdRate = (token: SwapTokenOption) => {\n if (activeMode === \"deposit\") {\n return getDepositTokenUsdRate();\n }\n return getTokenUsdRate(token);\n };\n const getExactOutPercentAmountFromBalance = (\n token: SwapTokenOption,\n pct: number,\n preferUsd: boolean\n ) => {\n const usdAmount = getTotalBalancePercentUsdAmount(pct);\n if (usdAmount.lte(0)) return undefined;\n\n if (preferUsd) {\n return {\n amount: usdAmount.toDecimalPlaces(2, Decimal.ROUND_DOWN).toFixed(),\n mode: \"usd\" as const,\n };\n }\n\n const rate = getExactOutDestinationUsdRate(token);\n if (rate.lte(0)) return undefined;\n\n return {\n amount: usdAmount\n .div(rate)\n .toDecimalPlaces(\n getCappedTokenDisplayDecimals(token.decimals),\n Decimal.ROUND_DOWN\n )\n .toFixed(),\n mode: \"token\" as const,\n };\n };\n const depositSourceTargetUsdKey =\n activeMode === \"deposit\"\n ? (getDepositSourceTargetUsd()?.toFixed() ?? \"\")\n : \"\";\n const normalizedQuoteAmountKey = parseFiatNumber(amount)?.toFixed() ?? \"\";\n const quoteRecipientKey =\n activeMode === \"swap\"\n ? effectiveRecipientAddress\n : activeMode === \"send\"\n ? recipientAddress\n : \"\";\n const activeQuoteInputKey = [\n activeMode,\n swapType,\n normalizedQuoteAmountKey,\n toTokenQuoteKey,\n quoteRecipientKey.toLowerCase(),\n activeMode === \"swap\" ? fromTokensQuoteKey : \"\",\n activeMode === \"deposit\"\n ? [\n depositAmountMode,\n depositQuoteAmountKey,\n selectedOpportunityIdentity,\n depositSourceTargetUsdKey,\n depositSourceFilter,\n sourceSelectionTouched ? \"manual\" : \"auto\",\n sourceSelectionRevision,\n exactOutQuoteSourceMode,\n ].join(\":\")\n : \"\",\n activeMode === \"send\"\n ? [\n sourceSelectionTouched ? \"manual\" : \"auto\",\n sourceSelectionRevision,\n exactOutQuoteSourceMode,\n ].join(\":\")\n : \"\",\n ].join(\"|\");\n\n const exactOutAutoSourceInputKey = [\n activeMode,\n normalizedQuoteAmountKey,\n toTokenQuoteKey,\n quoteRecipientKey.toLowerCase(),\n activeMode === \"deposit\"\n ? [\n depositAmountMode,\n depositQuoteAmountKey,\n selectedOpportunityIdentity,\n depositSourceTargetUsdKey,\n ].join(\":\")\n : \"\",\n ].join(\"|\");\n\n activeQuoteInputKeyRef.current = activeQuoteInputKey;\n\n useEffect(() => {\n lastAutoIntentSourceTokensRef.current = [];\n }, [exactOutAutoSourceInputKey]);\n\n useEffect(() => {\n setTxError(null);\n }, [activeQuoteInputKey]);\n const hasCurrentQuoteIntent = Boolean(\n intentData &&\n swapIntentRef.current &&\n swapIntentRef.current.runId === swapRunIdRef.current &&\n swapIntentRef.current.quoteInputKey === activeQuoteInputKey\n );\n\n useEffect(() => {\n if (swapStep !== \"idle\") return;\n if (!hasCurrentQuoteIntent) return;\n if (sourceSelectionTouched) return;\n if (\n activeMode !== \"send\" &&\n !(activeMode === \"deposit\" && swapType === \"exactOut\")\n ) {\n return;\n }\n\n const nextTokens = lastIntentSourceTokensRef.current;\n if (nextTokens.length === 0) return;\n\n setFromTokens((current) => {\n if (\n getSourceTokenListSyncKey(current) ===\n getSourceTokenListSyncKey(nextTokens)\n ) {\n return current;\n }\n\n syncingIntentSourcesRef.current = true;\n return nextTokens;\n });\n }, [\n activeMode,\n hasCurrentQuoteIntent,\n sourceSelectionTouched,\n swapStep,\n swapType,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (!nexusSDK?.analytics) return;\n const parsed = parseFiatNumber(amount);\n if (!parsed || parsed.lte(0)) return;\n if (amount === amountEnteredLastValueRef.current) return;\n const timeout = setTimeout(() => {\n amountEnteredLastValueRef.current = amount;\n trackDeposit(\"deposit_amount_entered\", {\n amountToken: depositTokenDisplay,\n amountUsd: Number(depositUsdDisplay) || 0,\n inputMethod: lastInputMethodRef.current,\n });\n lastInputMethodRef.current = \"typed\";\n }, 500);\n return () => clearTimeout(timeout);\n }, [\n amount,\n activeMode,\n nexusSDK,\n depositTokenDisplay,\n depositUsdDisplay,\n trackDeposit,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (intentData) hadSimulationSuccessRef.current = true;\n }, [intentData, activeMode]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (sourceSelectionTouched) return;\n previousAutoSourceCountRef.current = (intentData?.sources ?? []).length;\n }, [intentData, activeMode, sourceSelectionTouched]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n const prev = prevSourceTouchedRef.current;\n const curr = sourceSelectionTouched;\n if (prev === curr) return;\n prevSourceTouchedRef.current = curr;\n if (!prev && curr) {\n trackDeposit(\"deposit_source_selection_changed\", {\n sourceCount: fromTokens.length,\n sourceChainIds: fromTokens.map((t) => t.chainId).filter(Boolean),\n sourceTokenSymbols: fromTokens.map((t) => t.symbol).filter(Boolean),\n previousSourceCount: previousAutoSourceCountRef.current,\n });\n } else if (prev && !curr) {\n trackDeposit(\"deposit_source_selection_reverted_to_auto\", {\n previousSourceCount: fromTokens.length,\n });\n }\n }, [sourceSelectionTouched, activeMode, fromTokens, trackDeposit]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (swapStep !== \"preview-intent\") return;\n if (intentLoading) return;\n if (!intentData) return;\n if (hadPreviewViewedRef.current) return;\n hadPreviewViewedRef.current = true;\n previewViewedTsRef.current = Date.now();\n trackDeposit(\"deposit_preview_viewed\", {\n totalFeeUsd: Number(intentFeeUsd) || 0,\n toAmountUsd: Number(depositUsdDisplay) || 0,\n sourceCount: (intentData?.sources ?? []).length,\n });\n }, [\n swapStep,\n intentLoading,\n intentData,\n activeMode,\n intentFeeUsd,\n depositUsdDisplay,\n trackDeposit,\n ]);\n const requiredDestinationTokenAmount =\n activeMode === \"deposit\"\n ? depositTokenAmountForQuote\n : activeMode === \"send\"\n ? parseFiatNumber(amount)\n : undefined;\n const canRefreshExactOutQuote = () =>\n activeMode === \"deposit\"\n ? Boolean(\n hasPositiveDecimalInput(amount) &&\n toToken &&\n selectedOpportunity &&\n depositTokenAmountForQuote &&\n depositTokenAmountForQuote.gt(0)\n )\n : activeMode === \"send\"\n ? Boolean(hasPositiveDecimalInput(amount) && toToken)\n : false;\n const invalidateExactOutQuoteForRefresh = (options?: {\n sourceTokens?: SwapTokenOption[];\n }) => {\n immediateQuoteAfterSourceEditRef.current = true;\n const receiveIssue = buildReceiveAmountIssue({\n sourceTokens: options?.sourceTokens,\n });\n applyReceiveAmountIssue(receiveIssue);\n const configuredIssue = buildConfiguredAmountIssue();\n if (configuredIssue) {\n clearPreviewForBlockingAmountIssue();\n }\n const sourceBalanceIssue = buildExactOutSourceBalanceIssue(\n options?.sourceTokens\n );\n if (sourceBalanceIssue) {\n clearPreviewForBlockingAmountIssue();\n setSwapQuoteIssue(sourceBalanceIssue);\n }\n const shouldLoadQuote = Boolean(\n !receiveIssue &&\n !configuredIssue &&\n !sourceBalanceIssue &&\n nexusSDK &&\n canRefreshExactOutQuote()\n );\n if (!receiveIssue && !configuredIssue && !sourceBalanceIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n setTxError(null);\n setSwapQuoteIssue(null);\n }\n return shouldLoadQuote;\n };\n\n useEffect(() => {\n if (\n activeMode !== \"swap\" ||\n swapStep !== \"idle\" ||\n swapType !== \"exactIn\"\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n\n const sources = getPredictiveExactInSourceTokens();\n const key = getPredictiveQuoteCacheKey();\n if (!toToken || sources.length === 0 || !key) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n\n const runId = ++predictiveQuoteRunRef.current;\n let cancelled = false;\n\n void (async () => {\n const baseline = predictiveQuoteCacheRef.current[key];\n const cachedDestinationRate = parseFiatNumber(\n baseline?.destinationUsdRate\n );\n const destinationRate =\n cachedDestinationRate && cachedDestinationRate.gt(0)\n ? cachedDestinationRate\n : await resolveUsdRateForToken(toToken);\n\n if (cancelled || runId !== predictiveQuoteRunRef.current) return;\n if (destinationRate.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n\n let sourceUsd = new Decimal(0);\n for (const source of sources) {\n const sourceAmount =\n parseFiatNumber(source.userAmount) ?? new Decimal(0);\n if (sourceAmount.lte(0)) continue;\n\n if (source.userAmountMode === \"usd\") {\n sourceUsd = sourceUsd.plus(sourceAmount);\n continue;\n }\n\n const sourceRate = await resolveUsdRateForToken(source);\n if (cancelled || runId !== predictiveQuoteRunRef.current) return;\n if (sourceRate.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n sourceUsd = sourceUsd.plus(sourceAmount.mul(sourceRate));\n }\n\n if (sourceUsd.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n\n const cachedAmountPerSourceUsd = parseFiatNumber(\n baseline?.exactInDestinationAmountPerSourceUsd\n );\n const predictedDestinationAmount =\n cachedAmountPerSourceUsd && cachedAmountPerSourceUsd.gt(0)\n ? sourceUsd.mul(cachedAmountPerSourceUsd)\n : sourceUsd\n .mul(BASIS_POINTS - PREDICTIVE_EXACT_IN_DISCOUNT_BPS)\n .div(BASIS_POINTS)\n .div(destinationRate);\n const predictedDestinationUsd =\n cachedAmountPerSourceUsd && cachedAmountPerSourceUsd.gt(0)\n ? predictedDestinationAmount.mul(destinationRate)\n : sourceUsd\n .mul(BASIS_POINTS - PREDICTIVE_EXACT_IN_DISCOUNT_BPS)\n .div(BASIS_POINTS);\n\n if (\n cancelled ||\n runId !== predictiveQuoteRunRef.current ||\n predictedDestinationAmount.lte(0)\n ) {\n return;\n }\n\n setPredictiveQuote({\n key,\n mode: \"exactIn\",\n toAmount: getPredictiveDisplayAmount(\n predictedDestinationAmount,\n toToken\n ),\n toUsd: predictedDestinationUsd.toDecimalPlaces(6).toFixed(),\n });\n })();\n\n return () => {\n cancelled = true;\n };\n }, [\n activeMode,\n amount,\n fromTokens,\n swapStep,\n swapType,\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n useEffect(() => {\n if (\n (activeMode !== \"deposit\" && activeMode !== \"send\") ||\n swapStep !== \"idle\" ||\n swapType !== \"exactOut\" ||\n !nexusSDK\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current\n );\n return;\n }\n\n const parsedAmount = parseFiatNumber(amount);\n const key = getPredictiveQuoteCacheKey();\n if (\n !toToken ||\n !parsedAmount ||\n parsedAmount.lte(0) ||\n !key ||\n (activeMode === \"deposit\" && !selectedOpportunity)\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current\n );\n return;\n }\n\n const runId = ++predictiveQuoteRunRef.current;\n let cancelled = false;\n\n void (async () => {\n const baseline = predictiveQuoteCacheRef.current[key];\n const cachedDestinationRate = parseFiatNumber(\n baseline?.destinationUsdRate\n );\n const destinationRate =\n cachedDestinationRate && cachedDestinationRate.gt(0)\n ? cachedDestinationRate\n : await resolveUsdRateForToken(toToken);\n\n if (cancelled || runId !== predictiveQuoteRunRef.current) return;\n if (destinationRate.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current\n );\n return;\n }\n\n const destinationAmount =\n activeMode === \"deposit\" && depositAmountMode === \"usd\"\n ? parsedAmount.div(destinationRate)\n : parsedAmount;\n const destinationUsd =\n activeMode === \"deposit\" && depositAmountMode === \"usd\"\n ? parsedAmount\n : destinationAmount.mul(destinationRate);\n const destinationCoverage = getExactOutDestinationBalanceCoverage({\n requestedAmount: destinationAmount,\n requestedUsd: destinationUsd,\n token: toToken,\n });\n const destinationUsdNeedingSources = Decimal.max(\n destinationUsd.minus(destinationCoverage?.usd ?? new Decimal(0)),\n new Decimal(0)\n );\n const cachedSourceUsdRatio = parseFiatNumber(\n baseline?.exactOutSourceUsdPerDestinationUsd\n );\n const requiredSourceUsd = getPredictiveExactOutSourceTargetUsd(\n destinationUsdNeedingSources,\n cachedSourceUsdRatio\n );\n const sources = requiredSourceUsd.gt(0)\n ? await buildPredictiveExactOutSources(requiredSourceUsd)\n : [];\n\n if (\n cancelled ||\n runId !== predictiveQuoteRunRef.current ||\n (requiredSourceUsd.gt(0) && sources.length === 0)\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current\n );\n return;\n }\n\n setPredictiveQuote({\n key,\n mode: \"exactOut\",\n sources,\n toAmount: getPredictiveDisplayAmount(destinationAmount, toToken),\n toUsd: destinationUsd.toDecimalPlaces(6).toFixed(),\n });\n })();\n\n return () => {\n cancelled = true;\n };\n }, [\n activeMode,\n amount,\n depositAmountMode,\n destinationBalance,\n fromTokensQuoteKey,\n nexusSDK,\n selectedOpportunityIdentity,\n sourceSelectionRevision,\n swapBalance,\n swapStep,\n swapType,\n toToken?.balance,\n toToken?.balanceInFiat,\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n const resolvedDepositSourceTokens = useMemo(() => {\n if (activeMode !== \"deposit\" || !swapBalance) return [];\n const selection = getResolvedDepositSourceSelection();\n return getDepositSourceTokensForIds(selection.selectedSourceIds);\n }, [\n activeMode,\n depositSourceFilter,\n depositQuoteAmountKey,\n depositSourceTargetUsdKey,\n depositUsdDecimal.toFixed(),\n fromTokensQuoteKey,\n selectedOpportunity?.chainId,\n selectedOpportunity?.tokenAddress,\n selectedOpportunity?.tokenSymbol,\n sourceSelectionRevision,\n sourceSelectionTouched,\n swapBalance,\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.symbol,\n ]);\n const lockedDestinationSourceTokens = useMemo(() => {\n if (\n (activeMode !== \"deposit\" && activeMode !== \"send\") ||\n !toToken?.chainId ||\n !requiredDestinationTokenAmount ||\n requiredDestinationTokenAmount.lte(0)\n ) {\n return [];\n }\n\n const heldDestinationToken = getHeldDestinationTokenOption();\n return heldDestinationToken ? [heldDestinationToken] : [];\n }, [\n activeMode,\n requiredDestinationTokenAmount?.toFixed(),\n swapBalance,\n toToken?.chainId,\n toToken?.chainLogo,\n toToken?.chainName,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.logo,\n toToken?.symbol,\n ]);\n\n const setSourcePickerDraftSelection = useCallback(\n (tokens: SwapTokenOption[]) => {\n const nextTokens = tokens.map((token) => ({\n ...token,\n userAmount: \"\",\n }));\n sourcePickerDraftTokensRef.current = nextTokens;\n setSourcePickerDraftTokens(nextTokens);\n },\n []\n );\n\n const getAutoExactOutSourceTokensForPicker = useCallback(() => {\n const autoIntentTokens = lastAutoIntentSourceTokensRef.current;\n if (autoIntentTokens.length > 0) {\n return autoIntentTokens;\n }\n\n return sourceSelectionTouched ? [] : fromTokens;\n }, [fromTokens, sourceSelectionTouched]);\n\n const resetSourcePickerDraft = useCallback(() => {\n sourcePickerDraftTokensRef.current = null;\n setSourcePickerDraftTokens(null);\n }, []);\n\n const beginSourcePickerEdit = useCallback(() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n resetSourcePickerDraft();\n return;\n }\n\n sourcePickerDraftDepositFilterRef.current = depositSourceFilter;\n sourcePickerDraftTouchedRef.current = sourceSelectionTouched;\n sourcePickerDraftModeRef.current = exactOutQuoteSourceModeRef.current;\n setSourcePickerDraftSelection(fromTokens);\n }, [\n activeMode,\n depositSourceFilter,\n fromTokens,\n resetSourcePickerDraft,\n setSourcePickerDraftSelection,\n sourceSelectionTouched,\n ]);\n\n const handleSourcePickerCancel = useCallback(() => {\n resetSourcePickerDraft();\n closeDrawerToIdle();\n }, [closeDrawerToIdle, resetSourcePickerDraft]);\n\n const handleSourcePickerDraftSelectionChange = useCallback(\n (tokens: SwapTokenOption[]) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n\n setSourcePickerDraftSelection(tokens);\n sourcePickerDraftTouchedRef.current = true;\n sourcePickerDraftModeRef.current = \"selected\";\n if (activeMode === \"deposit\") {\n sourcePickerDraftDepositFilterRef.current = \"custom\";\n }\n },\n [activeMode, setSourcePickerDraftSelection]\n );\n\n const handleSourcePickerFilterTabSelect = useCallback(\n (tab: Exclude) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n\n const nextFilter: DepositSourceFilter =\n tab === \"stables\" ? \"stablecoins\" : tab;\n\n if (tab === \"all\") {\n sourcePickerDraftDepositFilterRef.current = nextFilter;\n sourcePickerDraftTouchedRef.current = false;\n sourcePickerDraftModeRef.current = \"all\";\n setSourcePickerDraftSelection(getAutoExactOutSourceTokensForPicker());\n return;\n }\n\n if (activeMode !== \"deposit\") return;\n\n const selection = getResolvedDepositSourceSelection({\n filter: nextFilter,\n isManualSelection: false,\n });\n const sourcePoolTokens = getDepositSourceTokensForIds(\n selection.sourcePoolIds\n );\n\n sourcePickerDraftDepositFilterRef.current = nextFilter;\n sourcePickerDraftTouchedRef.current = false;\n sourcePickerDraftModeRef.current = \"all\";\n setSourcePickerDraftSelection(sourcePoolTokens);\n },\n [\n activeMode,\n getAutoExactOutSourceTokensForPicker,\n getDepositSourceTokensForIds,\n getResolvedDepositSourceSelection,\n setSourcePickerDraftSelection,\n ]\n );\n\n const commitSourcePickerDraft = useCallback(\n (tokens?: SwapTokenOption[]) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n resetSourcePickerDraft();\n closeDrawerToIdle();\n return;\n }\n\n const nextTokens =\n tokens ?? sourcePickerDraftTokensRef.current ?? fromTokens;\n const normalizedTokens = nextTokens.map((token) => ({\n ...token,\n userAmount: \"\",\n }));\n\n setSourceSelectionTouched(sourcePickerDraftTouchedRef.current);\n setExactOutQuoteSourceModeValue(sourcePickerDraftModeRef.current);\n if (activeMode === \"deposit\") {\n setDepositSourceFilter(sourcePickerDraftDepositFilterRef.current);\n }\n invalidateExactOutQuoteForRefresh({\n sourceTokens: normalizedTokens,\n });\n setSourceSelectionRevision((current) => current + 1);\n setFromTokens(normalizedTokens);\n resetSourcePickerDraft();\n closeDrawerToIdle();\n },\n [\n activeMode,\n closeDrawerToIdle,\n fromTokens,\n invalidateExactOutQuoteForRefresh,\n resetSourcePickerDraft,\n setExactOutQuoteSourceModeValue,\n ]\n );\n\n const sourcePickerSelectedTokens =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n swapStep === \"choose-swap-asset\"\n ? (sourcePickerDraftTokens ?? fromTokens)\n : fromTokens;\n\n useEffect(() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n if (lockedDestinationSourceTokens.length === 0) return;\n if (activeMode === \"deposit\" && !sourceSelectionTouched) return;\n\n setFromTokens((current) => {\n const missing = lockedDestinationSourceTokens.filter(\n (locked) =>\n !current.some(\n (token) =>\n getTokenSelectionKey(token) === getTokenSelectionKey(locked)\n )\n );\n if (missing.length === 0) return current;\n return [\n ...current,\n ...missing.map((token) => ({ ...token, userAmount: \"\" })),\n ];\n });\n }, [activeMode, lockedDestinationSourceTokens, sourceSelectionTouched]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (sourceSelectionTouched) return;\n if (\n !toToken ||\n !depositTokenAmountForQuote ||\n depositTokenAmountForQuote.lte(0)\n ) {\n return;\n }\n if (resolvedDepositSourceTokens.length === 0) {\n return;\n }\n\n setFromTokens((current) => {\n const canInitialize = current.length === 0;\n if (!canInitialize) return current;\n\n const next: SwapTokenOption[] = [];\n const seen = new Set();\n for (const token of resolvedDepositSourceTokens) {\n const key = getTokenSelectionKey(token);\n if (!key || seen.has(key)) continue;\n seen.add(key);\n next.push({ ...token, userAmount: \"\" });\n }\n\n const currentKeys = current.map(getTokenSelectionKey).sort().join(\"|\");\n const nextKeys = next.map(getTokenSelectionKey).sort().join(\"|\");\n if (currentKeys === nextKeys) return current;\n return next;\n });\n }, [\n activeMode,\n depositQuoteAmountKey,\n resolvedDepositSourceTokens,\n sourceSelectionTouched,\n toTokenQuoteKey,\n ]);\n\n // ---------------------------------------------------------------------------\n // Handlers\n // ---------------------------------------------------------------------------\n\n const applyConfiguredPrefillsAfterReset = () => {\n const prefillAmount = config.prefill?.amount ?? \"\";\n const prefillRecipient = config.prefill?.recipient ?? \"\";\n\n appliedTokenPrefillRef.current = null;\n syncingIntentSourcesRef.current = false;\n maxPercentRunRef.current += 1;\n setAmount(prefillAmount);\n setRecipientAddress(prefillRecipient);\n setTxError(null);\n setSwapQuoteIssue(null);\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n setPredictiveQuote(null);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setMaxCalculationPercent(null);\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setExactOutQuoteSourceModeValue(\"all\");\n setSourceSelectionRevision((current) => current + 1);\n setDepositAmountMode(\"token\");\n\n if (activeMode === \"swap\") {\n const sourcePrefill = config.prefill?.source;\n const destinationPrefill = config.prefill?.destination;\n const sourceToken = resolvePrefillToken(sourcePrefill);\n const destinationToken = resolvePrefillToken(destinationPrefill);\n\n setFromTokens(\n sourceToken\n ? [\n {\n ...sourceToken,\n userAmount: prefillAmount,\n },\n ]\n : []\n );\n setSourceSelectionTouched(Boolean(sourceToken));\n setToToken(destinationToken);\n setSelectedOpportunity(undefined);\n setSwapType(\"exactIn\");\n\n if (\n (!sourcePrefill || sourceToken) &&\n (!destinationPrefill || destinationToken) &&\n (sourcePrefill || destinationPrefill)\n ) {\n appliedTokenPrefillRef.current = [\n sourcePrefill\n ? `source:${sourcePrefill.chain}:${sourcePrefill.token.toLowerCase()}`\n : \"\",\n destinationPrefill\n ? `destination:${destinationPrefill.chain}:${destinationPrefill.token.toLowerCase()}`\n : \"\",\n config.prefill?.amount ? `amount:${config.prefill.amount}` : \"\",\n ].join(\"|\");\n }\n return;\n }\n\n if (activeMode === \"send\") {\n const sendPrefill =\n config.prefill?.token && config.prefill?.chain\n ? {\n token: config.prefill.token,\n chain: config.prefill.chain,\n }\n : config.prefill?.destination;\n const token = resolvePrefillToken(sendPrefill);\n\n setFromTokens([]);\n setSelectedOpportunity(undefined);\n setToToken(token);\n setSwapType(\"exactOut\");\n\n if (sendPrefill && token) {\n appliedTokenPrefillRef.current = `send:${sendPrefill.chain}:${sendPrefill.token.toLowerCase()}`;\n }\n return;\n }\n\n const destinationPrefill = config.prefill?.destination;\n const configuredPrefillDeposit = destinationPrefill\n ? configuredDepositOptions.find(\n (deposit) =>\n deposit.chainId === destinationPrefill.chain &&\n deposit.tokenAddress.toLowerCase() ===\n destinationPrefill.token.toLowerCase()\n )\n : undefined;\n const nextDeposit = configuredPrefillDeposit ?? configuredDeposit;\n\n setFromTokens([]);\n setSelectedOpportunity(nextDeposit);\n setToToken(nextDeposit ? toTokenFromOpportunity(nextDeposit) : undefined);\n setSwapType(\"exactOut\");\n };\n\n const handleReset = () => {\n clearPendingSwapIntent();\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n amountEnteredLastValueRef.current = config.prefill?.amount ?? \"\";\n applyConfiguredPrefillsAfterReset();\n rotateAttempt();\n };\n\n const resetSourceSelectionAfterFailure = () => {\n lastAutoIntentSourceTokensRef.current = [];\n lastIntentSourceTokensRef.current = [];\n resetSourcePickerDraft();\n resetExactOutSourcesToAuto();\n };\n\n const handleFailureBack = () => {\n clearPendingSwapIntent();\n resetSourceSelectionAfterFailure();\n setTxError(null);\n void refreshSelectedSourceBalances();\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n rotateAttempt();\n };\n\n const clearTerminalInputState = () => {\n setAmount(\"\");\n setRecipientAddress(\"\");\n setTxError(null);\n setSwapQuoteIssue(null);\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n setFromTokens((current) => (current.length === 0 ? current : []));\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setDepositAmountMode(\"token\");\n };\n\n const resetInputsAfterSuccessfulExecution = () => {\n clearTerminalInputState();\n if (activeMode === \"deposit\") {\n setSelectedOpportunity(configuredDeposit);\n setToToken(\n configuredDeposit\n ? toTokenFromOpportunity(configuredDeposit)\n : undefined\n );\n } else {\n setToToken(undefined);\n }\n };\n\n const handleTerminalReceiptDone = () => {\n clearPendingSwapIntent();\n clearTerminalInputState();\n void refreshSelectedSourceBalances();\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n amountEnteredLastValueRef.current = \"\";\n rotateAttempt();\n };\n\n const handleModalOpenChange = useCallback(\n (open: boolean) => {\n if (!open && swapStepRef.current === \"progress\") return;\n if (!isControlledOpen) {\n setInternalOpen(open);\n }\n onOpenChange?.(open);\n if (!open) {\n clearPendingSwapIntent();\n onClose?.();\n }\n },\n [clearPendingSwapIntent, isControlledOpen, onClose, onOpenChange]\n );\n\n const handleClose = () => {\n if (!embed) {\n handleModalOpenChange(false);\n return;\n }\n clearPendingSwapIntent();\n onClose?.();\n };\n\n const handleConnectWallet = async () => {\n if (walletActionPending || nexusLoading) return;\n\n const clickHandler = onConnectClick || onConnectWallet;\n if (clickHandler) {\n setWalletActionPending(true);\n setTxError(null);\n try {\n await clickHandler();\n } catch (error: any) {\n setTxError(error?.message || \"Unable to connect wallet.\");\n } finally {\n setWalletActionPending(false);\n }\n return;\n }\n\n if (isWalletConnectPending) return;\n\n setWalletActionPending(true);\n setTxError(null);\n try {\n let activeConnector = connector;\n\n if (walletStatus !== \"connected\") {\n const nextConnector = connectors[0];\n if (!nextConnector) {\n throw new Error(\"No wallet connector available.\");\n }\n await connectAsync({ connector: nextConnector });\n return;\n }\n\n const effectiveProvider = await getEffectiveWalletProvider(\n activeConnector\n );\n\n if (\n !effectiveProvider ||\n typeof effectiveProvider.request !== \"function\"\n ) {\n throw new Error(\"Wallet provider is not ready yet.\");\n }\n\n await handleInit(effectiveProvider as EthereumProvider, ownerAddress);\n } catch (error: any) {\n setTxError(error?.message || \"Unable to connect wallet.\");\n } finally {\n setWalletActionPending(false);\n }\n };\n\n const handleOpenRecipientEditor = () => {\n if (activeMode === \"swap\" && !recipientAddress && defaultRecipientAddress) {\n setRecipientAddress(defaultRecipientAddress);\n }\n setTxError(null);\n openDrawerStep(\"enter-recipient\");\n };\n\n const handleResetRecipientToDefault = () => {\n setRecipientAddress(defaultRecipientAddress);\n setTxError(null);\n };\n\n const handleSaveRecipient = () => {\n const next = recipientAddress.trim();\n if (!next) {\n setTxError(\"Recipient address is required\");\n return;\n }\n if (!next.endsWith(\".eth\") && !isAddress(next)) {\n setTxError(\"Incorrect address\");\n return;\n }\n if (\n activeMode === \"send\" &&\n ownerAddress &&\n isAddress(next) &&\n next.toLowerCase() === ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\n setRecipientAddress(next);\n setTxError(null);\n closeDrawerToIdle();\n };\n\n /** Start swap flow — v2 SDK per-operation onIntent hooks populate preview. */\n const handleEnterPreview = async (options: { background?: boolean } = {}) => {\n const { background = false } = options;\n const isExactOutFlow = activeMode === \"deposit\" || activeMode === \"send\";\n const quoteInputKey = activeQuoteInputKeyRef.current;\n const isCurrentQuoteInput = () =>\n activeQuoteInputKeyRef.current === quoteInputKey;\n\n if (!toToken) {\n return;\n }\n\n if (isExactOutFlow) {\n if (!hasPositiveDecimalInput(amount)) {\n return;\n }\n } else if (!hasReadyExactInSwapInput(fromTokens, toToken)) {\n if (!background) {\n setTxError(null);\n setSwapQuoteIssue(null);\n }\n return;\n }\n\n const receiveIssue = buildReceiveAmountIssue();\n if (receiveIssue) {\n applyReceiveAmountIssue(receiveIssue);\n if (!background && swapStepRef.current !== \"idle\") {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n }\n return;\n }\n\n const configuredIssue = buildConfiguredAmountIssue();\n if (configuredIssue) {\n clearPreviewForBlockingAmountIssue();\n if (!background && swapStepRef.current !== \"idle\") {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n }\n return;\n }\n\n const sourceBalanceIssue = isExactOutFlow\n ? buildExactOutSourceBalanceIssue()\n : null;\n if (sourceBalanceIssue) {\n clearPreviewForBlockingAmountIssue();\n setSwapQuoteIssue(sourceBalanceIssue);\n if (!background && swapStepRef.current !== \"idle\") {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n }\n return;\n }\n\n if (!background && activeMode === \"deposit\") {\n trackDeposit(\"deposit_confirm_clicked\", {\n amountToken: depositTokenDisplay,\n amountUsd: Number(depositUsdDisplay) || 0,\n selectionMode: sourceSelectionTouched ? \"manual\" : \"auto\",\n sourceCount: (intentData?.sources ?? []).length,\n });\n }\n\n setTxError(null);\n setSwapQuoteIssue(null);\n\n if (\n !background &&\n swapIntentRef.current?.runId === swapRunIdRef.current &&\n swapIntentRef.current?.quoteInputKey === quoteInputKey &&\n intentData &&\n (activeMode !== \"send\" || Boolean(recipientAddress)) &&\n ((activeMode !== \"deposit\" && activeMode !== \"send\") ||\n (intentData.sources ?? []).length > 0 ||\n Boolean(intentData.destination))\n ) {\n swapStepRef.current = \"preview-intent\";\n setSwapStep(\"preview-intent\");\n return;\n }\n\n let resolvedRecipientAddress =\n activeMode === \"swap\" ? effectiveRecipientAddress : recipientAddress;\n\n if (!background && activeMode === \"send\" && !resolvedRecipientAddress) {\n setTxError(\"Recipient address is required\");\n return;\n }\n\n if ((!background && activeMode === \"send\") || hasCustomSwapRecipient) {\n if (!resolvedRecipientAddress) {\n setTxError(\"Recipient address is required\");\n return;\n }\n\n if (\n activeMode === \"send\" &&\n ownerAddress &&\n isAddress(resolvedRecipientAddress) &&\n resolvedRecipientAddress.toLowerCase() === ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\n\n if (resolvedRecipientAddress.endsWith(\".eth\")) {\n try {\n const mainnetClient =\n publicClient?.chain?.id === 1\n ? publicClient\n : createPublicClient({\n chain: mainnet,\n transport: http(),\n });\n const ensAddr = await mainnetClient.getEnsAddress({\n name: normalize(resolvedRecipientAddress),\n });\n if (!ensAddr) {\n setTxError(\"Could not resolve ENS name to an address.\");\n return;\n }\n resolvedRecipientAddress = ensAddr;\n } catch (e: any) {\n setTxError(e.message || \"Failed to resolve ENS name.\");\n return;\n }\n } else {\n if (!isAddress(resolvedRecipientAddress)) {\n setTxError(\"Invalid recipient address.\");\n return;\n }\n }\n\n if (\n activeMode === \"send\" &&\n ownerAddress &&\n isAddress(resolvedRecipientAddress) &&\n resolvedRecipientAddress.toLowerCase() === ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\n }\n\n if (!isCurrentQuoteInput()) {\n return;\n }\n\n if (!background) {\n swapStepRef.current = \"preview-intent\";\n setSwapStep(\"preview-intent\");\n }\n setIntentLoading(true);\n setQuoteRefreshing(background);\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n swapIntentRef.current?.deny();\n swapIntentRef.current = null;\n if (!background) {\n resetProgressEvents();\n swapStepsListRef.current = [];\n resetSteps();\n }\n\n if (!nexusSDK) {\n setTxError(\"SDK not initialized\");\n if (!background) {\n setSwapStep(\"idle\");\n }\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n swapRunIdRef.current += 1;\n const runId = swapRunIdRef.current;\n\n const isActionPlanStep = (step: SwapStepType | BridgeStepType) => {\n const type = getProgressStepType(step);\n return (\n type === \"APPROVAL\" ||\n type === \"TRANSACTION_SENT\" ||\n type === \"TRANSACTION_CONFIRMED\"\n );\n };\n\n const hasSwapPlanSteps = (stepList: Array) =>\n stepList.some((step) => !isActionPlanStep(step));\n\n const handleProgressStepSideEffects = (\n event: any,\n step: SwapStepType | BridgeStepType,\n completed: boolean\n ) => {\n const type = getProgressStepType(step);\n const rawStepType = String(\n event?.stepType ?? (step as any)?.type ?? (step as any)?.typeID ?? \"\"\n ).toLowerCase();\n const rawState = String(event?.state ?? \"\").toLowerCase();\n const explorerUrl = getPlanStepExplorerUrl(event, step);\n const sourceMovementStep = isSourceMovementProgressStep(type, rawStepType);\n const hasSourceMovementTransaction = Boolean(\n explorerUrl || getPlanStepTransactionHash(event, step)\n );\n const intentExplorerUrl = getEventIntentExplorerUrl(\n appConfig.nexusNetwork,\n event,\n step\n );\n\n patchCurrentIntentExplorerUrl(intentExplorerUrl);\n\n if (\n type === \"TRANSACTION_SENT\" ||\n type === \"TRANSACTION_CONFIRMED\" ||\n type === \"SOURCE_SWAP\" ||\n type === \"BRIDGE_DEPOSIT\" ||\n type === \"BRIDGE_INTENT_SUBMISSION\" ||\n type === \"BRIDGE_FILL\" ||\n type === \"DESTINATION_SWAP\" ||\n type === \"SWAP_COMPLETE\" ||\n type === \"SWAP_SKIPPED\"\n ) {\n markSwapExecutionStarted();\n }\n\n if (\n sourceMovementStep &&\n hasSourceMovementTransaction &&\n PLAN_STEP_FUNDS_MAY_HAVE_MOVED_STATES.has(rawState)\n ) {\n fundsMovedRef.current = true;\n }\n\n if (explorerUrl) {\n if (\n rawStepType === \"destination_swap\" ||\n rawStepType === \"execute_transaction\" ||\n type.includes(\"DESTINATION_SWAP\") ||\n type === \"TRANSACTION_SENT\" ||\n type === \"TRANSACTION_CONFIRMED\"\n ) {\n mergeExplorerUrls({ destinationExplorerUrl: explorerUrl });\n } else if (\n sourceMovementStep\n ) {\n mergeExplorerUrls({ sourceExplorerUrl: explorerUrl });\n }\n\n if (\n !intentUrlRef.current &&\n (rawStepType === \"bridge_intent_submission\" ||\n rawStepType === \"request_submission\" ||\n type === \"BRIDGE_INTENT_SUBMISSION\")\n ) {\n patchCurrentIntentExplorerUrl(explorerUrl);\n }\n }\n\n if (completed) {\n onStepComplete(step as SwapStepType);\n }\n };\n\n const handlePlanEvent = (event: any) => {\n if (event.type === \"plan_preview\" || event.type === \"plan_confirmed\") {\n const stepList = Array.isArray(event.plan?.steps)\n ? event.plan.steps.map((step: any) =>\n normalizePlanStep(step, step?.type, undefined, false)\n )\n : [];\n logSwapPlanSteps(event.type, stepList, event.plan?.steps);\n if (stepList.length === 0) return;\n\n if (hasSwapPlanSteps(stepList)) {\n swapStepsListRef.current = stepList as SwapStepType[];\n appendProgressListEvent(\n PROGRESS_EVENT_NAMES.SWAP_PLAN_LIST,\n stepList\n );\n } else {\n appendProgressListEvent(\n PROGRESS_EVENT_NAMES.BRIDGE_PLAN_LIST,\n stepList\n );\n }\n onStepsList(stepList as SwapStepType[]);\n return;\n }\n\n if (event.type !== \"plan_progress\") {\n logSdkSwapEvent(\"unhandled typed event\", event);\n return;\n }\n\n const state = String(event.state ?? \"\").toLowerCase();\n const completed = PLAN_FINAL_STATES.has(state);\n const step = normalizePlanStep(\n event.step,\n event.stepType,\n event.state,\n completed\n );\n const eventName = isActionPlanStep(step)\n ? PROGRESS_EVENT_NAMES.BRIDGE_PLAN_PROGRESS\n : PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS;\n\n logSwapPlanProgress(event, step, eventName, completed);\n appendProgressEvent(eventName, step, completed, event);\n handleProgressStepSideEffects(event, step, completed);\n };\n\n const appendSkippedSwapProgress = () => {\n const step = {\n completed: true,\n type: \"SWAP_SKIPPED\",\n typeID: \"SWAP_SKIPPED\",\n } as SwapStepType;\n enterSkippedSwapProgress();\n appendProgressEvent(PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS, step, true);\n onStepComplete(step);\n };\n\n const handleSwapEvent = (event: any) => {\n if (!event || typeof event !== \"object\") return;\n if (typeof event.type === \"string\") {\n handlePlanEvent(event);\n return;\n }\n logSdkSwapEvent(\"ignored event without string type\", event);\n };\n\n const onEvent = (event: any) => {\n const isCurrentRun = swapRunIdRef.current === runId;\n const isCurrentQuote = isCurrentQuoteInput();\n logSdkSwapEvent(\"onEvent\", event, {\n currentRunId: swapRunIdRef.current,\n isCurrentQuote,\n isCurrentRun,\n quoteInputKey,\n runId,\n });\n if (!isCurrentRun || !isCurrentQuote) {\n logSdkSwapEvent(\"ignored stale onEvent\", event, {\n currentRunId: swapRunIdRef.current,\n isCurrentQuote,\n quoteInputKey,\n runId,\n });\n return;\n }\n patchCurrentIntentExplorerUrl(\n getEventIntentExplorerUrl(appConfig.nexusNetwork, event)\n );\n handleSwapEvent(event);\n };\n\n const buildRecipientTransferExecuteConfig = (transferAmount: bigint) => {\n if (!resolvedRecipientAddress) {\n throw new Error(\"Recipient address is required\");\n }\n\n const isNative =\n !toToken.contractAddress ||\n toToken.contractAddress.toLowerCase() ===\n \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\" ||\n toToken.contractAddress ===\n \"0x0000000000000000000000000000000000000000\";\n\n if (isNative) {\n return {\n to: resolvedRecipientAddress as `0x${string}`,\n value: transferAmount,\n gas: BigInt(100000),\n };\n }\n\n return {\n to: toToken.contractAddress as `0x${string}`,\n data: encodeFunctionData({\n abi: erc20Abi,\n functionName: \"transfer\",\n args: [resolvedRecipientAddress as `0x${string}`, transferAmount],\n }),\n gas: BigInt(100000),\n };\n };\n\n const executeRecipientTransfer = async (transferAmount: bigint) => {\n const result = await nexusSDK.execute(\n {\n toChainId: toToken.chainId!,\n ...buildRecipientTransferExecuteConfig(transferAmount),\n },\n { onEvent }\n );\n const finalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result\n );\n if (finalExplorerUrl) {\n setTransferExplorerUrl(finalExplorerUrl);\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n return finalExplorerUrl;\n };\n\n try {\n if (!isExactOutFlow) {\n const fromPayload: {\n chainId: number;\n tokenAddress: `0x${string}`;\n amountRaw: bigint;\n }[] = [];\n\n const exactInSourceTokens = getReadyExactInSourceTokens(fromTokens);\n\n for (const token of exactInSourceTokens) {\n // Determine the amount to use for this specific token\n let rawAmountStr = token.userAmount;\n if (!rawAmountStr && exactInSourceTokens.length === 1) {\n rawAmountStr = amount; // fallback for single-token case\n }\n\n let cleanAmount = parseFiatNumber(rawAmountStr) ?? new Decimal(0);\n if (cleanAmount.lte(0)) continue;\n\n if (token.userAmountMode === \"usd\") {\n const tokenBalance =\n parseFiatNumber(token.balance) ?? new Decimal(0);\n const fiatBalance =\n parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n const price = tokenBalance.gt(0)\n ? fiatBalance.div(tokenBalance)\n : new Decimal(0);\n if (price.gt(0)) {\n cleanAmount = cleanAmount.div(price);\n } else {\n cleanAmount = new Decimal(0);\n }\n }\n\n if (cleanAmount.lte(0)) continue;\n\n const safeTokenAmountStr = toViemDecimalString(\n cleanAmount,\n token.decimals || 18\n );\n\n fromPayload.push({\n chainId: token.chainId!,\n tokenAddress: token.contractAddress as `0x${string}`,\n amountRaw: parseUnits(safeTokenAmountStr, token.decimals || 18),\n });\n }\n\n if (fromPayload.length === 0) {\n throw new Error(\"No source amount available for swap.\");\n }\n\n resetExplorerUrls();\n const exactInSwapPayload = {\n sources: fromPayload,\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n };\n let intentExplorerUrl: string | null = null;\n let intentId = currentSwapEntry?.intentId;\n let finalExplorerUrl: string | null =\n explorerUrlsRef.current.destinationExplorerUrl ||\n explorerUrlsRef.current.sourceExplorerUrl;\n\n if (hasCustomSwapRecipient && resolvedRecipientAddress) {\n const sdkWithOptionalTransfer = nexusSDK as any;\n\n if (typeof sdkWithOptionalTransfer.swapAndTransfer === \"function\") {\n const swapAndTransferExactInInput = {\n mode: \"exactIn\",\n recipient: resolvedRecipientAddress as `0x${string}`,\n ...exactInSwapPayload,\n };\n logSdkIntentInput(\"swapAndTransfer exactIn\", swapAndTransferExactInInput, {\n activeMode,\n quoteInputKey,\n runId,\n });\n const result = await sdkWithOptionalTransfer.swapAndTransfer(\n swapAndTransferExactInInput,\n { onEvent }\n );\n if (result?.success === false) {\n throw new Error(result?.error || \"Swap and transfer failed\");\n }\n\n const swapResult = getSdkSwapResult(result);\n intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result,\n swapResult\n );\n intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const resultFinalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result,\n swapResult\n );\n finalExplorerUrl = resultFinalExplorerUrl || finalExplorerUrl;\n if (resultFinalExplorerUrl) {\n setTransferExplorerUrl(resultFinalExplorerUrl);\n mergeExplorerUrls({\n destinationExplorerUrl: resultFinalExplorerUrl,\n });\n }\n } else {\n logSdkIntentInput(\"swapWithExactIn\", exactInSwapPayload, {\n activeMode,\n quoteInputKey,\n runId,\n });\n startIntentFetchTiming({\n background,\n operation: \"swapWithExactIn\",\n quoteInputKey,\n runId,\n });\n const result = await nexusSDK.swapWithExactIn(exactInSwapPayload, {\n hooks: {\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n },\n onEvent,\n });\n\n intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result\n );\n intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n\n const latestSwapIntent = (\n swapIntentRef.current as unknown as {\n intent?: SwapIntentData;\n } | null\n )?.intent;\n const transferAmount = latestSwapIntent?.destination?.amount;\n if (!transferAmount) {\n throw new Error(\n \"Unable to determine received amount to transfer.\"\n );\n }\n\n const transferAmountBigInt = parseUnits(\n toViemDecimalString(transferAmount, toToken.decimals || 18),\n toToken.decimals || 18\n );\n finalExplorerUrl =\n (await executeRecipientTransfer(transferAmountBigInt)) ||\n finalExplorerUrl;\n }\n } else {\n // Start exact-in swap — the intent hook will fire and populate preview\n logSdkIntentInput(\"swapWithExactIn\", exactInSwapPayload, {\n activeMode,\n quoteInputKey,\n runId,\n });\n startIntentFetchTiming({\n background,\n operation: \"swapWithExactIn\",\n quoteInputKey,\n runId,\n });\n const result = await nexusSDK.swapWithExactIn(exactInSwapPayload, {\n hooks: {\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n },\n onEvent,\n });\n intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result\n );\n intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const swapResult = getSdkSwapResult(result);\n const resultFinalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result,\n swapResult\n );\n finalExplorerUrl = resultFinalExplorerUrl || finalExplorerUrl;\n if (resultFinalExplorerUrl) {\n setTransferExplorerUrl(resultFinalExplorerUrl);\n mergeExplorerUrls({\n destinationExplorerUrl: resultFinalExplorerUrl,\n });\n }\n }\n\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n const resolvedFinalExplorerUrl =\n finalExplorerUrl ||\n explorerUrlsRef.current.destinationExplorerUrl ||\n explorerUrlsRef.current.sourceExplorerUrl;\n const resolvedIntentExplorerUrl =\n intentExplorerUrl || intentUrlRef.current;\n finishCurrentSwapHistoryEntry(\"fulfilled\", {\n finalExplorerUrl: resolvedFinalExplorerUrl,\n ...(resolvedIntentExplorerUrl\n ? { intentExplorerUrl: resolvedIntentExplorerUrl }\n : {}),\n ...(intentId ? { intentId } : {}),\n });\n resetInputsAfterSuccessfulExecution();\n onComplete?.();\n setSwapStep(\"success\");\n }\n } else {\n const exactOutAmountString =\n activeMode === \"deposit\"\n ? depositTokenAmountForQuote\n ?.toDecimalPlaces(toToken.decimals || 18, Decimal.ROUND_DOWN)\n .toFixed()\n : amount;\n if (!exactOutAmountString || new Decimal(exactOutAmountString).lte(0)) {\n setTxError(\n depositAmountMode === \"usd\"\n ? \"Unable to convert USD amount into the destination token amount.\"\n : \"Enter a valid amount.\"\n );\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n const amountBigInt = parseUnits(\n toViemDecimalString(exactOutAmountString, toToken.decimals || 18),\n toToken.decimals || 18\n );\n\n resetExplorerUrls();\n\n const fromSourcesPayload = buildExactOutSourcesPayload(\n getExactOutSourceTokens()\n );\n\n let executeConfig: any;\n if (activeMode === \"deposit\" && !selectedOpportunity?.executeDeposit) {\n throw new Error(\"Deposit config is missing executeDeposit.\");\n }\n\n if (activeMode === \"deposit\" && selectedOpportunity) {\n const user = (ownerAddress ?? connectedAddress) as `0x${string}`;\n\n const executeParams = selectedOpportunity.executeDeposit(\n selectedOpportunity.tokenSymbol,\n selectedOpportunity.tokenAddress,\n amountBigInt,\n selectedOpportunity.chainId,\n user\n );\n if (!isPositiveGasLimit(executeParams.gas)) {\n throw new Error(\n \"Deposit config executeDeposit must return a positive gas limit.\"\n );\n }\n executeConfig = {\n to: executeParams.to,\n value: executeParams.value,\n data: executeParams.data,\n tokenApproval: executeParams.tokenApproval,\n gas: executeParams.gas,\n };\n } else if (\n (activeMode === \"send\" || hasCustomSwapRecipient) &&\n resolvedRecipientAddress\n ) {\n executeConfig = buildRecipientTransferExecuteConfig(amountBigInt);\n }\n\n if (executeConfig?.tokenApproval) {\n executeConfig = {\n ...executeConfig,\n tokenApproval: {\n toTokenAddress:\n executeConfig.tokenApproval.toTokenAddress ||\n toToken.contractAddress,\n amount: executeConfig.tokenApproval.amount,\n spender: executeConfig.tokenApproval.spender,\n },\n };\n }\n\n if (executeConfig) {\n const sdkWithOptionalTransfer = nexusSDK as any;\n const isTransferExactOut =\n (activeMode === \"send\" || hasCustomSwapRecipient) &&\n typeof sdkWithOptionalTransfer.swapAndTransfer === \"function\";\n const exactOutOperationInput = isTransferExactOut\n ? {\n mode: \"exactOut\",\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmountRaw: amountBigInt,\n recipient: resolvedRecipientAddress as `0x${string}`,\n ...fromSourcesPayload,\n }\n : {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmountRaw: amountBigInt,\n execute: executeConfig,\n ...fromSourcesPayload,\n };\n logSdkIntentInput(\n isTransferExactOut\n ? \"swapAndTransfer exactOut\"\n : \"swapAndExecute exactOut\",\n exactOutOperationInput,\n {\n activeMode,\n quoteInputKey,\n runId,\n }\n );\n startIntentFetchTiming({\n background,\n operation: isTransferExactOut\n ? \"swapAndTransfer exactOut\"\n : \"swapAndExecute exactOut\",\n quoteInputKey,\n runId,\n });\n const result =\n isTransferExactOut\n ? await sdkWithOptionalTransfer.swapAndTransfer(\n exactOutOperationInput,\n {\n onEvent,\n onIntent: (data: any) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n }\n )\n : await nexusSDK.swapAndExecute(\n exactOutOperationInput as any,\n {\n onEvent,\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n }\n );\n\n const swapResult = result?.swapResult ?? result?.result ?? null;\n const swapSkipped = Boolean((result as any)?.swapSkipped);\n if (swapSkipped) {\n appendSkippedSwapProgress();\n }\n if (\n !swapResult &&\n !swapSkipped &&\n activeMode !== \"send\" &&\n !hasCustomSwapRecipient\n ) {\n throw new Error(\"Swap failed\");\n }\n const executeTxHash = getSdkTransactionHash(result);\n const intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result,\n swapResult\n );\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const finalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n executeTxHash,\n result,\n swapResult\n );\n if (finalExplorerUrl) {\n if (activeMode === \"send\" || hasCustomSwapRecipient) {\n setTransferExplorerUrl(finalExplorerUrl);\n }\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n patchCurrentSwapHistoryEntry({\n ...(finalExplorerUrl ? { finalExplorerUrl } : {}),\n ...(intentExplorerUrl ? { intentExplorerUrl } : {}),\n ...(intentId ? { intentId } : {}),\n });\n } else {\n const exactOutSwapInput = {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmountRaw: amountBigInt,\n ...fromSourcesPayload,\n };\n logSdkIntentInput(\"swapWithExactOut\", exactOutSwapInput, {\n activeMode,\n quoteInputKey,\n runId,\n });\n startIntentFetchTiming({\n background,\n operation: \"swapWithExactOut\",\n quoteInputKey,\n runId,\n });\n const result = await nexusSDK.swapWithExactOut(\n exactOutSwapInput,\n {\n hooks: {\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n },\n onEvent,\n }\n );\n const intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result\n );\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const swapResult = getSdkSwapResult(result);\n const finalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result,\n swapResult\n );\n if (finalExplorerUrl) {\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n patchCurrentSwapHistoryEntry({\n ...(finalExplorerUrl ? { finalExplorerUrl } : {}),\n ...(intentExplorerUrl ? { intentExplorerUrl } : {}),\n ...(intentId ? { intentId } : {}),\n });\n }\n\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n finishCurrentSwapHistoryEntry(\"fulfilled\");\n resetInputsAfterSuccessfulExecution();\n onComplete?.();\n if (activeMode === \"deposit\") {\n reachedTerminalRef.current = true;\n const now = Date.now();\n trackDeposit(\"deposit_completed\", {\n postConfirmDurationMs: previewConfirmedTsRef.current\n ? now - previewConfirmedTsRef.current\n : 0,\n totalDurationMs: now - widgetOpenedTsRef.current,\n attemptCount: attemptCountRef.current,\n amountToken: depositTokenDisplay,\n amountUsd: Number(depositUsdDisplay) || 0,\n });\n }\n setSwapStep(\"success\");\n }\n }\n } catch (err: any) {\n const caughtTimeout = isTimeoutLikeError(err);\n if (caughtTimeout) {\n console.warn(\"Timeout in handleEnterPreview:\", err);\n } else {\n console.error(\"Error in handleEnterPreview:\", err);\n }\n if (swapRunIdRef.current !== runId || !isCurrentQuoteInput()) {\n return;\n }\n finishIntentFetchTiming(runId, \"failed\");\n if (activeMode === \"deposit\" && err?.code !== \"USER_DENIED_INTENT\") {\n const hasActiveExecution =\n swapStepRef.current === \"progress\" &&\n Boolean(currentSwapIdRef.current);\n const isInsufficient = isInsufficientSourcesError(err);\n const errMessage =\n (typeof err?.message === \"string\" ? err.message : \"\") ||\n (typeof err === \"string\" ? err : \"\");\n const errName = typeof err?.name === \"string\" ? err.name : \"\";\n const isTimeout = isTimeoutLikeError(err);\n const isUserRejected =\n err?.code === 4001 ||\n err?.code === \"ACTION_REJECTED\" ||\n errName === \"UserRejectedRequestError\" ||\n /user rejected|user denied/i.test(errMessage);\n const failedAtStep:\n | \"simulation\"\n | \"nexus_operation\"\n | \"execute_leg\"\n | \"unknown\" = !hasActiveExecution ? \"simulation\" : \"nexus_operation\";\n const errorCategory: string = isUserRejected\n ? \"user_rejected\"\n : isTimeout\n ? \"timeout\"\n : isInsufficient\n ? \"no_eligible_sources\"\n : !hasActiveExecution\n ? \"quote_failed\"\n : fundsMovedRef.current\n ? \"pending_after_funds_moved\"\n : \"execution_failed\";\n reachedTerminalRef.current = true;\n if (fundsMovedRef.current) {\n trackDeposit(\"deposit_pending_after_funds_moved\", {\n errorCode: err?.code ?? \"UNKNOWN\",\n errorMessage: errMessage || \"Transaction pending.\",\n intentUrl: intentUrlRef.current,\n failedAtStep,\n });\n } else {\n trackDeposit(\"deposit_failed\", {\n errorCode: err?.code ?? \"UNKNOWN\",\n errorCategory,\n errorMessage: errMessage || \"Transaction failed.\",\n failedAtStep,\n });\n }\n }\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n const hasActiveExecution =\n swapStepRef.current === \"progress\" && Boolean(currentSwapIdRef.current);\n const isTimeout = caughtTimeout;\n const showFailedProgressThenReceipt = (\n error: string,\n patch: Partial = {}\n ) => {\n const failedProgressEvent = progressEventsRef.current.at(-1);\n const isTransferExecution =\n activeMode === \"send\" || hasCustomSwapRecipient;\n const fallbackFailedStep =\n activeMode === \"deposit\" || isTransferExecution\n ? ({ type: \"APPROVAL\", typeID: \"AP\" } as BridgeStepType)\n : ({\n type: \"DETERMINING_SWAP\",\n typeID: \"DETERMINING_SWAP\",\n } as unknown as SwapStepType);\n const failedStep = failedProgressEvent?.step ?? fallbackFailedStep;\n const autoRefundAvailable =\n isAutoRefundAvailableProgressEvent(failedProgressEvent);\n setFailedProgressStep(failedStep);\n finishCurrentSwapHistoryEntry(\"failed\", {\n error,\n autoRefundAvailable,\n failureDescription: getFailureDescriptionForProgressStep(\n failedStep,\n autoRefundAvailable\n ),\n failureMessage: getFailureMessageForProgressStep(\n failedStep,\n hasCustomSwapRecipient ? \"send\" : activeMode,\n autoRefundAvailable\n ),\n failedStepType: getProgressStepType(failedStep),\n ...patch,\n });\n window.setTimeout(() => {\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n setSwapStep(\"failed\");\n }\n }, 700);\n };\n const showTimeoutReceipt = (\n message = \"Transaction timed out\",\n patch: Partial = {}\n ) => {\n finishCurrentSwapHistoryEntry(\"timeout\", {\n error: message,\n failureDescription:\n \"This transaction is still pending. Check the intent explorer for the latest status.\",\n failureMessage: TIMEOUT_LABEL,\n ...patch,\n });\n window.setTimeout(() => {\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n setSwapStep(\"failed\");\n }\n }, 700);\n };\n if (err?.code === \"USER_DENIED_INTENT\") {\n if (hasActiveExecution) {\n showFailedProgressThenReceipt(\"Transaction cancelled by user\");\n } else if (!background && swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n return;\n }\n if (isInsufficientSourcesError(err) && !hasActiveExecution) {\n const issue = buildInsufficientSourcesIssue(err);\n if (!background || swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n setTxError(null);\n setSwapQuoteIssue(issue);\n onError?.(issue.message);\n return;\n }\n if (isExactOutFlow && isViemInvalidDecimalError(err) && !hasActiveExecution) {\n const issue = buildExactOutSourceBalanceIssue();\n if (issue) {\n if (!background || swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n setTxError(null);\n setSwapQuoteIssue(issue);\n onError?.(issue.message);\n return;\n }\n }\n const errorMessage =\n err?.message ||\n (typeof err === \"string\"\n ? err\n : \"Transaction failed. Please try again or check console.\");\n if (isTimeout && hasActiveExecution) {\n showTimeoutReceipt(errorMessage);\n setTxError(null);\n return;\n }\n if (hasActiveExecution && fundsMovedRef.current) {\n showTimeoutReceipt(errorMessage, {\n failureDescription:\n \"Funds have already moved and the transaction is still pending. Check the intent explorer for the latest status.\",\n });\n setTxError(null);\n return;\n }\n if (hasActiveExecution) {\n showFailedProgressThenReceipt(errorMessage);\n } else if (!background || swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n setTxError(errorMessage);\n onError?.(errorMessage);\n }\n };\n\n const hasInsufficientSourcesQuoteIssue =\n swapQuoteIssue?.type === \"insufficientSources\";\n const hasReceiveAmountQuoteIssue = Boolean(receiveAmountIssue);\n\n useEffect(() => {\n if (activeMode !== \"swap\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = false;\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n }\n\n if (hasReceiveAmountQuoteIssue) {\n clearPendingSwapIntent(true);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n if (hasInsufficientSourcesQuoteIssue) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n const hasEnoughForQuote = hasReadyExactInSwapInput(fromTokens, toToken);\n\n if (!hasEnoughForQuote) {\n clearPendingSwapIntent();\n setSwapQuoteIssue(null);\n setTxError(null);\n return;\n }\n\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n return;\n }\n\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n let quoteStarted = false;\n const quoteDelay = getQuoteRequestDelay();\n const timer = window.setTimeout(() => {\n quoteStarted = true;\n void handleEnterPreview({ background: true });\n }, quoteDelay);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (!quoteStarted && swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n activeQuoteInputKey,\n amount,\n defaultRecipientAddress,\n fromTokensQuoteKey,\n hasCurrentQuoteIntent,\n hasInsufficientSourcesQuoteIssue,\n hasReceiveAmountQuoteIssue,\n getQuoteRequestDelay,\n nexusSDK,\n recipientAddress,\n swapStep,\n toTokenQuoteKey,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = false;\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n }\n\n if (hasReceiveAmountQuoteIssue) {\n clearPendingSwapIntent(true);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n if (hasInsufficientSourcesQuoteIssue) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n const parsedAmount = parseFiatNumber(amount);\n const hasEnoughForQuote = Boolean(\n parsedAmount?.gt(0) &&\n toToken &&\n selectedOpportunity &&\n depositTokenAmountForQuote\n );\n\n if (!hasEnoughForQuote) {\n clearPendingSwapIntent();\n clearSelectedSources();\n return;\n }\n\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n return;\n }\n\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n let quoteStarted = false;\n const quoteDelay = getQuoteRequestDelay();\n const timer = window.setTimeout(() => {\n quoteStarted = true;\n void handleEnterPreview({ background: true });\n }, quoteDelay);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (!quoteStarted && swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n amount,\n activeQuoteInputKey,\n depositAmountMode,\n depositQuoteAmountKey,\n hasCurrentQuoteIntent,\n hasInsufficientSourcesQuoteIssue,\n hasReceiveAmountQuoteIssue,\n getQuoteRequestDelay,\n nexusSDK,\n sourceSelectionRevision,\n selectedOpportunityIdentity,\n swapStep,\n toTokenQuoteKey,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"send\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = false;\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n }\n\n if (hasReceiveAmountQuoteIssue) {\n clearPendingSwapIntent(true);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n if (hasInsufficientSourcesQuoteIssue) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n const parsedAmount = parseFiatNumber(amount);\n const hasEnoughForQuote = Boolean(parsedAmount?.gt(0) && toToken);\n\n if (!hasEnoughForQuote) {\n clearPendingSwapIntent();\n clearSelectedSources();\n return;\n }\n\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n return;\n }\n\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n let quoteStarted = false;\n const quoteDelay = getQuoteRequestDelay();\n const timer = window.setTimeout(() => {\n quoteStarted = true;\n void handleEnterPreview({ background: true });\n }, quoteDelay);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (!quoteStarted && swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n amount,\n activeQuoteInputKey,\n hasCurrentQuoteIntent,\n hasInsufficientSourcesQuoteIssue,\n hasReceiveAmountQuoteIssue,\n getQuoteRequestDelay,\n nexusSDK,\n sourceSelectionRevision,\n swapStep,\n toTokenQuoteKey,\n ]);\n\n const refreshActiveSwapIntent = useCallback(async () => {\n if (receiveAmountIssueRef.current) return;\n\n const activeIntent = swapIntentRef.current;\n if (\n !activeIntent ||\n intentLoading ||\n quoteRefreshing ||\n receiveMaxCalculating ||\n previewQuoteRefreshing\n ) {\n return;\n }\n\n const runId = activeIntent.runId;\n const quoteInputKey = activeIntent.quoteInputKey;\n if (!quoteInputKey || activeQuoteInputKeyRef.current !== quoteInputKey) {\n return;\n }\n const isPreviewRefresh = swapStepRef.current === \"preview-intent\";\n if (isPreviewRefresh) {\n setPreviewQuoteRefreshing(true);\n } else {\n setQuoteRefreshing(true);\n }\n try {\n const updatedRaw = await activeIntent.refresh();\n const updatedBridgeProvider = normalizeBridgeProvider(\n (updatedRaw as any)?.bridgeProvider ??\n (updatedRaw as any)?.normalizedIntent?.bridgeProvider ??\n (updatedRaw as any)?.swap?.bridgeProvider ??\n activeIntent.intent?.bridgeProvider\n );\n const updated = normalizeRenderableSwapIntentData(\n updatedRaw,\n updatedBridgeProvider\n );\n if (\n !updated ||\n swapRunIdRef.current !== runId ||\n activeQuoteInputKeyRef.current !== quoteInputKey\n ) {\n return;\n }\n\n if (swapIntentRef.current) {\n swapIntentRef.current.intent = updated;\n }\n applySwapIntent(updated);\n } catch (err) {\n console.error(\"Unable to refresh swap intent\", err);\n } finally {\n if (\n swapRunIdRef.current === runId &&\n activeQuoteInputKeyRef.current === quoteInputKey\n ) {\n if (isPreviewRefresh) {\n setPreviewQuoteRefreshing(false);\n } else {\n setQuoteRefreshing(false);\n }\n }\n }\n }, [\n applySwapIntent,\n intentLoading,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n ]);\n\n useEffect(() => {\n const hasRefreshableIntent =\n (activeMode === \"swap\" ||\n activeMode === \"deposit\" ||\n activeMode === \"send\") &&\n Boolean(\n intentData &&\n swapIntentRef.current &&\n swapIntentRef.current.quoteInputKey === activeQuoteInputKey\n ) &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n\n if (!hasRefreshableIntent || receiveAmountIssue) {\n setQuoteRefreshProgress(0);\n setQuoteRefreshSecondsRemaining(0);\n return;\n }\n\n let cancelled = false;\n let timeout: number | undefined;\n\n const scheduleRefresh = () => {\n const quoteAge = Date.now() - lastSwapIntentRefreshAtRef.current;\n const delay = Math.max(0, QUOTE_REFRESH_INTERVAL_MS - quoteAge);\n timeout = window.setTimeout(() => {\n if (receiveAmountIssueRef.current) {\n clearPendingSwapIntent(true);\n setQuoteRefreshProgress(0);\n setQuoteRefreshSecondsRemaining(0);\n return;\n }\n\n if (\n intentLoading ||\n quoteRefreshing ||\n receiveMaxCalculating ||\n previewQuoteRefreshing\n ) {\n if (!cancelled) {\n timeout = window.setTimeout(scheduleRefresh, 1000);\n }\n return;\n }\n\n void refreshActiveSwapIntent().finally(() => {\n if (!cancelled) {\n scheduleRefresh();\n }\n });\n }, delay);\n };\n\n scheduleRefresh();\n\n return () => {\n cancelled = true;\n if (timeout !== undefined) {\n window.clearTimeout(timeout);\n }\n };\n }, [\n activeMode,\n activeQuoteInputKey,\n intentData,\n intentLoading,\n receiveAmountIssue,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n refreshActiveSwapIntent,\n swapStep,\n ]);\n\n useEffect(() => {\n const hasRefreshableIntent =\n (activeMode === \"swap\" ||\n activeMode === \"deposit\" ||\n activeMode === \"send\") &&\n Boolean(\n intentData &&\n swapIntentRef.current &&\n swapIntentRef.current.quoteInputKey === activeQuoteInputKey\n ) &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n\n if (!hasRefreshableIntent || receiveAmountIssue) {\n setQuoteRefreshProgress(0);\n setQuoteRefreshSecondsRemaining(0);\n return;\n }\n\n const updateProgress = () => {\n const quoteAge = Date.now() - lastSwapIntentRefreshAtRef.current;\n const remaining = Math.max(0, QUOTE_REFRESH_INTERVAL_MS - quoteAge);\n setQuoteRefreshProgress(remaining / QUOTE_REFRESH_INTERVAL_MS);\n setQuoteRefreshSecondsRemaining(Math.ceil(remaining / 1000));\n };\n\n updateProgress();\n const interval = window.setInterval(updateProgress, 250);\n\n return () => window.clearInterval(interval);\n }, [\n activeMode,\n activeQuoteInputKey,\n intentData,\n receiveAmountIssue,\n swapStep,\n ]);\n\n /** User accepted swap from the preview — call allow() from the intent hook */\n const handleSwapAccept = () => {\n const activeIntent = swapIntentRef.current;\n if (activeIntent) {\n if (\n activeIntent.quoteInputKey &&\n activeQuoteInputKeyRef.current !== activeIntent.quoteInputKey\n ) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n setSwapStep(\"idle\");\n return;\n }\n if (activeMode === \"deposit\") {\n previewConfirmedTsRef.current = Date.now();\n attemptCountRef.current += 1;\n const timeInPreviewMs = previewViewedTsRef.current\n ? previewConfirmedTsRef.current - previewViewedTsRef.current\n : 0;\n trackDeposit(\"deposit_preview_confirmed\", {\n timeInPreviewMs,\n totalFeeUsd: Number(intentFeeUsd) || 0,\n sourceCount: (intentData?.sources ?? []).length,\n });\n }\n onStart?.();\n startSwapHistoryEntry();\n setSwapStep(\"progress\");\n setQuoteRefreshing(false);\n resetProgressEvents();\n if (swapStepsListRef.current.length > 0) {\n seed(swapStepsListRef.current);\n } else {\n resetSteps();\n }\n activeIntent.allow();\n }\n };\n\n // ---------------------------------------------------------------------------\n // Header title\n // ---------------------------------------------------------------------------\n const getTitle = () => {\n const configuredWidgetHeading = normalizeConfiguredString(\n appearanceConfig?.widgetHeading\n );\n if (swapStep === \"history\") return \"Transaction History\";\n // Drawer panels overlay the main page,\n // so the header should still show the main page title.\n\n if (swapStep === \"preview-intent\") {\n return activeMode === \"deposit\"\n ? \"Confirm Deposit\"\n : activeMode === \"send\"\n ? \"Confirm Send\"\n : \"Confirm Swap\";\n }\n\n if (activeMode === \"swap\") {\n if (swapStep === \"progress\") return \"Swapping…\";\n if (swapStep === \"success\") return \"Swap Complete\";\n if (swapStep === \"failed\" && currentSwapEntry?.status === \"timeout\") {\n return TIMEOUT_LABEL;\n }\n if (swapStep === \"failed\") return \"Swap Failed\";\n return configuredWidgetHeading ?? \"Swap and Bridge\";\n }\n if (activeMode === \"deposit\") {\n if (swapStep === \"progress\") return \"Depositing…\";\n if (swapStep === \"success\") return \"Deposit Complete\";\n if (swapStep === \"failed\" && currentSwapEntry?.status === \"timeout\") {\n return TIMEOUT_LABEL;\n }\n if (swapStep === \"failed\") return \"Deposit Failed\";\n return configuredWidgetHeading ?? \"Deposit\";\n }\n if (activeMode === \"send\") {\n if (swapStep === \"progress\") return \"Sending…\";\n if (swapStep === \"success\") return \"Send Complete\";\n if (swapStep === \"failed\" && currentSwapEntry?.status === \"timeout\") {\n return TIMEOUT_LABEL;\n }\n if (swapStep === \"failed\") return \"Send Failed\";\n return configuredWidgetHeading ?? \"Send\";\n }\n return configuredWidgetHeading ?? \"Nexus Widget\";\n };\n\n // Titles that should be center-aligned (main screens / confirm screens)\n // Left-aligned: choose-swap-asset, choose-receive-asset (sub-screens with subtitles)\n const isTitleCentered = () => {\n if (swapStep === \"history\") return false;\n return true; // idle, drawer panels, preview-intent, progress, etc.\n };\n\n const canGoBack = swapStep === \"preview-intent\" || swapStep === \"history\";\n const handleBack = () => {\n if (swapStep === \"history\") {\n setSwapStep(\"idle\");\n return;\n }\n if (swapStep === \"choose-swap-asset\") {\n handleSourcePickerCancel();\n return;\n }\n if (swapStep === \"choose-receive-asset\") {\n closeDrawerToIdle();\n return;\n }\n if (swapStep === \"enter-recipient\") {\n closeDrawerToIdle();\n return;\n }\n if (swapStep === \"preview-intent\") {\n const canRequoteAfterPreviewBack =\n activeMode === \"swap\"\n ? hasReadyExactInSwapInput(fromTokens, toToken)\n : canRefreshExactOutQuote();\n\n if (\n canRequoteAfterPreviewBack &&\n (activeMode === \"deposit\" || activeMode === \"send\")\n ) {\n setExactOutQuoteSourceModeValue(\"all\");\n }\n if (activeMode === \"deposit\" || activeMode === \"send\") {\n invalidateExactOutQuoteForRefresh();\n } else {\n clearPendingSwapIntent(true, {\n keepQuoteRefreshing: canRequoteAfterPreviewBack,\n });\n }\n if (canRequoteAfterPreviewBack && activeMode === \"swap\") {\n setQuoteRefreshing(true);\n setTxError(null);\n setSwapQuoteIssue(null);\n }\n setSwapStep(\"idle\");\n return;\n }\n if (swapStep === \"progress\") {\n return;\n } // can't go back during tx\n setSwapStep(\"idle\");\n };\n\n const handleSwapAmountChange = (val: string, panel: \"send\" | \"receive\") => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n setSwapQuoteIssue(null);\n setTxError(null);\n const nextAmount = parseFiatNumber(val);\n const receiveIssue = buildReceiveAmountIssue({ inputAmount: val });\n applyReceiveAmountIssue(receiveIssue);\n const hasSelectedSourceToken = fromTokens.some(\n (token) => token.chainId && token.contractAddress\n );\n const shouldLoadQuote = Boolean(\n !receiveIssue &&\n nexusSDK &&\n nextAmount?.gt(0) &&\n toToken &&\n hasSelectedSourceToken\n );\n if (!receiveIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n }\n setAmount(val);\n if (panel === \"receive\") {\n setFromTokens((prev) =>\n prev.map((token) => ({ ...token, userAmount: \"\" }))\n );\n }\n // Nexus Widget swaps are exact-in only. Exact-out is reserved for Deposit and Send.\n if (swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n };\n\n const handleSwapTokensUpdate = (tokens: SwapTokenOption[]) => {\n setSwapQuoteIssue(null);\n setTxError(null);\n applyReceiveAmountIssue(buildReceiveAmountIssue({ sourceTokens: tokens }));\n setFromTokens(tokens);\n };\n\n const handleDepositAmountChange = (val: string) => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n resetExactOutSourcesToAuto();\n maxPercentRunRef.current += 1;\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setSwapQuoteIssue(null);\n const nextAmount = parseFiatNumber(val);\n const receiveIssue = buildReceiveAmountIssue({ inputAmount: val });\n applyReceiveAmountIssue(receiveIssue);\n const shouldLoadQuote = Boolean(\n !receiveIssue &&\n nexusSDK &&\n nextAmount?.gt(0) &&\n toToken &&\n selectedOpportunity\n );\n if (!receiveIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n } else {\n clearSelectedSources();\n }\n setAmount(val);\n };\n\n const handleSendAmountChange = (val: string) => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n resetExactOutSourcesToAuto();\n maxPercentRunRef.current += 1;\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setSwapQuoteIssue(null);\n setSwapType(\"exactOut\");\n const nextAmount = parseFiatNumber(val);\n const receiveIssue = buildReceiveAmountIssue({\n inputAmount: val,\n type: \"exactOut\",\n });\n applyReceiveAmountIssue(receiveIssue);\n const shouldLoadQuote = Boolean(\n !receiveIssue && nexusSDK && nextAmount?.gt(0) && toToken\n );\n if (!receiveIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n } else {\n clearSelectedSources();\n }\n setAmount(val);\n };\n\n const handleDepositAmountModeToggle = () => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n resetExactOutSourcesToAuto();\n const rate = getDepositTokenUsdRate();\n const parsedAmount = parseFiatNumber(amount) ?? new Decimal(0);\n if (parsedAmount.gt(0) && rate.gt(0)) {\n const converted =\n depositAmountMode === \"token\"\n ? parsedAmount.mul(rate).toDecimalPlaces(2)\n : parsedAmount\n .div(rate)\n .toDecimalPlaces(\n getCappedTokenDisplayDecimals(toToken?.decimals),\n Decimal.ROUND_DOWN\n );\n setAmount(converted.toFixed());\n }\n clearPendingSwapIntent();\n setDepositAmountMode((current) => (current === \"token\" ? \"usd\" : \"token\"));\n };\n\n const handleDepositPercentSelect = async (pct: number) => {\n if (isConfiguredAmountFixed) return;\n if (!toToken) return;\n\n syncingIntentSourcesRef.current = false;\n setTxError(null);\n setSwapQuoteIssue(null);\n const runId = ++maxPercentRunRef.current;\n lastInputMethodRef.current =\n pct === 20\n ? \"percent_20\"\n : pct === 25\n ? \"percent_25\"\n : pct === 50\n ? \"percent_50\"\n : pct === 75\n ? \"percent_75\"\n : \"percent_max\";\n\n if (pct !== 100) {\n const nextAmount = getExactOutPercentAmountFromBalance(\n toToken,\n pct,\n depositAmountMode === \"usd\"\n );\n\n if (nextAmount) {\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setDepositAmountMode(nextAmount.mode);\n handleDepositAmountChange(nextAmount.amount);\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setTxError(\"Unable to calculate this percentage for the deposit asset.\");\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n const maxAmount = getExactOutPercentAmountFromBalance(\n toToken,\n 100,\n depositAmountMode === \"usd\"\n );\n if (runId !== maxPercentRunRef.current) return;\n if (!maxAmount) {\n setTxError(\"No depositable amount is available for this deposit.\");\n return;\n }\n\n setDepositAmountMode(maxAmount.mode);\n handleDepositAmountChange(maxAmount.amount);\n };\n\n const handleSendPercentSelect = async (pct: number) => {\n if (isConfiguredAmountFixed) return;\n if (!toToken) return;\n\n syncingIntentSourcesRef.current = false;\n setTxError(null);\n setSwapQuoteIssue(null);\n const runId = ++maxPercentRunRef.current;\n lastInputMethodRef.current =\n pct === 20\n ? \"percent_20\"\n : pct === 25\n ? \"percent_25\"\n : pct === 50\n ? \"percent_50\"\n : pct === 75\n ? \"percent_75\"\n : \"percent_max\";\n\n if (pct !== 100) {\n const nextAmount = getExactOutPercentAmountFromBalance(\n toToken,\n pct,\n false\n );\n\n if (nextAmount) {\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleSendAmountChange(nextAmount.amount);\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setTxError(\"Unable to calculate this percentage for the send asset.\");\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n const maxAmount = getExactOutPercentAmountFromBalance(toToken, 100, false);\n if (runId !== maxPercentRunRef.current) return;\n if (!maxAmount) {\n setTxError(\"No transferable amount is available for this asset.\");\n return;\n }\n\n handleSendAmountChange(maxAmount.amount);\n };\n\n // ---------------------------------------------------------------------------\n // Render\n // ---------------------------------------------------------------------------\n const insufficientSourceIssue =\n swapQuoteIssue?.type === \"insufficientSources\" ? swapQuoteIssue : null;\n const configuredAmountIssue = buildConfiguredAmountIssue();\n const blockingQuoteIssue =\n insufficientSourceIssue ?? receiveAmountIssue ?? configuredAmountIssue;\n const isExactOutPaymentFlow =\n activeMode === \"deposit\" || activeMode === \"send\";\n const hasCurrentRunnableIntent = hasCurrentQuoteIntent;\n const hasIntentSources = Boolean((intentData?.sources ?? []).length > 0);\n const hasCurrentExactOutPaymentIntent =\n hasCurrentRunnableIntent &&\n (hasIntentSources ||\n (isExactOutPaymentFlow && Boolean(intentData?.destination)));\n const isExactOutRouteLoading =\n isExactOutPaymentFlow &&\n swapStep === \"idle\" &&\n swapType === \"exactOut\" &&\n Boolean(\n toToken && (receiveMaxCalculating || (amount && Number(amount) > 0))\n ) &&\n !blockingQuoteIssue &&\n !hasCurrentExactOutPaymentIntent &&\n (quoteRefreshing || intentLoading || receiveMaxCalculating);\n const isQuoteUnavailableForAutoSourceFlow =\n isExactOutPaymentFlow &&\n Boolean(hasPositiveDecimalInput(amount) && toToken) &&\n !quoteRefreshing &&\n !receiveMaxCalculating &&\n !intentLoading &&\n !blockingQuoteIssue &&\n !hasCurrentExactOutPaymentIntent;\n const hasPositiveRootAmount = hasPositiveDecimalInput(amount);\n const hasReadySwapQuoteInput = hasReadyExactInSwapInput(fromTokens, toToken);\n const needsWalletConnection = !ownerAddress || !nexusSDK;\n const isExactOutPaymentQuotePending =\n isExactOutPaymentFlow && (quoteRefreshing || intentLoading);\n const walletConnectBusy =\n walletActionPending ||\n nexusLoading ||\n isWalletConnectPending ||\n walletStatus === \"connecting\";\n const hasConnectWalletHandler = Boolean(\n onConnectClick || onConnectWallet || connectors.length > 0\n );\n const walletCtaLabel = hasConnectWalletHandler\n ? walletConnectBusy\n ? \"Connecting...\"\n : \"Connect Wallet\"\n : \"Connect your wallet to proceed\";\n const isSwapCtaDisabled = needsWalletConnection\n ? !hasConnectWalletHandler || walletConnectBusy\n : !hasReadySwapQuoteInput ||\n receiveMaxCalculating ||\n quoteRefreshing ||\n Boolean(blockingQuoteIssue);\n const isDepositCtaDisabled = needsWalletConnection\n ? !hasConnectWalletHandler || walletConnectBusy\n : !hasPositiveRootAmount ||\n !toToken ||\n receiveMaxCalculating ||\n isExactOutPaymentQuotePending ||\n (!hasCurrentExactOutPaymentIntent &&\n isQuoteUnavailableForAutoSourceFlow) ||\n Boolean(blockingQuoteIssue);\n const sendNeedsRecipient = activeMode === \"send\" && !recipientAddress;\n const isSendCtaDisabled = needsWalletConnection\n ? !hasConnectWalletHandler || walletConnectBusy\n : !hasPositiveRootAmount ||\n !toToken ||\n hasSameOwnerSendRecipient ||\n receiveMaxCalculating ||\n (!sendNeedsRecipient &&\n (isExactOutPaymentQuotePending ||\n (!hasCurrentExactOutPaymentIntent &&\n isQuoteUnavailableForAutoSourceFlow))) ||\n Boolean(blockingQuoteIssue);\n const quoteCtaLabel = (fallback: string) => {\n if (needsWalletConnection) return walletCtaLabel;\n if (insufficientSourceIssue) return \"Insufficient balance\";\n if (receiveAmountIssue) return receiveAmountIssue.ctaLabel;\n if (configuredAmountIssue) return configuredAmountIssue.ctaLabel;\n if (receiveMaxCalculating) return \"Calculating...\";\n if (\n isExactOutPaymentQuotePending ||\n (!hasCurrentExactOutPaymentIntent && (quoteRefreshing || intentLoading))\n ) {\n return \"Fetching quotes...\";\n }\n if (isQuoteUnavailableForAutoSourceFlow) return \"Quote unavailable\";\n if (!hasPositiveRootAmount) return \"Enter amount\";\n return fallback;\n };\n const sendCtaLabel = (() => {\n if (needsWalletConnection) return walletCtaLabel;\n if (insufficientSourceIssue) return \"Insufficient balance\";\n if (receiveAmountIssue) return receiveAmountIssue.ctaLabel;\n if (configuredAmountIssue) return configuredAmountIssue.ctaLabel;\n if (!hasPositiveRootAmount) return \"Enter amount\";\n if (!toToken) return \"Select token\";\n if (hasSameOwnerSendRecipient) return \"Change recipient\";\n if (sendNeedsRecipient) return \"Add recipient\";\n return quoteCtaLabel(\"Review send\");\n })();\n const previewIntentSourceUsdNumber = (intentData?.sources ?? []).reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0)\n );\n const previewSourceUsdNumber = previewIntentSourceUsdNumber.gt(0)\n ? previewIntentSourceUsdNumber\n : fromTokens.length > 0\n ? fromTokens.reduce(\n (sum, token) =>\n sum.plus(\n getTokenUsdValue(\n token,\n swapType === \"exactIn\" && fromTokens.length === 1\n ? amount\n : undefined\n )\n ),\n new Decimal(0)\n )\n : undefined;\n const previewExactOutDestinationAmount =\n activeMode === \"deposit\"\n ? depositTokenAmountForQuote\n : activeMode === \"send\"\n ? parseFiatNumber(amount)\n : undefined;\n const previewExactOutDestinationUsdNumber =\n activeMode === \"deposit\"\n ? depositUsdDecimal\n : activeMode === \"send\" && amount && toToken\n ? getTokenUsdValue(\n {\n ...toToken,\n userAmount: amount,\n userAmountMode: \"token\",\n },\n amount\n )\n : undefined;\n const previewDestinationUsdNumber =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n previewExactOutDestinationUsdNumber?.gt(0)\n ? previewExactOutDestinationUsdNumber\n : parseFiatNumber((intentData?.destination as any)?.value);\n const previewDestinationAmount =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n previewExactOutDestinationAmount?.gt(0)\n ? previewExactOutDestinationAmount\n .toDecimalPlaces(toToken?.decimals ?? 18, Decimal.ROUND_DOWN)\n .toFixed()\n : intentToAmount;\n const previewFromAmountUsd =\n previewSourceUsdNumber && previewSourceUsdNumber.gt(0)\n ? previewSourceUsdNumber.toDecimalPlaces(6).toFixed()\n : undefined;\n const previewToAmountUsd =\n previewDestinationUsdNumber && previewDestinationUsdNumber.gt(0)\n ? previewDestinationUsdNumber.toDecimalPlaces(6).toFixed()\n : undefined;\n const previewDestinationGasFeeUsd = (() => {\n const value = getIntentDestinationGasUsdValue(intentData);\n return value && value.gt(0)\n ? value.toDecimalPlaces(6, Decimal.ROUND_DOWN).toFixed()\n : undefined;\n })();\n const predictiveExactInQuote =\n predictiveQuote?.mode === \"exactIn\" &&\n predictiveQuote.key === getPredictiveQuoteCacheKey(\"swap\", \"exactIn\")\n ? predictiveQuote\n : null;\n const predictiveExactOutQuote =\n predictiveQuote?.mode === \"exactOut\" &&\n predictiveQuote.key === getPredictiveQuoteCacheKey(activeMode, \"exactOut\")\n ? predictiveQuote\n : null;\n const shouldUseCurrentExactOutIntentSources =\n isExactOutPaymentFlow && hasCurrentQuoteIntent && hasIntentSources;\n const currentExactOutIntentSourceTokens =\n shouldUseCurrentExactOutIntentSources\n ? sortSwapTokensByUsdDesc(\n (intentData?.sources ?? [])\n .map(buildIntentSourceToken)\n .filter(hasPositiveSourceSpend)\n )\n : [];\n const resolvedToToken =\n toToken ??\n (activeMode === \"deposit\" && selectedOpportunity\n ? toTokenFromOpportunity(selectedOpportunity)\n : undefined);\n const enrichedResolvedToToken = enrichTokenOptionLogo(resolvedToToken);\n const toTokenWithFetchedBalance =\n enrichedResolvedToToken && destinationBalance\n ? { ...enrichedResolvedToToken, balance: destinationBalance }\n : enrichedResolvedToToken;\n const idleReceiveQuoteAmount =\n activeMode === \"swap\" && swapType === \"exactIn\"\n ? (intentToAmount ?? predictiveExactInQuote?.toAmount)\n : undefined;\n const idleReceiveQuoteUsd =\n activeMode === \"swap\" && swapType === \"exactIn\"\n ? (previewToAmountUsd ?? predictiveExactInQuote?.toUsd)\n : previewToAmountUsd;\n const exactOutDestinationCoverage = getExactOutDestinationBalanceCoverage({\n requestedAmount: previewExactOutDestinationAmount,\n requestedUsd: previewExactOutDestinationUsdNumber,\n producedAmount: hasIntentSources\n ? parseFiatNumber(intentData?.destination?.amount)\n : undefined,\n producedUsd: hasIntentSources\n ? parseFiatNumber(intentData?.destination?.value)\n : undefined,\n token: toTokenWithFetchedBalance,\n });\n const destinationBalanceDisplayToken = buildDestinationBalanceDisplayToken(\n exactOutDestinationCoverage,\n toTokenWithFetchedBalance\n );\n const shouldShowPredictiveExactOutDisplay =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (quoteRefreshing || intentLoading) &&\n !hasIntentSources &&\n Boolean(\n predictiveExactOutQuote &&\n ((predictiveExactOutQuote.sources?.length ?? 0) > 0 ||\n destinationBalanceDisplayToken)\n );\n const baseDisplayFromTokens = shouldUseCurrentExactOutIntentSources\n ? currentExactOutIntentSourceTokens\n : shouldShowPredictiveExactOutDisplay\n ? (predictiveExactOutQuote?.sources ?? fromTokens)\n : fromTokens;\n const displayFromTokens = (() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n return baseDisplayFromTokens;\n }\n if (!destinationBalanceDisplayToken) {\n return mergeDisplaySourceTokens(\n sortDisplaySourcesByBalanceUsdDesc(baseDisplayFromTokens)\n );\n }\n\n const destinationKey = getTokenSelectionKey(destinationBalanceDisplayToken);\n let replacedEmptyDestinationToken = false;\n const tokens = baseDisplayFromTokens.map((token) => {\n const isDestinationToken = getTokenSelectionKey(token) === destinationKey;\n if (\n isDestinationToken &&\n !hasPositiveDecimalInput(token.userAmount) &&\n !hasPositiveDecimalInput(token.userAmountUsd)\n ) {\n replacedEmptyDestinationToken = true;\n return destinationBalanceDisplayToken;\n }\n return token;\n });\n\n const displayTokens = replacedEmptyDestinationToken\n ? tokens\n : [...tokens, destinationBalanceDisplayToken];\n return mergeDisplaySourceTokens(\n sortDisplaySourcesByBalanceUsdDesc(displayTokens)\n );\n })();\n const displayExactOutRouteLoading =\n isExactOutRouteLoading && !shouldShowPredictiveExactOutDisplay;\n const isSourcePickerRefreshDisabled =\n quoteRefreshing || intentLoading || previewQuoteRefreshing;\n const isIntentSourceUiActionable =\n activeMode === \"deposit\"\n ? !isDepositCtaDisabled\n : activeMode === \"send\"\n ? !isSendCtaDisabled\n : !isSwapCtaDisabled;\n\n useEffect(() => {\n if (!intentFetchTiming?.endTimeMs) {\n return;\n }\n if (latestIntentFetchRunIdRef.current !== intentFetchTiming.runId) {\n return;\n }\n if (intentUiRenderLoggedRunIdRef.current === intentFetchTiming.runId) {\n return;\n }\n const hasRenderedSources =\n displayFromTokens.length > 0 ||\n hasIntentSources ||\n Boolean(intentData?.destination);\n if (!hasRenderedSources) return;\n if (\n isSwapBalancePending ||\n isSourcePickerRefreshDisabled ||\n quoteRefreshing ||\n intentLoading ||\n previewQuoteRefreshing ||\n receiveMaxCalculating ||\n displayExactOutRouteLoading ||\n !isIntentSourceUiActionable\n ) {\n return;\n }\n\n const renderTimeMs = Date.now();\n const renderDurationMs = renderTimeMs - intentFetchTiming.startTimeMs;\n const fetchDurationMs =\n intentFetchTiming.fetchDurationMs ??\n intentFetchTiming.endTimeMs - intentFetchTiming.startTimeMs;\n const differenceMs = renderDurationMs - fetchDurationMs;\n\n console.log(\"[NexusWidget] intent sources UI render complete\", {\n runId: intentFetchTiming.runId,\n walletAddress: intentFetchTiming.walletAddress,\n operation: intentFetchTiming.operation,\n quoteInputKey: intentFetchTiming.quoteInputKey,\n sourceCount: displayFromTokens.length,\n dateNow: renderTimeMs,\n date: new Date(renderTimeMs),\n totalRenderTimeSeconds: renderDurationMs / 1000,\n elapsedMs: renderDurationMs,\n });\n console.log(\"[NexusWidget] intent render/fetch difference\", {\n runId: intentFetchTiming.runId,\n walletAddress: intentFetchTiming.walletAddress,\n operation: intentFetchTiming.operation,\n quoteInputKey: intentFetchTiming.quoteInputKey,\n differenceMs,\n dateNow: renderTimeMs,\n date: new Date(renderTimeMs),\n });\n intentUiRenderLoggedRunIdRef.current = intentFetchTiming.runId;\n }, [\n displayExactOutRouteLoading,\n displayFromTokens.length,\n hasIntentSources,\n intentLoading,\n intentData?.destination,\n intentFetchTiming,\n isDepositCtaDisabled,\n isIntentSourceUiActionable,\n isSendCtaDisabled,\n isSourcePickerRefreshDisabled,\n isSwapCtaDisabled,\n isSwapBalancePending,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n ]);\n\n const totalSwapBalanceUsd = getActiveTotalBalanceUsd()\n .toDecimalPlaces(2)\n .toFixed();\n const requestedOutputUsd =\n amount && toToken\n ? getTokenUsdValue(\n {\n ...toToken,\n userAmount: amount,\n userAmountMode: \"token\",\n },\n amount\n )\n : new Decimal(0);\n const sendAmountUsd = requestedOutputUsd.toNumber();\n const exactOutRequiredUsdAmount = (() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return undefined;\n\n const issueMissingUsd = parseFiatNumber(insufficientSourceIssue?.missingUsd);\n if (issueMissingUsd && issueMissingUsd.gt(0)) {\n return getExactOutAvailableSourceUsd().plus(issueMissingUsd);\n }\n\n const requiredFundingUsd = getExactOutRequiredFundingUsd();\n if (requiredFundingUsd?.gt(0)) return requiredFundingUsd;\n\n if (requestedOutputUsd.gt(0)) return requestedOutputUsd;\n\n return undefined;\n })();\n const exactOutRequiredUsdDisplay = exactOutRequiredUsdAmount\n ?.toDecimalPlaces(2)\n .toFixed();\n const assetSelectorRequiredUsdDisplay =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (swapStep === \"choose-swap-asset\" ||\n closingDrawerStep === \"choose-swap-asset\")\n ? (sourceSelectionRequiredUsdDisplay ?? exactOutRequiredUsdDisplay)\n : exactOutRequiredUsdDisplay;\n const captureSourceSelectionRequiredUsd = () => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n setSourceSelectionRequiredUsdDisplay(exactOutRequiredUsdDisplay);\n };\n const isIdleSwapQuoteLoading =\n activeMode === \"swap\" &&\n swapStep === \"idle\" &&\n (quoteRefreshing || intentLoading);\n const isReceiveAmountLoading =\n receiveMaxCalculating ||\n (isIdleSwapQuoteLoading &&\n swapType === \"exactIn\" &&\n !idleReceiveQuoteAmount);\n const isReceiveUsdLoading =\n receiveMaxCalculating ||\n (isIdleSwapQuoteLoading && swapType === \"exactIn\" && !idleReceiveQuoteUsd);\n const hasQuoteRefreshCountdown =\n (activeMode === \"swap\" ||\n activeMode === \"deposit\" ||\n activeMode === \"send\") &&\n hasCurrentQuoteIntent &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n const isRecipientDrawerClosing = closingDrawerStep === \"enter-recipient\";\n const isSwapAssetDrawerClosing = closingDrawerStep === \"choose-swap-asset\";\n const isReceiveAssetDrawerClosing =\n closingDrawerStep === \"choose-receive-asset\";\n const isReceiveAssetPickerLocked =\n activeMode === \"deposit\"\n ? configuredDepositOptions.length <= 1\n : (config.allowedDestinationPairs?.length ?? 0) === 1;\n const hideDestinationTokenDropdownIcon =\n activeMode === \"deposit\"\n ? configuredDepositOptions.length === 1\n : (config.allowedDestinationPairs?.length ?? 0) === 1;\n const isTokenAssetDrawerActive =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n closingDrawerStep === \"choose-swap-asset\" ||\n closingDrawerStep === \"choose-receive-asset\";\n const configuredReceiveTokenCount =\n activeMode === \"deposit\" || activeMode === \"send\"\n ? Math.max(\n configuredDestinationTokenOptions.length,\n config.allowedDestinationPairs?.length ?? 0\n )\n : 0;\n const isConfiguredReceiveAssetDrawerActive =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (swapStep === \"choose-receive-asset\" ||\n closingDrawerStep === \"choose-receive-asset\") &&\n Boolean(config.allowedDestinationPairs?.length);\n const tokenAssetDrawerMinRootContentHeight =\n isConfiguredReceiveAssetDrawerActive\n ? getConfiguredReceiveSelectorRootHeight(configuredReceiveTokenCount)\n : isTokenAssetDrawerActive\n ? TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT\n : 0;\n const isDrawerOverlayActive =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n swapStep === \"enter-recipient\" ||\n closingDrawerStep !== null;\n const isPreviewIntentScreen = swapStep === \"preview-intent\";\n const displayedRootContentHeight =\n hasMeasuredRootContent && rootContentHeight\n ? Math.max(\n rootContentHeight,\n tokenAssetDrawerMinRootContentHeight\n )\n : null;\n\n const widgetContent = (\n {\n const transitions = [];\n if (hasMeasuredRootContent && shouldAnimateRootHeight) {\n transitions.push(`height ${ROOT_HEIGHT_TRANSITION_MS}ms ease-out`);\n }\n return transitions.join(\", \");\n })(),\n willChange: \"height\",\n width: \"450px\",\n maxWidth: \"100%\",\n minWidth: \"320px\",\n WebkitFontSmoothing: \"antialiased\",\n MozOsxFontSmoothing: \"grayscale\",\n }}\n >\n \n \n
\n {canGoBack && (\n \n \n \n )}\n \n {getTitle()}\n
\n\n {/* Sub-screen asset counts */}\n {!isTitleCentered() &&\n activeMode === \"swap\" &&\n swapStep === \"choose-swap-asset\" &&\n swapType === \"exactIn\" && (\n \n {fromTokens.length} asset(s) selected\n \n )}\n \n\n {/* Right side icons */}\n \n {hasQuoteRefreshCountdown && (\n \n )}\n setSwapStep(\"history\")}\n style={{\n alignItems: \"center\",\n backgroundColor: theme.primitives.iconButton.backgroundColor,\n borderColor: theme.primitives.iconButton.borderColor,\n borderRadius: theme.radius.iconButton,\n borderStyle: \"solid\",\n borderWidth: \"1px\",\n boxShadow: theme.primitives.iconButton.boxShadow,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n height: \"28px\",\n justifyContent: \"center\",\n width: \"28px\",\n cursor: \"pointer\",\n padding: 0,\n }}\n >\n \n \n \n \n \n \n {showCloseButton && (\n \n \n \n \n \n )}\n \n \n\n {/* ------------------------------------------------------------------ */}\n {/* Main content area */}\n {/* ------------------------------------------------------------------ */}\n \n {/* =============================================================== */}\n {/* SHARED SUB-SCREENS (non-drawer panels) */}\n {/* =============================================================== */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep !== \"idle\" &&\n swapStep !== \"choose-swap-asset\" &&\n swapStep !== \"choose-receive-asset\" &&\n swapStep !== \"enter-recipient\" && (\n <>\n {/* Panel: preview. */}\n {swapStep === \"preview-intent\" && (\n \n {\n clearPendingSwapIntent();\n setSwapStep(\"idle\");\n }}\n onTransitionChange={setIsPreviewTransitioning}\n opportunity={selectedOpportunity}\n recipientAddress={transferRecipientAddress}\n steps={steps}\n supportedTokenAssets={supportedChainsAndTokens}\n swapBalances={swapBalance}\n swapType={swapType}\n toAmount={previewDestinationAmount}\n toAmountTokens={\n previewDestinationAmount\n ? `${previewDestinationAmount}`\n : undefined\n }\n toAmountUsd={previewToAmountUsd}\n toToken={toTokenWithFetchedBalance}\n totalFeeUsd={intentFeeUsd}\n />\n \n )}\n\n {swapStep === \"progress\" && (\n \n )}\n\n {(swapStep === \"success\" || swapStep === \"failed\") &&\n currentSwapEntry && (\n
\n \n
\n )}\n \n )}\n\n {/* =============================================================== */}\n {/* HISTORY SCREEN */}\n {/* =============================================================== */}\n {swapStep === \"history\" && (\n \n )}\n\n {/* =============================================================== */}\n {/* SWAP IDLE SCREEN */}\n {/* =============================================================== */}\n {activeMode === \"swap\" &&\n [\n \"idle\",\n \"choose-swap-asset\",\n \"choose-receive-asset\",\n \"enter-recipient\",\n ].includes(swapStep) && (\n <>\n {\n handleSwapAmountChange(val, panel);\n }}\n onOpenDestPicker={() =>\n openDrawerStep(\"choose-receive-asset\")\n }\n onOpenRecipientPicker={handleOpenRecipientEditor}\n onOpenSourcePicker={(index) => {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n setEditingAssetIndex(index ?? null);\n beginSourcePickerEdit();\n openDrawerStep(\"choose-swap-asset\");\n }}\n onUpdateTokens={handleSwapTokensUpdate}\n receiveQuoteAmount={\n swapType === \"exactIn\" ? idleReceiveQuoteAmount : undefined\n }\n receiveQuoteUsd={idleReceiveQuoteUsd}\n recipientAddress={effectiveRecipientAddress}\n sourceRouteMessage={insufficientSourceIssue?.message}\n sourceRouteStatus={\n insufficientSourceIssue\n ? \"insufficient\"\n : isExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n swapType={swapType}\n toToken={toTokenWithFetchedBalance}\n totalBalance={totalSwapBalanceUsd}\n usdValue={amount && usdValue > 0 ? usdValue.toFixed(2) : \"\"}\n />\n\n {receiveAmountIssue && (\n \n )}\n\n {configuredAmountIssue && (\n \n )}\n\n {txError && !blockingQuoteIssue && (\n \n )}\n\n {/* CTA Button */}\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n void handleEnterPreview();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: blockingQuoteIssue\n ? \"#FCEEED\"\n : isSwapCtaDisabled\n ? theme.colors.surfaceCool\n : primaryButtonBackground,\n border: blockingQuoteIssue ? \"1px solid #F7C4C1\" : \"none\",\n borderRadius: theme.radius.primaryButton,\n boxShadow:\n blockingQuoteIssue || isSwapCtaDisabled\n ? \"none\"\n : theme.shadows.primaryButton,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"7px\",\n height: \"42px\",\n justifyContent: \"center\",\n marginTop: \"8px\",\n paddingInline: \"16px\",\n cursor: isSwapCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {blockingQuoteIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n quoteRefreshing ||\n receiveMaxCalculating ? (\n \n ) : null}\n \n {needsWalletConnection\n ? walletCtaLabel\n : fromTokens.length === 0\n ? \"Add Assets to Bridge\"\n : quoteCtaLabel(\"Review swap\")}\n \n \n \n \n )}\n\n {/* =============================================================== */}\n {/* DEPOSIT MODE LAYOUT */}\n {/* =============================================================== */}\n {activeMode === \"deposit\" &&\n [\n \"idle\",\n \"choose-swap-asset\",\n \"choose-receive-asset\",\n \"enter-recipient\",\n ].includes(swapStep) && (\n <>\n {selectedOpportunity && (\n <>\n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n captureSourceSelectionRequiredUsd();\n beginSourcePickerEdit();\n openDrawerStep(\"choose-swap-asset\");\n }}\n onOpenTokenPicker={() =>\n openDrawerStep(\"choose-receive-asset\")\n }\n onSetPercent={handleDepositPercentSelect}\n routeMessage={insufficientSourceIssue?.message}\n routeStatus={\n insufficientSourceIssue\n ? \"insufficient\"\n : displayExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n showAutoBadge={!sourceSelectionTouched}\n isTokenPickerDisabled={isReceiveAssetPickerLocked}\n hideDestinationTokenDropdownIcon={\n hideDestinationTokenDropdownIcon\n }\n tokenValue={depositTokenDisplay}\n toToken={toTokenWithFetchedBalance}\n totalBalance={totalSwapBalanceUsd}\n usdValue={depositUsdDisplay}\n />\n\n {receiveAmountIssue && (\n \n )}\n\n {configuredAmountIssue && (\n \n )}\n\n {txError && !blockingQuoteIssue && (\n \n )}\n\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n void handleEnterPreview();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: blockingQuoteIssue\n ? \"#FCEEED\"\n : isDepositCtaDisabled\n ? theme.colors.surfaceCool\n : primaryButtonBackground,\n border: blockingQuoteIssue\n ? \"1px solid #F7C4C1\"\n : \"none\",\n borderRadius: blockingQuoteIssue\n ? \"4px\"\n : theme.radius.primaryButton,\n boxShadow:\n blockingQuoteIssue || isDepositCtaDisabled\n ? \"none\"\n : theme.shadows.primaryButton,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"7px\",\n height: \"40px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n cursor: isDepositCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {blockingQuoteIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n isExactOutPaymentQuotePending ||\n receiveMaxCalculating ? (\n \n ) : null}\n \n {quoteCtaLabel(\"Review deposit\")}\n \n \n \n \n )}\n \n )}\n\n {/* =============================================================== */}\n {/* SEND MODE — recipient first, then amount, then asset */}\n {/* =============================================================== */}\n {activeMode === \"send\" &&\n [\n \"idle\",\n \"choose-swap-asset\",\n \"choose-receive-asset\",\n \"enter-recipient\",\n ].includes(swapStep) && (\n <>\n \n openDrawerStep(\"choose-receive-asset\")\n }\n isRecipientLocked={isConfiguredRecipientLocked}\n onOpenRecipientPicker={\n isConfiguredRecipientLocked\n ? () => undefined\n : handleOpenRecipientEditor\n }\n onOpenSourcePicker={() => {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n setEditingAssetIndex(null);\n captureSourceSelectionRequiredUsd();\n beginSourcePickerEdit();\n openDrawerStep(\"choose-swap-asset\");\n }}\n onSetPercent={handleSendPercentSelect}\n recipientAddress={recipientAddress || \"\"}\n routeMessage={insufficientSourceIssue?.message}\n routeStatus={\n insufficientSourceIssue\n ? \"insufficient\"\n : displayExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n showAutoBadge={!sourceSelectionTouched}\n toToken={toTokenWithFetchedBalance}\n totalBalance={totalSwapBalanceUsd}\n usdValue={\n amount && sendAmountUsd > 0 ? sendAmountUsd.toFixed(2) : \"\"\n }\n />\n\n {receiveAmountIssue && (\n \n )}\n\n {configuredAmountIssue && (\n \n )}\n\n {txError && !blockingQuoteIssue && (\n \n )}\n\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n if (sendNeedsRecipient) {\n handleOpenRecipientEditor();\n return;\n }\n void handleEnterPreview();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: blockingQuoteIssue\n ? \"#FCEEED\"\n : isSendCtaDisabled\n ? theme.colors.surfaceCool\n : primaryButtonBackground,\n border: blockingQuoteIssue ? \"1px solid #F7C4C1\" : \"none\",\n borderRadius: blockingQuoteIssue\n ? \"4px\"\n : theme.radius.primaryButton,\n boxShadow:\n blockingQuoteIssue || isSendCtaDisabled\n ? \"none\"\n : theme.shadows.primaryButton,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"7px\",\n height: \"40px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n cursor: isSendCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {blockingQuoteIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n (!sendNeedsRecipient &&\n (isExactOutPaymentQuotePending ||\n receiveMaxCalculating)) ? (\n \n ) : null}\n \n {sendCtaLabel}\n \n \n \n \n )}\n \n \n\n {/* ================================================================== */}\n {/* DRAWER PANELS — rendered as direct children of root widget */}\n {/* so they overlay the main page as bottom drawers */}\n {/* ================================================================== */}\n\n {/* Drawer: enter-recipient */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep === \"enter-recipient\" && (\n \n {\n setTxError(null);\n closeDrawerToIdle();\n }}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n backgroundColor: \"rgba(0,0,0,0.35)\",\n pointerEvents: \"auto\",\n opacity: isRecipientDrawerClosing ? 0 : 1,\n transition: `opacity ${DRAWER_CLOSE_MS}ms ease`,\n }}\n />\n \n \n \n \n \n {\n setTxError(null);\n closeDrawerToIdle();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: theme.colors.surface,\n border: `1px solid ${theme.colors.border}`,\n borderRadius: \"8px\",\n cursor: \"pointer\",\n display: \"flex\",\n flexShrink: 0,\n height: \"32px\",\n justifyContent: \"center\",\n padding: 0,\n width: \"32px\",\n }}\n type=\"button\"\n >\n \n \n \n Recipient\n \n \n \n \n \n Wallet Address\n \n {activeMode === \"swap\" && defaultRecipientAddress && (\n \n Reset to default\n \n )}\n \n {\n setRecipientAddress(next);\n if (txError) setTxError(null);\n }}\n onClear={() => setRecipientAddress(\"\")}\n placeholder=\"Wallet address\"\n value={recipientAddress}\n />\n {txError && (\n \n {txError}\n \n )}\n {activeMode === \"send\" && (\n \n Recipient must be different from the connected wallet.\n \n )}\n \n Save\n \n \n \n )}\n\n {/* Drawer: choose-swap-asset */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep === \"choose-swap-asset\" && (\n \n \n \n {\n if (activeMode === \"swap\") {\n const next = [...fromTokens];\n const targetIndex =\n editingAssetIndex !== null &&\n editingAssetIndex < next.length\n ? editingAssetIndex\n : null;\n const existingToken =\n targetIndex !== null ? next[targetIndex] : undefined;\n const tokenChanged = !isSameTokenSelection(\n existingToken,\n token\n );\n const preservedAmount = tokenChanged\n ? \"\"\n : existingToken?.userAmount ||\n (targetIndex === 0 ? amount : \"\");\n const newToken = {\n ...token,\n userAmount: preservedAmount,\n };\n\n if (targetIndex !== null) {\n next[targetIndex] = newToken;\n } else {\n next.push(newToken);\n }\n\n if (tokenChanged) {\n clearPendingSwapIntent();\n setAmount(getSourceAmountInput(next));\n }\n if (swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n setFromTokens(next);\n closeDrawerToIdle();\n } else if (\n activeMode === \"deposit\" ||\n activeMode === \"send\"\n ) {\n handleSourcePickerDraftSelectionChange([\n { ...token, userAmount: \"\" },\n ]);\n }\n }}\n onSelectionChange={\n activeMode === \"deposit\" || activeMode === \"send\"\n ? handleSourcePickerDraftSelectionChange\n : undefined\n }\n onToggle={(token) => {\n const isDepositOrSendSourcePicker =\n activeMode === \"deposit\" || activeMode === \"send\";\n const prevTokens = isDepositOrSendSourcePicker\n ? sourcePickerSelectedTokens\n : fromTokens;\n const isSameSelection = (\n a: SwapTokenOption,\n b: SwapTokenOption\n ) => {\n if (a.isUnified || b.isUnified) {\n return Boolean(\n a.isUnified &&\n b.isUnified &&\n a.unifiedSymbol === b.unifiedSymbol\n );\n }\n return (\n a.contractAddress.toLowerCase() ===\n b.contractAddress.toLowerCase() && a.chainId === b.chainId\n );\n };\n const sourceTokens = token.sourceTokens ?? [];\n const isSameUnifiedGroup = (item: SwapTokenOption) =>\n Boolean(\n item.isUnified &&\n token.isUnified &&\n item.unifiedSymbol === token.unifiedSymbol\n );\n const withDefaultAmount = (item: SwapTokenOption) => ({\n ...item,\n userAmount:\n activeMode === \"swap\" && prevTokens.length === 0\n ? amount\n : \"\",\n });\n const getNextTokens = () => {\n if (\n isDepositOrSendSourcePicker &&\n token.isUnified &&\n sourceTokens.length > 0\n ) {\n const hasUnifiedSelection =\n prevTokens.some(isSameUnifiedGroup);\n const areAllChildrenSelected = sourceTokens.every(\n (source) =>\n prevTokens.some((item) =>\n isSameSelection(item, source)\n )\n );\n const withoutGroup = prevTokens.filter(\n (item) =>\n !isSameUnifiedGroup(item) &&\n !sourceTokens.some((source) =>\n isSameSelection(item, source)\n )\n );\n\n if (hasUnifiedSelection || areAllChildrenSelected) {\n return withoutGroup;\n }\n\n return [\n ...withoutGroup,\n ...sourceTokens.map((source) =>\n withDefaultAmount(source)\n ),\n ];\n }\n\n if (isDepositOrSendSourcePicker && !token.isUnified) {\n const unifiedSelection = prevTokens.find(\n (item) =>\n item.isUnified &&\n item.sourceTokens?.some((source) =>\n isSameSelection(source, token)\n )\n );\n\n if (unifiedSelection?.sourceTokens?.length) {\n const withoutUnified = prevTokens.filter(\n (item) => !isSameSelection(item, unifiedSelection)\n );\n return [\n ...withoutUnified,\n ...unifiedSelection.sourceTokens\n .filter((source) => !isSameSelection(source, token))\n .map((source) => withDefaultAmount(source)),\n ];\n }\n }\n\n const exists = prevTokens.find((item) =>\n isSameSelection(item, token)\n );\n if (exists) {\n return prevTokens.filter(\n (item) => !isSameSelection(item, token)\n );\n }\n const tokenSourceKeys = new Set(\n (token.sourceTokens ?? []).map(\n (source) =>\n `${source.chainId}-${source.contractAddress.toLowerCase()}`\n )\n );\n const next = prevTokens.filter((existing) => {\n if (\n token.isUnified &&\n tokenSourceKeys.has(\n `${existing.chainId}-${existing.contractAddress.toLowerCase()}`\n )\n ) {\n return false;\n }\n if (\n existing.isUnified &&\n existing.sourceTokens?.some(\n (source) =>\n source.chainId === token.chainId &&\n source.contractAddress.toLowerCase() ===\n token.contractAddress.toLowerCase()\n )\n ) {\n return false;\n }\n return true;\n });\n return [...next, withDefaultAmount(token)];\n };\n const nextTokens = getNextTokens();\n\n if (isDepositOrSendSourcePicker) {\n handleSourcePickerDraftSelectionChange(nextTokens);\n } else {\n clearPendingSwapIntent();\n setFromTokens(nextTokens);\n }\n }}\n preserveSelectedBelowMinimum={false}\n requiredUsd={assetSelectorRequiredUsdDisplay}\n selectedTokens={sourcePickerSelectedTokens}\n showBelowMinimumInline={\n activeMode === \"swap\" && swapType === \"exactIn\"\n }\n swapBalance={swapBalance}\n swapSupportedChains={swapSupportedChainsAndTokens}\n title={\n activeMode === \"deposit\" || activeMode === \"send\"\n ? \"Choose Assets to Pay with\"\n : \"Choose assets to send\"\n }\n />\n \n \n )}\n\n {/* Drawer: choose-receive-asset */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep === \"choose-receive-asset\" && (\n \n \n \n {\n const tokenChanged = !isSameTokenSelection(toToken, token);\n if (tokenChanged) {\n onReceiveAssetChange?.({\n chainId: token.chainId,\n chainName: token.chainName,\n contractAddress: token.contractAddress,\n symbol: token.symbol,\n });\n }\n if (activeMode === \"send\" || activeMode === \"deposit\") {\n setExactOutQuoteSourceModeValue(\"all\");\n if (tokenChanged) {\n clearPendingSwapIntent();\n if (!isConfiguredAmountFixed) {\n setAmount(\"\");\n }\n }\n setSwapType(\"exactOut\");\n if (activeMode === \"deposit\") {\n const nextDeposit = getDepositForTokenSelection(\n configuredDepositOptions,\n token\n );\n if (nextDeposit) {\n setSelectedOpportunity(nextDeposit);\n }\n }\n setToToken(token);\n closeDrawerToIdle();\n return;\n }\n if (tokenChanged) {\n clearPendingSwapIntent();\n }\n if (swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n setToToken(token);\n closeDrawerToIdle();\n }}\n />\n \n \n )}\n \n );\n\n if (embed) return widgetContent;\n\n return (\n \n \n \n \n \n {widgetContent}\n \n \n );\n}\n\nexport default NexusWidget;\n", + "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\"use client\";\n\nimport {\n ERROR_CODES,\n type EthereumProvider,\n} from \"@avail-project/nexus-core\";\nimport Decimal from \"decimal.js\";\nimport { AlertCircle, ArrowLeft, ChevronDown, Loader2 } from \"lucide-react\";\nimport React, {\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { flushSync } from \"react-dom\";\nimport {\n createPublicClient,\n encodeFunctionData,\n erc20Abi,\n http,\n isAddress,\n parseUnits,\n zeroAddress,\n} from \"viem\";\nimport { mainnet } from \"viem/chains\";\nimport { normalize } from \"viem/ens\";\nimport {\n useAccount,\n useConnect,\n useConnectorClient,\n usePublicClient,\n useWalletClient,\n} from \"wagmi\";\nimport { ErrorBoundary } from \"../common/components/ErrorBoundary\";\nimport { useTransactionSteps } from \"../common/tx/useTransactionSteps\";\nimport type {\n BridgeStepType,\n SwapStepType,\n} from \"../common/types/transaction-flow\";\nimport {\n CHAIN_METADATA,\n getShortChainName,\n isSwapSupportedBySdkChainList,\n SUPPORTED_CHAINS,\n TOKEN_CONTRACT_ADDRESSES,\n TOKEN_METADATA,\n} from \"../common/utils/constant\";\nimport { type UserAsset, useNexus } from \"../nexus/NexusProvider\";\nimport { Button } from \"../ui/button\";\nimport { Dialog, DialogContent, DialogTrigger } from \"../ui/dialog\";\nimport { DepositIdleForm } from \"./components/deposit-idle-form\";\nimport {\n type NexusWidgetProgressEvent,\n NexusWidgetProgressScreen,\n} from \"./components/nexus-widget-progress-screen\";\nimport {\n getCachedReceiveTokenMatch,\n preloadReceiveTokens,\n ReceiveAssetSelector,\n} from \"./components/receive-asset-selector\";\nimport { RecipientInput } from \"./components/recipient-input\";\nimport { SendIdleForm } from \"./components/send-idle-form\";\nimport { StatusAlert } from \"./components/status-alerts\";\nimport {\n deriveTokenOptions,\n SwapAssetSelector,\n type SwapTokenOption,\n} from \"./components/swap-asset-selector\";\nimport { SwapIdleForm } from \"./components/swap-idle-form\";\nimport {\n type BridgeProvider,\n type SwapIntentData,\n type SwapIntentDestination,\n type SwapIntentSource,\n SwapIntentPreview,\n} from \"./components/swap-intent-preview\";\nimport {\n NEXUS_WIDGET_DEFAULT_PRIMARY_COLOR,\n NEXUS_WIDGET_FAST_SPINNER_STYLE,\n nexusWidgetTheme,\n} from \"./theme\";\nimport {\n type NexusWidgetAppearance,\n type NexusWidgetConfig,\n type NexusWidgetDepositConfig,\n type NexusWidgetDestination,\n type NexusWidgetDestinationToken,\n type NexusWidgetDepositOpportunityConfig,\n type NexusWidgetDepositOpportunityMetadata,\n type NexusWidgetMode,\n type NexusWidgetPrefillToken,\n type NexusWidgetProps,\n type NexusWidgetRuntimePrefill,\n type SwapType,\n} from \"./types\";\nimport { findCitreaReceiveToken } from \"./utils/citrea-tokens\";\nimport {\n type DepositSourceFilter,\n getDepositSourceId,\n resolveDepositSourceSelection,\n} from \"./utils/deposit-source-selection\";\n\n// ---------------------------------------------------------------------------\n// Types for swap step machine\n// ---------------------------------------------------------------------------\n\ntype SwapStep =\n | \"idle\" // main screen\n | \"choose-swap-asset\" // pick source token\n | \"choose-receive-asset\" // pick receive token\n | \"enter-recipient\" // pick recipient (send mode)\n | \"preview-intent\" // intent preview card\n | \"progress\" // transaction in flight\n | \"success\" // completed seamlessly\n | \"failed\" // failed swap receipt\n | \"history\"; // transaction history\n\ntype SourceFilterTab = \"all\" | \"native\" | \"stables\";\n\ntype SwapHistoryStatus =\n | \"pending\"\n | \"fulfilled\"\n | \"failed\"\n | \"timeout\"\n | \"refund-initiated\";\n\ninterface SwapHistoryEntry {\n autoRefundAvailable?: boolean;\n createdAt: number;\n durationSeconds?: number;\n endedAt?: number;\n error?: string;\n failedStepType?: string;\n failureDescription?: string;\n failureMessage?: string;\n feeUsd?: string;\n finalExplorerUrl?: string | null;\n fromTokens: SwapTokenOption[];\n id: string;\n intentData: SwapIntentData | null;\n intentExplorerUrl?: string | null;\n intentId?: number;\n mode: NexusWidgetMode;\n opportunity?: NexusWidgetDepositOpportunityMetadata;\n recipientAddress?: string;\n requestedToAmount?: string;\n requestedToValue?: string;\n sourceExplorerUrl?: string | null;\n startedAt: number;\n status: SwapHistoryStatus;\n toToken?: SwapTokenOption;\n}\n\ntype HistorySourceRow = {\n amount: string;\n chainId?: number;\n chainLogo?: string;\n chainName: string;\n contractAddress?: string;\n key: string;\n symbol: string;\n tokenLogo?: string;\n value?: unknown;\n};\n\ntype SwapQuoteIssue = {\n type: \"insufficientSources\";\n message: string;\n missingUsd?: string;\n};\n\ntype ReceiveAmountIssue = {\n ctaLabel: string;\n message: string;\n type: \"configuredAmountLimit\" | \"receiveLimitExceeded\" | \"unpricedReceiveToken\";\n};\n\ntype CachedIntentUsdRate = {\n amount: string;\n rate: string;\n updatedAt: number;\n value: string;\n};\n\ntype PredictiveQuote = {\n key: string;\n mode: \"exactIn\" | \"exactOut\";\n sources?: SwapTokenOption[];\n toAmount?: string;\n toUsd?: string;\n};\n\ntype IntentFetchTiming = {\n background: boolean;\n endTimeMs?: number;\n fetchDurationMs?: number;\n operation: string;\n quoteInputKey: string;\n runId: number;\n sourceCount?: number;\n startTimeMs: number;\n walletAddress?: string | null;\n};\n\ntype PredictiveQuoteBaseline = {\n destinationUsdRate: string;\n exactInDestinationAmountPerSourceUsd?: string;\n exactOutSourceUsdPerDestinationUsd?: string;\n updatedAt: number;\n};\n\nconst DESTINATION_RECEIVE_LIMIT_USD_BY_CHAIN_ID: Record = {\n [SUPPORTED_CHAINS.MEGAETH]: 10_000,\n [SUPPORTED_CHAINS.CITREA]: 2000,\n [SUPPORTED_CHAINS.SCROLL]: 500,\n};\n\nconst SCIENTIFIC_DECIMAL_REGEX = /^-?(?:\\d+\\.?\\d*|\\.\\d+)e[+-]?\\d+$/i;\n\nconst QUOTE_REFRESH_INTERVAL_MS = 30000;\nconst EXACT_OUT_INPUT_DEBOUNCE_MS = 1300;\nconst DRAWER_CLOSE_MS = 220;\nconst BALANCE_REFRESH_AFTER_TERMINAL_MS = 3000;\nconst MODAL_HEIGHT_TRANSITION_MS = 220;\nconst ROOT_HEIGHT_TRANSITION_MS = 140;\nconst ASSET_SELECTOR_DRAWER_HEIGHT = \"90%\";\nconst TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT = 620;\nconst CONFIGURED_RECEIVE_SELECTOR_BASE_HEIGHT = 170;\nconst CONFIGURED_RECEIVE_SELECTOR_ROW_HEIGHT = 62;\nconst BASIS_POINTS = 10000;\nconst PREDICTIVE_EXACT_IN_DISCOUNT_BPS = 50;\nconst PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_PCT = 0.1;\nconst PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_MAX_USD = 2;\nconst PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_PCT = 0.02;\nconst PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_MAX_USD = 1;\nconst PREDICTIVE_QUOTE_DISPLAY_DECIMALS = 8;\nconst DEPOSIT_TOKEN_DISPLAY_DECIMALS = 8;\nconst ETHEREUM_MAINNET_CHAIN_ID = 1;\nconst SDK_EXACT_OUT_STABLE_SYMBOLS = new Set([\n \"USDC\",\n \"USDT\",\n \"DAI\",\n \"BUSD\",\n \"TUSD\",\n \"FRAX\",\n \"LUSD\",\n \"USDD\",\n \"USDP\",\n \"GUSD\",\n]);\nconst SWAP_HISTORY_STORAGE_KEY_PREFIX = \"nexus-widget-transaction-history-v1\";\nconst TIMEOUT_LABEL = \"Timed Out\";\nconst PROGRESS_EVENT_NAMES = {\n BRIDGE_PLAN_LIST: \"bridge_plan_list\",\n BRIDGE_PLAN_PROGRESS: \"bridge_plan_progress\",\n SWAP_PLAN_LIST: \"swap_plan_list\",\n SWAP_PLAN_PROGRESS: \"swap_plan_progress\",\n} as const;\nconst PLAN_FINAL_STATES = new Set([\"completed\", \"confirmed\", \"submitted\"]);\nconst PLAN_STEP_FUNDS_MAY_HAVE_MOVED_STATES = new Set([\n \"completed\",\n \"confirmed\",\n \"submitted\",\n \"failed\",\n]);\nconst theme = nexusWidgetTheme;\nconst tooltipSurface = theme.colors.surface;\nconst tooltipText = theme.colors.textStrong;\nconst tooltipBorder = theme.colors.border;\nconst uiFont = theme.fonts.sans;\nconst modalHeightTransitionStyle = {\n interpolateSize: \"allow-keywords\",\n} as React.CSSProperties;\n\nconst getCappedTokenDisplayDecimals = (decimals?: number) => {\n const parsedDecimals = Number(decimals);\n if (!Number.isFinite(parsedDecimals) || parsedDecimals < 0) {\n return DEPOSIT_TOKEN_DISPLAY_DECIMALS;\n }\n return Math.min(Math.floor(parsedDecimals), DEPOSIT_TOKEN_DISPLAY_DECIMALS);\n};\nconst modalHeightTransition = `height ${MODAL_HEIGHT_TRANSITION_MS}ms ease, max-height ${MODAL_HEIGHT_TRANSITION_MS}ms ease`;\n\nconst normalizeSdkExactOutStableSymbol = (symbol?: string) =>\n (symbol ?? \"\")\n .trim()\n .toUpperCase()\n .replaceAll(\"\\u20ae\", \"T\")\n .replaceAll(/[^A-Z0-9]/g, \"\");\n\nconst isSdkExactOutStableSymbol = (symbol?: string) =>\n SDK_EXACT_OUT_STABLE_SYMBOLS.has(normalizeSdkExactOutStableSymbol(symbol));\n\nconst applyPredictiveExactOutBufferUsd = (\n amount: Decimal,\n pct: number,\n maxUsd: number\n) => Decimal.min(amount.mul(pct), new Decimal(maxUsd));\n\nconst getPredictiveExactOutSourceTargetUsd = (\n destinationUsdNeedingSources: Decimal,\n cachedSourceUsdRatio?: Decimal\n) => {\n if (destinationUsdNeedingSources.lte(0)) return new Decimal(0);\n if (cachedSourceUsdRatio?.gt(0)) {\n return destinationUsdNeedingSources.mul(cachedSourceUsdRatio);\n }\n\n const destinationBuffer = applyPredictiveExactOutBufferUsd(\n destinationUsdNeedingSources,\n PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_PCT,\n PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_MAX_USD\n );\n const destinationBuffered = destinationUsdNeedingSources.plus(\n destinationBuffer\n );\n const sourceBuffer = applyPredictiveExactOutBufferUsd(\n destinationBuffered,\n PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_PCT,\n PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_MAX_USD\n );\n\n return destinationBuffered.plus(sourceBuffer);\n};\n\nconst getSwapHistoryStorageKey = (ownerAddress?: string) =>\n `${SWAP_HISTORY_STORAGE_KEY_PREFIX}:${ownerAddress?.toLowerCase() || \"anonymous\"}`;\n\nconst getTokenSelectionKey = (token?: SwapTokenOption | null) => {\n if (!token) return \"\";\n if (token.isUnified) {\n return `unified:${token.unifiedSymbol ?? token.symbol}`;\n }\n return `${token.chainId ?? \"unknown\"}:${token.contractAddress.toLowerCase()}`;\n};\n\nconst getDisplaySourceTokenKey = (token?: SwapTokenOption | null) => {\n if (!token) return \"\";\n const chainId = token.chainId ?? \"unknown\";\n const address = token.contractAddress?.toLowerCase();\n return address\n ? `${chainId}:${address}`\n : `${chainId}:symbol:${token.symbol.toUpperCase()}`;\n};\n\nconst sumDecimalStrings = (left?: string, right?: string) => {\n const leftAmount = parseDecimalLoose(left) ?? new Decimal(0);\n const rightAmount = parseDecimalLoose(right) ?? new Decimal(0);\n const sum = leftAmount.plus(rightAmount);\n return sum.gt(0) ? sum.toFixed() : undefined;\n};\n\nconst mergeDisplaySourceTokens = (tokens: SwapTokenOption[]) => {\n const merged = new Map();\n\n for (const token of tokens) {\n const key = getDisplaySourceTokenKey(token);\n if (!key) continue;\n const existing = merged.get(key);\n if (!existing) {\n merged.set(key, token);\n continue;\n }\n\n const userAmount = sumDecimalStrings(\n existing.userAmount || existing.balance,\n token.userAmount || token.balance\n );\n const userAmountUsd = sumDecimalStrings(\n existing.userAmountUsd || existing.balanceInFiat,\n token.userAmountUsd || token.balanceInFiat\n );\n\n merged.set(key, {\n ...existing,\n balance: userAmount ?? existing.balance,\n balanceInFiat: userAmountUsd ?? existing.balanceInFiat,\n chainLogo: existing.chainLogo || token.chainLogo,\n logo: existing.logo || token.logo,\n userAmount: userAmount ?? existing.userAmount,\n userAmountUsd: userAmountUsd ?? existing.userAmountUsd,\n });\n }\n\n return Array.from(merged.values());\n};\n\nconst getTokenQuoteKey = (token?: SwapTokenOption | null) => {\n if (!token) return \"\";\n return [\n getTokenSelectionKey(token),\n token.symbol ?? \"\",\n token.decimals ?? \"\",\n ].join(\":\");\n};\n\nconst getSourceTokensQuoteKey = (tokens: SwapTokenOption[]) =>\n tokens\n .filter((token) => {\n const amt = token.userAmount ?? \"\";\n const cleaned = amt.replaceAll(/[^0-9.]/g, \"\");\n const num = Number.parseFloat(cleaned);\n return !Number.isNaN(num) && num > 0;\n })\n .map((token) =>\n [\n getTokenSelectionKey(token),\n token.symbol ?? \"\",\n token.decimals ?? \"\",\n token.userAmount ?? \"\",\n token.userAmountUsd ?? \"\",\n token.userAmountMode ?? \"\",\n ].join(\":\")\n )\n .join(\"|\");\n\nconst getSourceTokenSyncKey = (token: SwapTokenOption) =>\n [\n getTokenSelectionKey(token),\n token.userAmount ?? \"\",\n token.userAmountUsd ?? \"\",\n token.balance ?? \"\",\n token.balanceInFiat ?? \"\",\n ].join(\":\");\n\nconst getSourceTokenListSyncKey = (tokens: SwapTokenOption[]) =>\n tokens.map(getSourceTokenSyncKey).join(\"|\");\n\nconst isSameTokenSelection = (\n a?: SwapTokenOption | null,\n b?: SwapTokenOption | null\n) => Boolean(a && b && getTokenSelectionKey(a) === getTokenSelectionKey(b));\n\nconst getDepositConfigIdentity = (deposit?: NexusWidgetDepositOpportunityMetadata | null) => {\n if (!deposit) return \"\";\n return [\n deposit.chainId,\n deposit.tokenAddress.toLowerCase(),\n deposit.tokenSymbol,\n deposit.tokenDecimals,\n deposit.protocol ?? \"\",\n deposit.title ?? \"\",\n ].join(\":\");\n};\n\nconst isSameDepositConfig = (\n a?: NexusWidgetDepositOpportunityConfig | null,\n b?: NexusWidgetDepositOpportunityConfig | null\n) => {\n if (!a || !b) return false;\n return getDepositConfigIdentity(a) === getDepositConfigIdentity(b);\n};\n\nconst getConfiguredReceiveSelectorRootHeight = (tokenCount: number) => {\n if (tokenCount <= 0) return TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT;\n const visibleRows = Math.min(Math.max(tokenCount, 1), 6);\n return Math.min(\n TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT,\n Math.ceil(\n (CONFIGURED_RECEIVE_SELECTOR_BASE_HEIGHT +\n visibleRows * CONFIGURED_RECEIVE_SELECTOR_ROW_HEIGHT) /\n 0.9\n )\n );\n};\n\nconst getConfiguredDeposit = (\n config: RuntimeNexusWidgetConfig\n): NexusWidgetDepositOpportunityConfig | undefined => config.deposit ?? config.deposits?.[0];\n\ntype RuntimeDestinationPair = {\n chain: number;\n decimals?: number;\n logo?: string;\n symbol?: string;\n token: `0x${string}`;\n};\n\ntype RuntimeNexusWidgetAppearance = NexusWidgetAppearance & {\n logoUrl?: string;\n themeMode?: \"auto\" | \"light\" | \"dark\";\n widgetHeading?: string;\n};\n\ntype RuntimeNexusWidgetConfig = {\n allowedDestinationChains?: number[];\n allowedDestinationPairs?: RuntimeDestinationPair[];\n allowedSourcePairs?: RuntimeDestinationPair[];\n appearance?: RuntimeNexusWidgetAppearance;\n deposit?: NexusWidgetDepositOpportunityConfig;\n deposits?: NexusWidgetDepositOpportunityConfig[];\n mode: NexusWidgetMode;\n prefill?: NexusWidgetRuntimePrefill;\n};\n\ntype RuntimeNexusWidgetAmountInput = {\n max?: string;\n min?: string;\n mode: \"fixed\" | \"user\";\n value?: string;\n};\n\ntype NormalizedNexusWidgetConfig = {\n activeMode: NexusWidgetMode;\n amountInput?: RuntimeNexusWidgetAmountInput;\n appearance?: RuntimeNexusWidgetAppearance;\n config: RuntimeNexusWidgetConfig;\n depositOptions: NexusWidgetDepositOpportunityConfig[];\n isAmountFixed: boolean;\n isRecipientLocked: boolean;\n};\n\nconst normalizeConfiguredString = (value: unknown) => {\n if (value === null || value === undefined) return undefined;\n const text = String(value).trim();\n return text || undefined;\n};\n\nconst normalizeConfiguredAddress = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n return text && isAddress(text) ? text : undefined;\n};\n\nconst isPositiveGasLimit = (value: unknown): value is bigint =>\n typeof value === \"bigint\" && value > BigInt(0);\n\nconst normalizePositiveNumericString = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n if (!text) return undefined;\n try {\n const decimal = new Decimal(text);\n return decimal.isFinite() && decimal.gt(0) ? text : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst normalizeNonNegativeNumericString = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n if (!text) return undefined;\n try {\n const decimal = new Decimal(text);\n return decimal.isFinite() && decimal.gte(0) ? text : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst normalizeHttpUrlString = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n if (!text) return undefined;\n return /^https?:\\/\\//i.test(text) ? text : undefined;\n};\n\nconst normalizeNexusWidgetPrimaryColor = (value?: string) => {\n return normalizeConfiguredString(value);\n};\n\nconst expandHexColor = (value: string) => {\n const text = value.trim();\n if (/^#[0-9a-fA-F]{6}$/.test(text)) return text;\n if (/^#[0-9a-fA-F]{3}$/.test(text)) {\n const [r, g, b] = text.slice(1);\n return `#${r}${r}${g}${g}${b}${b}`;\n }\n return undefined;\n};\n\nconst parseNumberComponent = (value: string, max: number) => {\n const text = value.trim();\n if (!text) return undefined;\n if (text.endsWith(\"%\")) {\n const percent = Number.parseFloat(text.slice(0, -1));\n return Number.isFinite(percent)\n ? Math.max(0, Math.min(max, (percent / 100) * max))\n : undefined;\n }\n const parsed = Number.parseFloat(text);\n return Number.isFinite(parsed) ? Math.max(0, Math.min(max, parsed)) : undefined;\n};\n\nconst parseAngleComponent = (value: string) => {\n const parsed = Number.parseFloat(value.trim());\n if (!Number.isFinite(parsed)) return undefined;\n return ((parsed % 360) + 360) % 360;\n};\n\nconst hslToRgb = (h: number, s: number, l: number): [number, number, number] => {\n const saturation = Math.max(0, Math.min(100, s)) / 100;\n const lightness = Math.max(0, Math.min(100, l)) / 100;\n const c = (1 - Math.abs(2 * lightness - 1)) * saturation;\n const hp = h / 60;\n const x = c * (1 - Math.abs((hp % 2) - 1));\n let r1 = 0;\n let g1 = 0;\n let b1 = 0;\n if (hp >= 0 && hp < 1) {\n r1 = c;\n g1 = x;\n } else if (hp >= 1 && hp < 2) {\n r1 = x;\n g1 = c;\n } else if (hp >= 2 && hp < 3) {\n g1 = c;\n b1 = x;\n } else if (hp >= 3 && hp < 4) {\n g1 = x;\n b1 = c;\n } else if (hp >= 4 && hp < 5) {\n r1 = x;\n b1 = c;\n } else {\n r1 = c;\n b1 = x;\n }\n const m = lightness - c / 2;\n return [\n Math.round((r1 + m) * 255),\n Math.round((g1 + m) * 255),\n Math.round((b1 + m) * 255),\n ];\n};\n\nconst parseCssColorToRgb = (\n color: string,\n allowBrowserResolution = true\n): [number, number, number] | undefined => {\n const text = normalizeConfiguredString(color);\n if (!text) return undefined;\n\n const hex = expandHexColor(text);\n if (hex) {\n return [\n Number.parseInt(hex.slice(1, 3), 16),\n Number.parseInt(hex.slice(3, 5), 16),\n Number.parseInt(hex.slice(5, 7), 16),\n ];\n }\n\n const rgbMatch = text.match(/^rgba?\\((.*)\\)$/i);\n if (rgbMatch) {\n const parts = rgbMatch[1].trim().split(/[\\s,\\/]+/).filter(Boolean);\n if (parts.length >= 3) {\n const r = parseNumberComponent(parts[0], 255);\n const g = parseNumberComponent(parts[1], 255);\n const b = parseNumberComponent(parts[2], 255);\n if (r !== undefined && g !== undefined && b !== undefined) {\n return [Math.round(r), Math.round(g), Math.round(b)];\n }\n }\n }\n\n const hslMatch = text.match(/^hsla?\\((.*)\\)$/i);\n if (hslMatch) {\n const parts = hslMatch[1].trim().split(/[\\s,\\/]+/).filter(Boolean);\n if (parts.length >= 3) {\n const h = parseAngleComponent(parts[0]);\n const s = parseNumberComponent(parts[1], 100);\n const l = parseNumberComponent(parts[2], 100);\n if (h !== undefined && s !== undefined && l !== undefined) {\n return hslToRgb(h, s, l);\n }\n }\n }\n\n if (allowBrowserResolution && typeof document !== \"undefined\") {\n const canvas = document.createElement(\"canvas\");\n const context = canvas.getContext(\"2d\");\n if (context) {\n const baseline = \"#010203\";\n context.fillStyle = baseline;\n context.fillStyle = text;\n const resolved = String(context.fillStyle);\n if (resolved !== baseline || text.toLowerCase() === baseline) {\n return parseCssColorToRgb(resolved, false);\n }\n }\n }\n\n return undefined;\n};\n\nconst getReadableTextColor = (background: string) => {\n const rgb = parseCssColorToRgb(background);\n if (!rgb) return \"#FFFFFE\";\n const [rValue, gValue, bValue] = rgb;\n const r = rValue / 255;\n const g = gValue / 255;\n const b = bValue / 255;\n const linear = [r, g, b].map((channel) =>\n channel <= 0.03928\n ? channel / 12.92\n : Math.pow((channel + 0.055) / 1.055, 2.4)\n );\n const luminance = 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];\n return luminance > 0.54 ? \"#161615\" : \"#FFFFFE\";\n};\n\nconst normalizeNexusWidgetAmountInput = (\n config: NexusWidgetConfig\n): RuntimeNexusWidgetAmountInput | undefined => {\n if (config.mode === \"swap\") return undefined;\n const value = normalizePositiveNumericString(\n \"amount\" in (config.prefill ?? {}) ? config.prefill?.amount : undefined\n );\n const min = normalizeNonNegativeNumericString(config.validation?.minAmount);\n const max = normalizePositiveNumericString(config.validation?.maxAmount);\n return value || min || max ? { mode: \"user\", value, min, max } : undefined;\n};\n\nconst normalizeNexusWidgetAppearance = (\n appearance?: NexusWidgetAppearance\n): RuntimeNexusWidgetAppearance | undefined => {\n if (!appearance) return undefined;\n const appName = normalizeConfiguredString(appearance.appName);\n const appLogoURL = normalizeHttpUrlString(appearance.appLogoURL);\n const heading = normalizeConfiguredString(appearance.heading);\n const mode =\n appearance.mode === \"dark\" || appearance.mode === \"light\"\n ? appearance.mode\n : \"system\";\n const primaryColor = normalizeConfiguredString(appearance.primaryColor);\n return {\n ...appearance,\n appLogoURL,\n appName,\n heading,\n logoUrl: appLogoURL,\n mode,\n primaryColor,\n themeMode: mode === \"system\" ? \"auto\" : mode,\n widgetHeading: heading,\n };\n};\n\nconst normalizeConfiguredChainId = (value: unknown) => {\n const chainId = Number(value);\n return Number.isFinite(chainId) && chainId > 0 ? chainId : undefined;\n};\n\nconst getConfiguredDestinationChainId = (\n destination?: NexusWidgetDestination\n) => normalizeConfiguredChainId(destination?.chain);\n\nconst getConfiguredDestinationTokenSymbol = (\n token: NexusWidgetDestinationToken\n) => normalizeConfiguredString(token.symbol);\n\nconst getConfiguredDestinationTokenAddress = (\n token: NexusWidgetDestinationToken\n) => normalizeConfiguredAddress(token.address);\n\nconst toSwapTokenOptionFromConfiguredDestinationToken = (\n token: NexusWidgetDestinationToken,\n chainId: number\n): SwapTokenOption | undefined => {\n const contractAddress = getConfiguredDestinationTokenAddress(token);\n const symbol = getConfiguredDestinationTokenSymbol(token);\n if (!contractAddress || !symbol) return undefined;\n const chainMeta = CHAIN_METADATA[chainId];\n return {\n balance: \"0\",\n balanceInFiat: \"$0.00\",\n chainId,\n chainLogo: chainMeta?.logo,\n chainName: getShortChainName(chainId, chainMeta?.name ?? `Chain ${chainId}`),\n contractAddress,\n decimals: token.decimals ?? 18,\n logo: token.logo ?? \"\",\n name: symbol,\n symbol,\n };\n};\n\nconst toSwapTokenOptionFromDepositConfig = (\n deposit: NexusWidgetDepositOpportunityConfig\n): SwapTokenOption => {\n const chainMeta = CHAIN_METADATA[deposit.chainId];\n return {\n balance: \"0\",\n balanceInFiat: \"$0.00\",\n chainId: deposit.chainId,\n chainLogo: chainMeta?.logo,\n chainName: getShortChainName(\n deposit.chainId,\n chainMeta?.name ?? `Chain ${deposit.chainId}`\n ),\n contractAddress: deposit.tokenAddress,\n decimals: deposit.tokenDecimals,\n logo: deposit.tokenLogo || deposit.logo || \"\",\n name: deposit.tokenSymbol,\n symbol: deposit.tokenSymbol,\n };\n};\n\nconst applyNexusWidgetAppearanceToDeposit = (\n deposit: NexusWidgetDepositOpportunityConfig,\n appearance?: RuntimeNexusWidgetAppearance\n): NexusWidgetDepositOpportunityConfig => {\n const appName = normalizeConfiguredString(appearance?.appName);\n const logoUrl = normalizeConfiguredString(appearance?.logoUrl);\n if (!appName && !logoUrl) return deposit;\n return {\n ...deposit,\n depositTargetLogo: logoUrl ?? deposit.depositTargetLogo,\n logo: logoUrl ?? deposit.logo,\n protocol: appName ?? deposit.protocol,\n title: appName ?? deposit.title,\n };\n};\n\nconst getConfiguredDestinationPairs = (\n destination?: NexusWidgetDestination\n) => {\n const pairs: RuntimeNexusWidgetConfig[\"allowedDestinationPairs\"] = [];\n const chain = getConfiguredDestinationChainId(destination);\n if (!chain) return pairs;\n for (const token of destination?.tokens ?? []) {\n const address = getConfiguredDestinationTokenAddress(token);\n if (!address) continue;\n pairs.push({ chain, token: address });\n }\n return pairs;\n};\n\nconst hasConfiguredDestinationTokenList = (\n destination?: NexusWidgetDestination\n) => Boolean(destination?.tokens?.length);\n\nconst getConfiguredPrefillTokenPair = (\n prefill?: { token?: NexusWidgetPrefillToken }\n): RuntimeDestinationPair | undefined => {\n const token = prefill?.token;\n if (!token) return undefined;\n const chain = normalizeConfiguredChainId(token.chain);\n const address = getConfiguredDestinationTokenAddress(token);\n const symbol = getConfiguredDestinationTokenSymbol(token);\n if (!chain || !address || !symbol) return undefined;\n return {\n chain,\n decimals: token.decimals,\n logo: normalizeConfiguredString(token.logo),\n symbol,\n token: address,\n };\n};\n\nconst getConfiguredDestinationChainIds = (\n destination?: NexusWidgetDestination\n) => {\n const chain = getConfiguredDestinationChainId(destination);\n return chain ? [chain] : [];\n};\n\nconst toDepositConfigFromDestination = (\n config: NexusWidgetDepositConfig,\n token: NexusWidgetDestinationToken,\n appearance?: RuntimeNexusWidgetAppearance\n): NexusWidgetDepositOpportunityConfig | undefined => {\n const chainId = getConfiguredDestinationChainId(config.destination);\n const depositAddress = normalizeConfiguredAddress(config.depositAddress);\n const tokenAddress = getConfiguredDestinationTokenAddress(token);\n const tokenSymbol = getConfiguredDestinationTokenSymbol(token);\n if (\n !chainId ||\n !depositAddress ||\n !tokenAddress ||\n !tokenSymbol ||\n typeof config.executeDeposit !== \"function\"\n ) {\n return undefined;\n }\n\n const chainMeta = CHAIN_METADATA[chainId];\n return {\n chainId,\n depositTargetLogo: appearance?.logoUrl,\n executeDeposit: config.executeDeposit,\n logo: appearance?.logoUrl ?? token.logo ?? undefined,\n protocol:\n appearance?.appName ??\n appearance?.widgetHeading ??\n \"Deposit\",\n subtitle:\n chainMeta?.name\n ? `on ${getShortChainName(chainId, chainMeta.name)}`\n : undefined,\n title: appearance?.appName ?? appearance?.widgetHeading ?? \"Deposit\",\n tokenAddress,\n tokenDecimals: token.decimals ?? 18,\n tokenLogo: token.logo,\n tokenSymbol,\n };\n};\n\nconst getNexusWidgetDepositOptions = (\n config: NexusWidgetConfig,\n appearance?: RuntimeNexusWidgetAppearance\n) => {\n if (config.mode !== \"deposit\") return [];\n return (config.destination.tokens ?? [])\n .map((token) => toDepositConfigFromDestination(config, token, appearance))\n .filter((deposit): deposit is NexusWidgetDepositOpportunityConfig => Boolean(deposit));\n};\n\nconst normalizeNexusWidgetConfig = (\n rawConfig: NexusWidgetConfig\n): NormalizedNexusWidgetConfig => {\n const appearance = normalizeNexusWidgetAppearance(rawConfig.appearance);\n const amountInput = normalizeNexusWidgetAmountInput(rawConfig);\n const prefill: NexusWidgetRuntimePrefill = {};\n let activeMode: NexusWidgetMode =\n rawConfig.mode === \"deposit\" ||\n rawConfig.mode === \"send\" ||\n rawConfig.mode === \"swap\"\n ? rawConfig.mode\n : \"swap\";\n let isRecipientLocked = false;\n const depositOptions = getNexusWidgetDepositOptions(rawConfig, appearance).map(\n (deposit) => applyNexusWidgetAppearanceToDeposit(deposit, appearance)\n );\n let allowedDestinationPairs:\n | RuntimeNexusWidgetConfig[\"allowedDestinationPairs\"]\n | undefined;\n let allowedDestinationChains:\n | RuntimeNexusWidgetConfig[\"allowedDestinationChains\"]\n | undefined;\n\n if (amountInput?.value) {\n prefill.amount = amountInput.value;\n }\n\n if (rawConfig.mode === \"deposit\") {\n activeMode = \"deposit\";\n const pairs = getConfiguredDestinationPairs(rawConfig.destination);\n if (pairs.length > 0) {\n allowedDestinationPairs = pairs;\n prefill.destination = pairs[0];\n }\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (chainIds.length > 0) {\n allowedDestinationChains = chainIds;\n }\n } else if (rawConfig.mode === \"send\") {\n const recipient = normalizeConfiguredAddress(rawConfig.recipientAddress);\n if (recipient) {\n prefill.recipient = recipient;\n isRecipientLocked = true;\n }\n\n const hasDestinationTokens = hasConfiguredDestinationTokenList(\n rawConfig.destination\n );\n const pairs = getConfiguredDestinationPairs(rawConfig.destination);\n if (pairs.length > 0) {\n allowedDestinationPairs = pairs;\n prefill.destination ??= pairs[0];\n } else if (!hasDestinationTokens) {\n const prefillToken = getConfiguredPrefillTokenPair(rawConfig.prefill);\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (\n prefillToken &&\n (!chainIds.length || chainIds.includes(prefillToken.chain))\n ) {\n prefill.destination = prefillToken;\n }\n }\n\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (chainIds.length > 0) {\n allowedDestinationChains = chainIds;\n }\n } else if (rawConfig.mode === \"swap\") {\n const recipient = normalizeConfiguredAddress(rawConfig.recipientAddress);\n if (recipient) {\n prefill.recipient = recipient;\n }\n\n const hasDestinationTokens = hasConfiguredDestinationTokenList(\n rawConfig.destination\n );\n const pairs = getConfiguredDestinationPairs(rawConfig.destination);\n if (pairs.length > 0) {\n allowedDestinationPairs = pairs;\n prefill.destination ??= pairs[0];\n } else if (!hasDestinationTokens) {\n const prefillToken = getConfiguredPrefillTokenPair(rawConfig.prefill);\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (\n prefillToken &&\n (!chainIds.length || chainIds.includes(prefillToken.chain))\n ) {\n prefill.destination = prefillToken;\n }\n }\n\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (chainIds.length > 0) {\n allowedDestinationChains = chainIds;\n }\n }\n\n const runtimeConfig: RuntimeNexusWidgetConfig = {\n allowedDestinationChains,\n allowedDestinationPairs,\n deposit:\n activeMode === \"deposit\"\n ? depositOptions[0]\n : undefined,\n deposits: activeMode === \"deposit\" ? depositOptions : [],\n appearance,\n mode: activeMode,\n prefill,\n };\n\n return {\n activeMode,\n amountInput,\n appearance,\n config: runtimeConfig,\n depositOptions,\n isAmountFixed: false,\n isRecipientLocked,\n };\n};\n\nconst getDepositForTokenSelection = (\n deposits: NexusWidgetDepositOpportunityConfig[],\n token?: SwapTokenOption | null\n) => {\n if (!token) return undefined;\n return deposits.find(\n (deposit) =>\n deposit.chainId === token.chainId &&\n deposit.tokenAddress.toLowerCase() ===\n token.contractAddress.toLowerCase()\n );\n};\n\nconst sanitizeOpportunityForHistory = (\n opportunity?: NexusWidgetDepositOpportunityMetadata\n): NexusWidgetDepositOpportunityMetadata | undefined => {\n if (!opportunity) return undefined;\n return {\n label: opportunity.label,\n protocol: opportunity.protocol,\n logo: opportunity.logo,\n title: opportunity.title,\n subtitle: opportunity.subtitle,\n chainId: opportunity.chainId,\n tokenSymbol: opportunity.tokenSymbol,\n tokenDecimals: opportunity.tokenDecimals,\n tokenLogo: opportunity.tokenLogo,\n tokenAddress: opportunity.tokenAddress,\n apy: opportunity.apy,\n description: opportunity.description,\n };\n};\n\nconst sanitizeHistoryEntry = (entry: SwapHistoryEntry): SwapHistoryEntry => ({\n ...entry,\n createdAt: entry.createdAt ?? entry.startedAt ?? Date.now(),\n failureMessage:\n entry.status === \"timeout\" ? TIMEOUT_LABEL : entry.failureMessage,\n opportunity: sanitizeOpportunityForHistory(entry.opportunity),\n});\n\nconst sortSwapHistoryEntries = (entries: SwapHistoryEntry[]) =>\n [...entries].sort(\n (a, b) =>\n (b.createdAt ?? b.startedAt ?? 0) - (a.createdAt ?? a.startedAt ?? 0)\n );\n\nconst isStoredHistoryStatus = (value: unknown): value is SwapHistoryStatus =>\n value === \"pending\" ||\n value === \"fulfilled\" ||\n value === \"failed\" ||\n value === \"timeout\" ||\n value === \"refund-initiated\";\n\nconst isStoredMode = (value: unknown): value is NexusWidgetMode =>\n value === \"swap\" || value === \"deposit\" || value === \"send\";\n\nconst normalizeStoredHistoryEntry = (\n value: unknown\n): SwapHistoryEntry | null => {\n if (!value || typeof value !== \"object\") return null;\n const entry = value as Partial;\n const startedAt =\n typeof entry.startedAt === \"number\" && Number.isFinite(entry.startedAt)\n ? entry.startedAt\n : undefined;\n const createdAt =\n typeof entry.createdAt === \"number\" && Number.isFinite(entry.createdAt)\n ? entry.createdAt\n : startedAt;\n\n if (\n !entry.id ||\n typeof entry.id !== \"string\" ||\n !isStoredMode(entry.mode) ||\n !isStoredHistoryStatus(entry.status) ||\n !createdAt ||\n !startedAt\n ) {\n return null;\n }\n\n return {\n ...entry,\n id: entry.id,\n mode: entry.mode,\n status: entry.status,\n createdAt,\n startedAt,\n failureMessage:\n entry.status === \"timeout\" ? TIMEOUT_LABEL : entry.failureMessage,\n intentData: entry.intentData ?? null,\n fromTokens: Array.isArray(entry.fromTokens) ? entry.fromTokens : [],\n opportunity: sanitizeOpportunityForHistory(entry.opportunity),\n } as SwapHistoryEntry;\n};\n\nconst readSwapHistoryFromStorage = (storageKey: string): SwapHistoryEntry[] => {\n if (typeof window === \"undefined\") return [];\n\n try {\n const raw = window.localStorage.getItem(storageKey);\n if (!raw) return [];\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n return sortSwapHistoryEntries(\n parsed\n .map(normalizeStoredHistoryEntry)\n .filter((entry): entry is SwapHistoryEntry => Boolean(entry))\n );\n } catch {\n return [];\n }\n};\n\nconst writeSwapHistoryToStorage = (\n storageKey: string,\n entries: SwapHistoryEntry[]\n) => {\n if (typeof window === \"undefined\") return;\n\n try {\n const persistableEntries =\n sortSwapHistoryEntries(entries).map(sanitizeHistoryEntry);\n window.localStorage.setItem(\n storageKey,\n JSON.stringify(persistableEntries, (_key, value) =>\n typeof value === \"bigint\" ? value.toString() : value\n )\n );\n } catch {\n // localStorage can be unavailable or full; in-memory history still works.\n }\n};\n\nfunction QuoteRefreshCountdown({\n progress,\n isRefreshing,\n secondsRemaining,\n}: {\n progress: number;\n isRefreshing: boolean;\n secondsRemaining: number;\n}) {\n const [showTooltip, setShowTooltip] = useState(false);\n const radius = 7;\n const circumference = 2 * Math.PI * radius;\n const clampedProgress = Math.max(0, Math.min(1, progress));\n const tooltipLabel = isRefreshing\n ? \"Refreshing quotes...\"\n : `Refreshing quotes in ${Math.max(0, secondsRemaining)} second${\n secondsRemaining === 1 ? \"\" : \"s\"\n }`;\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n style={{\n alignItems: \"center\",\n backgroundColor: \"#FFFFFE\",\n borderRadius: \"999px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n height: \"22px\",\n justifyContent: \"center\",\n outline: \"1px solid #E8E8E7\",\n position: \"relative\",\n width: \"22px\",\n }}\n tabIndex={0}\n >\n {showTooltip && (\n \n {tooltipLabel}\n \n )}\n \n \n \n \n \n );\n}\n\nconst normalizeDecimalInputText = (value: unknown) => {\n const raw = String(value).trim();\n if (!raw) return \"\";\n if (SCIENTIFIC_DECIMAL_REGEX.test(raw)) return raw;\n return raw.replace(/[^0-9.-]/g, \"\");\n};\n\nconst parseDecimalLoose = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = normalizeDecimalInputText(value);\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst toViemDecimalString = (value: unknown, decimals: number) => {\n const parsed = parseDecimalLoose(value);\n if (!parsed || parsed.lte(0)) return \"0\";\n return parsed\n .toDecimalPlaces(Math.max(0, decimals), Decimal.ROUND_DOWN)\n .toFixed();\n};\n\nconst formatDecimalDisplay = (\n value: unknown,\n options: { min?: number; max?: number } = {}\n) => {\n const amount = parseDecimalLoose(value) ?? new Decimal(0);\n const max = options.max ?? 2;\n return amount.toDecimalPlaces(max).toFixed();\n};\n\nconst formatUsdDisplay = (value: unknown) => {\n const amount = parseDecimalLoose(value) ?? new Decimal(0);\n if (amount.gt(0) && amount.lt(0.01)) return \"<$0.01\";\n return `$${formatDecimalDisplay(amount, { min: 2, max: 2 })}`;\n};\n\nconst formatTokenDisplay = (value: unknown) => {\n const amount = parseDecimalLoose(value) ?? new Decimal(0);\n return formatDecimalDisplay(amount, { max: 8 });\n};\n\nconst getSwapTokenUsdValue = (token: SwapTokenOption) =>\n parseDecimalLoose(token.userAmountUsd) ??\n parseDecimalLoose(token.balanceInFiat) ??\n new Decimal(0);\n\nconst getSwapTokenBalanceUsdValue = (token: SwapTokenOption) =>\n parseDecimalLoose(token.balanceInFiat) ?? new Decimal(0);\n\nconst sortSwapTokensByUsdDesc = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const usdDelta = getSwapTokenUsdValue(b).cmp(getSwapTokenUsdValue(a));\n if (usdDelta !== 0) return usdDelta;\n return (a.symbol ?? \"\").localeCompare(b.symbol ?? \"\");\n });\n\nconst sortDisplaySourcesByBalanceUsdDesc = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const balanceUsdDelta = getSwapTokenBalanceUsdValue(b).cmp(\n getSwapTokenBalanceUsdValue(a)\n );\n if (balanceUsdDelta !== 0) return balanceUsdDelta;\n\n const spendUsdDelta = getSwapTokenUsdValue(b).cmp(getSwapTokenUsdValue(a));\n if (spendUsdDelta !== 0) return spendUsdDelta;\n\n return `${a.symbol ?? \"\"} ${a.chainName ?? \"\"}`.localeCompare(\n `${b.symbol ?? \"\"} ${b.chainName ?? \"\"}`\n );\n });\n\nconst getIntentSourceUsdValue = (source: SwapIntentData[\"sources\"][number]) =>\n parseDecimalLoose(source.value) ?? new Decimal(0);\n\nconst sortIntentSourcesByUsdDesc = (sources: SwapIntentData[\"sources\"]) =>\n [...sources].sort((a, b) => {\n const usdDelta = getIntentSourceUsdValue(b).cmp(getIntentSourceUsdValue(a));\n if (usdDelta !== 0) return usdDelta;\n return (a.token?.symbol ?? \"\").localeCompare(b.token?.symbol ?? \"\");\n });\n\nconst extractIntentIdFromUrl = (url?: string | null) => {\n if (!url) return undefined;\n const match = url.match(/(\\d+)(?:\\/)?$/);\n if (!match) return undefined;\n const parsed = Number(match[1]);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;\n};\n\nconst getNonEmptyString = (...values: unknown[]) => {\n for (const value of values) {\n if (typeof value !== \"string\") continue;\n const trimmed = value.trim();\n if (trimmed.length > 0) return trimmed;\n }\n return null;\n};\n\nconst isHttpUrl = (value?: string | null): value is string =>\n Boolean(value && /^https?:\\/\\//i.test(value));\n\nconst hasValidIntentExplorer = (\n entry: Pick\n) => isHttpUrl(entry.intentExplorerUrl);\n\nconst getHistoryExplorerUrl = (\n entry: Pick<\n SwapHistoryEntry,\n \"finalExplorerUrl\" | \"intentExplorerUrl\" | \"sourceExplorerUrl\"\n >\n) =>\n [\n entry.intentExplorerUrl,\n entry.finalExplorerUrl,\n entry.sourceExplorerUrl,\n ].find(isHttpUrl) ?? null;\n\nconst getFiniteNumber = (...values: unknown[]) => {\n for (const value of values) {\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value !== \"string\") continue;\n const parsed = Number(value);\n if (Number.isFinite(parsed)) return parsed;\n }\n return undefined;\n};\n\nconst getObjectChainId = (value: any) =>\n getFiniteNumber(\n value?.chainId,\n value?.chain?.id,\n value?.chain?.chainId,\n value?.toChainId,\n value?.destinationChainId,\n value?.data?.chainId,\n value?.data?.chain?.id,\n value?.data?.chain?.chainId,\n value?.data?.toChainId,\n value?.data?.destinationChainId,\n value?.result?.chainId,\n value?.result?.chain?.id\n );\n\nconst getExplorerBaseUrl = (chainId?: number, ...candidates: unknown[]) => {\n const directCandidates = candidates.flatMap((candidate: any) => [\n candidate?.blockExplorerUrl,\n candidate?.blockExplorerURL,\n candidate?.chainBlockExplorerUrl,\n candidate?.explorerBaseUrl,\n candidate?.explorerUrlBase,\n candidate?.blockExplorerUrls?.[0],\n candidate?.blockExplorers?.default?.url,\n candidate?.chain?.blockExplorerUrl,\n candidate?.chain?.blockExplorerURL,\n candidate?.chain?.chainBlockExplorerUrl,\n candidate?.chain?.explorerBaseUrl,\n candidate?.chain?.explorerUrlBase,\n candidate?.chain?.blockExplorerUrls?.[0],\n candidate?.chain?.blockExplorers?.default?.url,\n candidate?.data?.blockExplorerUrl,\n candidate?.data?.blockExplorerURL,\n candidate?.data?.chainBlockExplorerUrl,\n candidate?.data?.blockExplorerUrls?.[0],\n candidate?.data?.blockExplorers?.default?.url,\n candidate?.data?.chain?.blockExplorerUrl,\n candidate?.data?.chain?.blockExplorerURL,\n candidate?.data?.chain?.chainBlockExplorerUrl,\n candidate?.data?.chain?.blockExplorerUrls?.[0],\n candidate?.data?.chain?.blockExplorers?.default?.url,\n ]);\n return getNonEmptyString(\n ...directCandidates,\n chainId ? CHAIN_METADATA[chainId]?.blockExplorerUrls?.[0] : undefined,\n chainId\n ? (CHAIN_METADATA[chainId] as any)?.blockExplorers?.default?.url\n : undefined\n );\n};\n\nconst getTransactionHash = (...values: unknown[]) => {\n for (const value of values) {\n if (typeof value !== \"string\") continue;\n const trimmed = value.trim();\n if (/^0x[a-fA-F0-9]{64}$/.test(trimmed)) return trimmed;\n }\n return null;\n};\n\nconst getIntentHash = (...values: unknown[]) => getTransactionHash(...values);\n\nconst getObjectIntentHash = (value: any) =>\n getIntentHash(\n value?.intentHash,\n value?.intent_hash,\n value?.intent?.hash,\n value?.intent?.intentHash,\n value?.intent?.intent_hash,\n value?.requestHash,\n value?.request_hash,\n value?.request?.hash,\n value?.request?.requestHash,\n value?.rffHash,\n value?.rff_hash,\n value?.rff?.hash,\n value?.data?.intentHash,\n value?.data?.intent_hash,\n value?.data?.intent?.hash,\n value?.data?.intent?.intentHash,\n value?.data?.intent?.intent_hash,\n value?.data?.requestHash,\n value?.data?.request_hash,\n value?.data?.request?.hash,\n value?.data?.request?.requestHash,\n value?.data?.rffHash,\n value?.data?.rff_hash,\n value?.data?.rff?.hash,\n value?.result?.intentHash,\n value?.result?.intent_hash,\n value?.result?.intent?.hash,\n value?.result?.requestHash,\n value?.result?.request_hash,\n value?.result?.rffHash,\n value?.result?.rff_hash\n );\n\nconst getNexusExplorerNetwork = (network?: unknown) => {\n const normalized =\n typeof network === \"string\" ? network.trim().toLowerCase() : \"\";\n if (normalized === \"canary\" || normalized === \"testnet\") return normalized;\n return \"mainnet\";\n};\n\nconst getRffExplorerUrl = (network: unknown, intentHash?: string | null) =>\n intentHash\n ? `https://nexus-v2.${getNexusExplorerNetwork(network)}.avail.so/rff/${intentHash}`\n : null;\n\nconst getObjectTransactionHash = (value: any) =>\n getTransactionHash(\n value?.txHash,\n value?.transactionHash,\n value?.executeTxHash,\n value?.executeTransactionHash,\n value?.transferTransactionHash,\n value?.receipt?.transactionHash,\n value?.tx?.hash,\n value?.transaction?.hash,\n value?.data?.txHash,\n value?.data?.transactionHash,\n value?.data?.executeTxHash,\n value?.data?.executeTransactionHash,\n value?.data?.transferTransactionHash,\n value?.data?.receipt?.transactionHash,\n value?.data?.tx?.hash,\n value?.data?.transaction?.hash,\n value?.result?.txHash,\n value?.result?.transactionHash,\n value?.result?.receipt?.transactionHash\n );\n\nconst getExplorerTxUrl = (\n chainId?: number,\n txHash?: string | null,\n ...candidates: unknown[]\n) => {\n if (!chainId || !txHash) return null;\n const baseUrl = getExplorerBaseUrl(chainId, ...candidates);\n return baseUrl ? `${String(baseUrl).replace(/\\/$/, \"\")}/tx/${txHash}` : null;\n};\n\nconst getSdkSwapResult = (result: any) => {\n const candidate = result?.swapResult ?? result?.result;\n return candidate && typeof candidate === \"object\" ? candidate : null;\n};\n\nconst getSdkTransactionHash = (result: any) =>\n getObjectTransactionHash(result) ||\n getObjectTransactionHash(result?.executeResponse) ||\n getObjectTransactionHash(result?.execute) ||\n getObjectTransactionHash(result?.transfer) ||\n getObjectTransactionHash(result?.swapResult) ||\n getObjectTransactionHash(result?.result) ||\n null;\n\nconst getSdkExplorerUrl = (result: any) =>\n getNonEmptyString(\n result?.explorerUrl,\n result?.explorerURL,\n result?.txExplorerUrl,\n result?.transactionExplorerUrl,\n result?.execute?.explorerUrl,\n result?.execute?.explorerURL,\n result?.execute?.txExplorerUrl,\n result?.execute?.transactionExplorerUrl,\n result?.executeResponse?.explorerUrl,\n result?.executeResponse?.explorerURL,\n result?.executeResponse?.txExplorerUrl,\n result?.executeResponse?.transactionExplorerUrl,\n result?.executeExplorerUrl,\n result?.transferExplorerUrl,\n result?.swapResult?.explorerUrl,\n result?.swapResult?.explorerURL,\n result?.swapResult?.txExplorerUrl,\n result?.swapResult?.transactionExplorerUrl,\n result?.result?.explorerUrl,\n result?.result?.explorerURL,\n result?.result?.txExplorerUrl,\n result?.result?.transactionExplorerUrl\n );\n\nconst getSdkIntentExplorerUrl = (result: any, swapResult?: any) =>\n getNonEmptyString(\n swapResult?.intentExplorerUrl,\n swapResult?.intentExplorerURL,\n swapResult?.intentUrl,\n swapResult?.intentURL,\n swapResult?.rffUrl,\n swapResult?.rffURL,\n swapResult?.rffExplorerUrl,\n swapResult?.rffExplorerURL,\n swapResult?.explorerUrl,\n swapResult?.explorerURL,\n result?.intentExplorerUrl,\n result?.intentExplorerURL,\n result?.intentUrl,\n result?.intentURL,\n result?.rffUrl,\n result?.rffURL,\n result?.rffExplorerUrl,\n result?.rffExplorerURL,\n result?.swapResult?.intentExplorerUrl,\n result?.swapResult?.intentExplorerURL,\n result?.swapResult?.intentUrl,\n result?.swapResult?.intentURL,\n result?.swapResult?.rffUrl,\n result?.swapResult?.rffURL,\n result?.swapResult?.rffExplorerUrl,\n result?.swapResult?.rffExplorerURL,\n result?.swapResult?.explorerUrl,\n result?.swapResult?.explorerURL,\n result?.result?.intentExplorerUrl,\n result?.result?.intentExplorerURL,\n result?.result?.intentUrl,\n result?.result?.intentURL,\n result?.result?.rffUrl,\n result?.result?.rffURL,\n result?.result?.rffExplorerUrl,\n result?.result?.rffExplorerURL,\n result?.result?.explorerUrl,\n result?.result?.explorerURL\n );\n\nconst getSdkIntentExplorerUrlForNetwork = (\n network: unknown,\n result: any,\n swapResult?: any\n) =>\n getSdkIntentExplorerUrl(result, swapResult) ||\n getRffExplorerUrl(\n network,\n getObjectIntentHash(swapResult) || getObjectIntentHash(result)\n );\n\nfunction MiniLogo({\n src,\n label,\n size = 30,\n fontSize = 13,\n outline,\n style,\n}: {\n src?: string;\n label?: string;\n size?: number;\n fontSize?: number;\n outline?: string;\n style?: React.CSSProperties;\n}) {\n const [failed, setFailed] = useState(!src);\n\n useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n background: \"#FFFFFE\",\n borderRadius: \"999px\",\n height: size,\n objectFit: \"cover\",\n outline,\n width: size,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {(label || \"?\").trim().slice(0, 1).toUpperCase()}\n \n );\n}\n\nfunction TokenLogoPair({\n tokenLogo,\n chainLogo,\n tokenSymbol,\n chainName,\n tokenOutline,\n size = 34,\n}: {\n tokenLogo?: string;\n chainLogo?: string;\n tokenSymbol?: string;\n chainName?: string;\n tokenOutline?: string;\n size?: number;\n}) {\n return (\n \n \n {chainLogo && (\n \n )}\n \n );\n}\n\nfunction SourceLogoStack({\n sources,\n size = 24,\n maxVisible = 3,\n}: {\n sources: HistorySourceRow[];\n size?: number;\n maxVisible?: number;\n}) {\n const visibleSources = sources.slice(0, maxVisible);\n const hiddenCount = Math.max(0, sources.length - visibleSources.length);\n\n return (\n \n {visibleSources.map((source, index) => (\n \n \n \n ))}\n {hiddenCount > 0 && (\n \n +{hiddenCount}\n \n )}\n \n );\n}\n\nfunction TruncatedAddress({\n address,\n color = \"var(--foreground-brand)\",\n}: {\n address: string;\n color?: string;\n}) {\n const [showTooltip, setShowTooltip] = useState(false);\n const label =\n address.length > 12\n ? `${address.slice(0, 6)}...${address.slice(-4)}`\n : address;\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n style={{\n color,\n display: \"inline-flex\",\n fontFamily: uiFont,\n fontSize: \"15px\",\n fontWeight: 500,\n lineHeight: \"20px\",\n outline: \"none\",\n position: \"relative\",\n }}\n tabIndex={0}\n >\n {label}\n {showTooltip && (\n \n {address}\n \n )}\n \n );\n}\n\nconst getDisplayDestinationSourceRow = (\n entry: SwapHistoryEntry\n): HistorySourceRow | null => {\n if (entry.mode !== \"deposit\" && entry.mode !== \"send\") return null;\n if (!entry.toToken || !entry.requestedToAmount) return null;\n\n const requestedAmount = parseDecimalLoose(entry.requestedToAmount);\n const intentDestinationAmount = parseDecimalLoose(\n entry.intentData?.destination.amount\n );\n const destinationBalanceAmount = parseDecimalLoose(\n entry.toToken.balance?.replace(entry.toToken.symbol, \"\")\n );\n if (\n !requestedAmount ||\n !destinationBalanceAmount ||\n requestedAmount.lte(0) ||\n destinationBalanceAmount.lte(0)\n ) {\n return null;\n }\n\n const intentCoversAmount = intentDestinationAmount ?? new Decimal(0);\n const displayAmount = Decimal.min(\n destinationBalanceAmount,\n Decimal.max(0, requestedAmount.minus(intentCoversAmount))\n );\n if (displayAmount.lte(0)) return null;\n\n const requestedValue = parseDecimalLoose(entry.requestedToValue);\n const destinationValue = parseDecimalLoose(\n entry.intentData?.destination.value\n );\n const rate =\n requestedValue && requestedAmount.gt(0)\n ? requestedValue.div(requestedAmount)\n : destinationValue && intentCoversAmount.gt(0)\n ? destinationValue.div(intentCoversAmount)\n : undefined;\n\n return {\n key: `destination-balance-${entry.toToken.chainId}-${entry.toToken.contractAddress}`,\n chainId: entry.toToken.chainId,\n contractAddress: entry.toToken.contractAddress,\n tokenLogo: entry.intentData?.destination.token.logo || entry.toToken.logo,\n chainLogo: entry.toToken.chainLogo,\n symbol: entry.toToken.symbol,\n chainName: getShortChainName(\n entry.toToken.chainId,\n entry.toToken.chainName\n ),\n amount: displayAmount\n .toDecimalPlaces(\n Math.max(0, entry.toToken.decimals ?? 18),\n Decimal.ROUND_DOWN\n )\n .toFixed(),\n value: rate\n ? displayAmount.mul(rate).toFixed()\n : entry.toToken.balanceInFiat,\n };\n};\n\nconst getHistorySourceRowMergeKey = (row: HistorySourceRow) => {\n const chainKey = row.chainId ?? row.chainName;\n const address = row.contractAddress?.toLowerCase();\n return address\n ? `${chainKey}:${address}`\n : `${chainKey}:symbol:${row.symbol.toUpperCase()}`;\n};\n\nconst mergeHistorySourceRows = (rows: HistorySourceRow[]) => {\n const merged = new Map();\n\n for (const row of rows) {\n const key = getHistorySourceRowMergeKey(row);\n const existing = merged.get(key);\n if (!existing) {\n merged.set(key, row);\n continue;\n }\n\n merged.set(key, {\n ...existing,\n amount: sumDecimalStrings(existing.amount, row.amount) ?? existing.amount,\n chainLogo: existing.chainLogo || row.chainLogo,\n key: existing.key,\n tokenLogo: existing.tokenLogo || row.tokenLogo,\n value: sumDecimalStrings(\n String(existing.value ?? \"\"),\n String(row.value ?? \"\")\n ),\n });\n }\n\n return Array.from(merged.values());\n};\n\nconst getProgressStepType = (step?: SwapStepType | BridgeStepType | null) =>\n String((step as any)?.type ?? (step as any)?.typeID ?? \"\").toUpperCase();\n\nconst isSourceMovementProgressStep = (type: string, rawStepType: string) =>\n rawStepType === \"source_swap\" ||\n rawStepType === \"eoa_to_ephemeral_transfer\" ||\n rawStepType === \"bridge_deposit\" ||\n rawStepType === \"vault_deposit\" ||\n type.includes(\"SOURCE_SWAP\") ||\n type.includes(\"EOA_TO_EPHEMERAL_TRANSFER\") ||\n type.includes(\"BRIDGE_DEPOSIT\") ||\n type.includes(\"VAULT_DEPOSIT\");\n\nconst isBridgeRefundStepType = (type: string) =>\n type.includes(\"BRIDGE_INTENT_SUBMISSION\") || type.includes(\"BRIDGE_DEPOSIT\");\n\nconst isSwapSkippedStepType = (type: string) => type.includes(\"SWAP_SKIPPED\");\n\nconst isAutoRefundAvailableProgressEvent = (event?: NexusWidgetProgressEvent) =>\n event?.name === PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS &&\n isBridgeRefundStepType(getProgressStepType(event.step));\n\nconst normalizeBridgeProvider = (\n value: unknown\n): BridgeProvider | undefined => {\n if (value === \"nexus\" || value === \"mayan\" || value === null) {\n return value;\n }\n return undefined;\n};\n\nconst normalizePlanStepType = (stepType: unknown, state?: unknown) => {\n const normalized = String(stepType ?? \"\").toLowerCase();\n const normalizedState = String(state ?? \"\").toLowerCase();\n\n if (normalized === \"execute_transaction\") {\n return normalizedState === \"confirmed\" || normalizedState === \"completed\"\n ? \"TRANSACTION_CONFIRMED\"\n : \"TRANSACTION_SENT\";\n }\n\n const mapped: Record = {\n allowance_approval: \"APPROVAL\",\n bridge_deposit: \"BRIDGE_DEPOSIT\",\n bridge_fill: \"BRIDGE_FILL\",\n bridge_intent_submission: \"BRIDGE_INTENT_SUBMISSION\",\n destination_swap: \"DESTINATION_SWAP\",\n eoa_to_ephemeral_transfer: \"EOA_TO_EPHEMERAL_TRANSFER\",\n execute_approval: \"APPROVAL\",\n request_signing: \"REQUEST_SIGNING\",\n request_submission: \"REQUEST_SUBMISSION\",\n source_swap: \"SOURCE_SWAP\",\n vault_deposit: \"BRIDGE_DEPOSIT\",\n };\n\n return mapped[normalized] ?? normalized.toUpperCase();\n};\n\nconst normalizePlanStep = (\n stepLike: unknown,\n fallbackStepType?: unknown,\n state?: unknown,\n completed?: boolean\n): SwapStepType | BridgeStepType => {\n const source =\n stepLike && typeof stepLike === \"object\" ? (stepLike as any) : {};\n const rawStepType = fallbackStepType ?? source.stepType ?? source.type;\n const progressType = normalizePlanStepType(\n rawStepType ?? source.typeID,\n state\n );\n const progressKey =\n source.id ?? source.stepId ?? source.typeID ?? progressType;\n\n return {\n ...source,\n completed,\n rawType: rawStepType,\n type: progressType,\n typeID: String(progressKey),\n } as SwapStepType | BridgeStepType;\n};\n\nconst getPlanStepChainId = (event: any, step: any) =>\n getObjectChainId(event) ?? getObjectChainId(step);\n\nconst getPlanStepTransactionHash = (event: any, step: any) =>\n getObjectTransactionHash(event) ?? getObjectTransactionHash(step);\n\nconst getPlanStepExplorerUrl = (event: any, step: any) => {\n const directExplorerUrl = getNonEmptyString(\n event?.explorerUrl,\n event?.explorerURL,\n event?.txExplorerUrl,\n event?.transactionExplorerUrl,\n step?.explorerUrl,\n step?.explorerURL,\n step?.txExplorerUrl,\n step?.transactionExplorerUrl,\n step?.data?.explorerUrl,\n step?.data?.explorerURL,\n step?.data?.txExplorerUrl,\n step?.data?.transactionExplorerUrl\n );\n if (directExplorerUrl) return directExplorerUrl;\n\n return getExplorerTxUrl(\n getPlanStepChainId(event, step),\n getPlanStepTransactionHash(event, step),\n event,\n step\n );\n};\n\nconst getPlanStepIntentExplorerUrl = (event: any, step: any) =>\n getNonEmptyString(\n event?.intentExplorerUrl,\n event?.intentExplorerURL,\n event?.intentUrl,\n event?.intentURL,\n event?.rffUrl,\n event?.rffURL,\n event?.rffExplorerUrl,\n event?.rffExplorerURL,\n step?.intentExplorerUrl,\n step?.intentExplorerURL,\n step?.intentUrl,\n step?.intentURL,\n step?.rffUrl,\n step?.rffURL,\n step?.rffExplorerUrl,\n step?.rffExplorerURL,\n step?.data?.intentExplorerUrl,\n step?.data?.intentExplorerURL,\n step?.data?.intentUrl,\n step?.data?.intentURL,\n step?.data?.rffUrl,\n step?.data?.rffURL,\n step?.data?.rffExplorerUrl,\n step?.data?.rffExplorerURL\n );\n\nconst isIntentSubmissionLikeEvent = (event: any, step?: any) => {\n const text = [\n event?.type,\n event?.event,\n event?.name,\n event?.status,\n event?.stepType,\n step?.type,\n step?.typeID,\n step?.rawType,\n ]\n .filter((value) => value !== null && value !== undefined)\n .join(\" \")\n .toLowerCase();\n\n return (\n text.includes(\"intent\") ||\n text.includes(\"request_submission\") ||\n text.includes(\"rff\")\n );\n};\n\nconst getGenericEventHash = (event: any, step?: any) =>\n getTransactionHash(\n event?.hash,\n event?.data?.hash,\n event?.result?.hash,\n step?.hash,\n step?.data?.hash,\n step?.result?.hash\n );\n\nconst getEventIntentExplorerUrl = (\n network: unknown,\n event: any,\n step?: any\n) => {\n const directUrl = step\n ? getPlanStepIntentExplorerUrl(event, step)\n : getNonEmptyString(\n event?.intentExplorerUrl,\n event?.intentExplorerURL,\n event?.intentUrl,\n event?.intentURL,\n event?.rffUrl,\n event?.rffURL,\n event?.rffExplorerUrl,\n event?.rffExplorerURL,\n event?.data?.intentExplorerUrl,\n event?.data?.intentExplorerURL,\n event?.data?.intentUrl,\n event?.data?.intentURL,\n event?.data?.rffUrl,\n event?.data?.rffURL,\n event?.data?.rffExplorerUrl,\n event?.data?.rffExplorerURL\n );\n if (directUrl) return directUrl;\n\n return getRffExplorerUrl(\n network,\n getObjectIntentHash(event) ||\n getObjectIntentHash(step) ||\n (isIntentSubmissionLikeEvent(event, step)\n ? getGenericEventHash(event, step)\n : null)\n );\n};\n\nconst isTimeoutLikeError = (error: unknown) => {\n const err = error as {\n code?: unknown;\n name?: unknown;\n message?: unknown;\n shortMessage?: unknown;\n };\n const text = [\n err?.code,\n err?.name,\n err?.message,\n err?.shortMessage,\n typeof error === \"string\" ? error : undefined,\n ]\n .filter((value) => value !== null && value !== undefined)\n .join(\" \");\n\n return /timeout|timed out|time out|deadline exceeded|expired while waiting|wait.*expired|poll.*expired/i.test(\n text\n );\n};\n\nconst getSdkEventType = (event: any) =>\n event?.type ?? event?.name ?? event?.event ?? \"unknown\";\n\nconst summarizeSdkProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n index?: number\n) => ({\n completed: (step as any)?.completed,\n index,\n rawType: (step as any)?.rawType ?? (step as any)?.stepType,\n state: (step as any)?.state,\n type: getProgressStepType(step),\n});\n\nconst logSdkSwapEvent = (\n label: string,\n event: any,\n meta?: Record\n) => {\n console.log(`[NexusWidget SDK][swap] ${label}`, {\n event,\n eventType: getSdkEventType(event),\n ...meta,\n });\n};\n\nconst logSdkIntentEvent = (\n label: string,\n data: any,\n meta?: Record\n) => {\n console.log(`[NexusWidget SDK][intent] ${label}`, {\n hasAllow: typeof data?.allow === \"function\",\n hasDeny: typeof data?.deny === \"function\",\n hasRefresh: typeof data?.refresh === \"function\",\n intent: data?.intent,\n raw: data,\n ...meta,\n });\n};\n\nconst logSdkIntentInput = (\n operation: string,\n input: unknown,\n meta?: Record\n) => {\n console.log(`[NexusWidget SDK][intent input] ${operation}`, {\n input,\n ...meta,\n });\n};\n\nconst normalizeSdkIntentString = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n return String(value);\n};\n\nconst normalizeSdkIntentAmount = (\n value: unknown,\n fallback: string = \"0\"\n) => normalizeSdkIntentString(value) ?? fallback;\n\nconst getLogoFromMetadata = (metadata: any) =>\n metadata?.logo ??\n metadata?.logoURI ??\n metadata?.logoUri ??\n metadata?.logoUrl ??\n metadata?.icon ??\n \"\";\n\nconst normalizeSdkIntentChain = (chain: any) => {\n const id = Number(chain?.id ?? chain?.chainId);\n if (!Number.isFinite(id)) return undefined;\n const chainMeta = CHAIN_METADATA[id];\n return {\n id,\n logo: getLogoFromMetadata(chain) || chainMeta?.logo || \"\",\n name: chain?.name ?? chainMeta?.name ?? \"\",\n };\n};\n\nconst normalizeSdkIntentToken = (token: any, chainId?: number) => {\n const chainMeta = chainId ? CHAIN_METADATA[chainId] : undefined;\n const decimals = Number(token?.decimals ?? chainMeta?.nativeCurrency.decimals);\n return {\n contractAddress:\n token?.contractAddress ??\n token?.address ??\n token?.tokenAddress ??\n zeroAddress,\n decimals: Number.isFinite(decimals) ? decimals : 18,\n logo: getLogoFromMetadata(token) || undefined,\n symbol: token?.symbol ?? token?.tokenSymbol ?? \"\",\n };\n};\n\nconst normalizeSdkIntentGas = (\n gas: any,\n chainId?: number\n): SwapIntentDestination[\"gas\"] => ({\n amount: normalizeSdkIntentAmount(gas?.amount),\n value: normalizeSdkIntentString(gas?.value),\n token: normalizeSdkIntentToken(gas?.token ?? gas, chainId),\n});\n\nconst normalizeSdkIntentSource = (\n source: any\n): SwapIntentSource | undefined => {\n const chain = normalizeSdkIntentChain(source?.chain);\n if (!chain) return undefined;\n return {\n ...source,\n amount: normalizeSdkIntentAmount(source?.amount),\n chain,\n token: normalizeSdkIntentToken(source?.token, chain.id),\n value: normalizeSdkIntentString(source?.value),\n };\n};\n\nconst normalizeSdkIntentDestination = (\n destination: any\n): SwapIntentDestination | undefined => {\n const chain = normalizeSdkIntentChain(destination?.chain);\n if (!chain) return undefined;\n return {\n ...destination,\n amount: normalizeSdkIntentAmount(destination?.amount),\n chain,\n gas: normalizeSdkIntentGas(destination?.gas, chain.id),\n token: normalizeSdkIntentToken(destination?.token, chain.id),\n value: normalizeSdkIntentString(destination?.value),\n };\n};\n\nconst normalizeSwapIntentData = (intent: any): SwapIntentData | null => {\n const destination = normalizeSdkIntentDestination(intent?.destination);\n if (!destination) return null;\n\n return {\n ...intent,\n destination,\n sources: Array.isArray(intent?.sources)\n ? intent.sources\n .map(normalizeSdkIntentSource)\n .filter(\n (source: SwapIntentSource | undefined): source is SwapIntentSource =>\n Boolean(source)\n )\n : [],\n };\n};\n\nconst normalizeSwapAndExecuteRequirementIntent = (\n intent: any\n): SwapIntentData | null => {\n const requirement = intent?.executeRequirement ?? intent?.executionRequirement;\n if (!requirement) return null;\n const destination = normalizeSdkIntentDestination({\n amount: requirement?.token?.amount,\n chain: requirement?.chain,\n gas: requirement?.gas,\n token: requirement?.token,\n value: requirement?.token?.value,\n });\n if (!destination) return null;\n\n return {\n ...intent,\n bridgeProvider: normalizeBridgeProvider(intent?.bridgeProvider),\n destination,\n feesAndBuffer: intent?.feesAndBuffer,\n sources: [],\n };\n};\n\nconst normalizeRenderableSwapIntentData = (\n rawIntent: any,\n bridgeProvider?: BridgeProvider\n): SwapIntentData | null => {\n const direct = normalizeSwapIntentData(rawIntent);\n const normalizedIntent = direct\n ? null\n : normalizeSwapIntentData(rawIntent?.normalizedIntent);\n const nestedSwap =\n direct || normalizedIntent ? null : normalizeSwapIntentData(rawIntent?.swap);\n const requirement = direct || normalizedIntent || nestedSwap\n ? null\n : normalizeSwapAndExecuteRequirementIntent(rawIntent);\n const normalized = direct ?? normalizedIntent ?? nestedSwap ?? requirement;\n if (!normalized) return null;\n\n return bridgeProvider === undefined\n ? normalized\n : { ...normalized, bridgeProvider };\n};\n\nconst logSwapPlanSteps = (\n eventType: \"plan_preview\" | \"plan_confirmed\",\n stepList: Array,\n rawSteps: unknown\n) => {\n console.log(`[NexusWidget SDK][swap] ${eventType} step list`, {\n count: stepList.length,\n eventType,\n rawSteps,\n steps: stepList.map((step, index) => summarizeSdkProgressStep(step, index)),\n });\n};\n\nconst logSwapPlanProgress = (\n event: any,\n step: SwapStepType | BridgeStepType,\n eventName: string,\n completed: boolean\n) => {\n console.log(\"[NexusWidget SDK][swap] plan_progress\", {\n completed,\n eventName,\n eventType: getSdkEventType(event),\n normalizedStep: summarizeSdkProgressStep(step),\n rawEvent: event,\n rawStep: event?.step,\n state: event?.state,\n stepType: event?.stepType,\n });\n};\n\nconst getFailureMessageForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n mode: NexusWidgetMode,\n autoRefundAvailable = false\n) => {\n if (autoRefundAvailable) {\n return \"Swap Failed. Refund Initiated\";\n }\n\n const type = getProgressStepType(step);\n if (\n type.includes(\"CREATE_PERMIT_FOR_SOURCE_SWAP\") ||\n type.includes(\"CREATE_PERMIT_EOA_TO_EPHEMERAL\") ||\n type.includes(\"EOA_EXECUTE_CALL\") ||\n type.includes(\"SOURCE_SWAP\") ||\n type.includes(\"COLLECTION\")\n ) {\n return \"Collection Failed\";\n }\n if (type.includes(\"DESTINATION_SWAP\") || type.includes(\"FULFIL\")) {\n return \"Destination Swap Failed\";\n }\n if (\n type.includes(\"TRANSACTION\") ||\n type.includes(\"APPROVAL\") ||\n type.includes(\"DEPOSIT\")\n ) {\n return mode === \"send\"\n ? \"Send failed. Funds are in your wallet\"\n : mode === \"deposit\"\n ? \"Deposit failed. Funds are in your wallet\"\n : \"Swap Failed\";\n }\n if (\n type.includes(\"SWAP\") ||\n type.includes(\"BRIDGE\") ||\n type.includes(\"INTENT\") ||\n type.includes(\"DETERMINING\")\n ) {\n return \"Swap Failed\";\n }\n return mode === \"send\"\n ? \"Send failed. Funds are in your wallet\"\n : mode === \"deposit\"\n ? \"Deposit failed. Funds are in your wallet\"\n : \"Swap Failed\";\n};\n\nconst getBridgeTokenSymbolForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined\n) => {\n const rawStep = step as any;\n return (\n getNonEmptyString(\n rawStep?.bridgeToken?.symbol,\n rawStep?.data?.bridgeToken?.symbol,\n rawStep?.bridgeTokenSymbol,\n rawStep?.data?.bridgeTokenSymbol,\n rawStep?.swaps?.[0]?.input?.symbol,\n rawStep?.data?.swaps?.[0]?.input?.symbol,\n rawStep?.input?.symbol,\n rawStep?.data?.input?.symbol,\n rawStep?.asset?.symbol,\n rawStep?.data?.asset?.symbol\n ) ?? \"USDC\"\n );\n};\n\nconst getFailureDescriptionForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n autoRefundAvailable = false\n) => {\n if (autoRefundAvailable) return undefined;\n const type = getProgressStepType(step);\n if (!type.includes(\"DESTINATION_SWAP\")) return undefined;\n const bridgeTokenSymbol = getBridgeTokenSymbolForProgressStep(step);\n return `${bridgeTokenSymbol} has been bridged and you have those funds in your wallet.`;\n};\n\nconst getSourceRows = (entry: SwapHistoryEntry): HistorySourceRow[] => {\n const sources = entry.intentData?.sources ?? [];\n const displayDestinationSourceRow = getDisplayDestinationSourceRow(entry);\n if (sources.length > 0) {\n const sourceRows = sources.map((source, index) => {\n const fallback = entry.fromTokens.find(\n (token) =>\n token.chainId === source.chain.id &&\n (token.contractAddress?.toLowerCase() ===\n source.token.contractAddress?.toLowerCase() ||\n token.symbol === source.token.symbol)\n );\n\n return {\n key: `${source.chain.id}-${source.token.contractAddress}-${index}`,\n chainId: source.chain.id,\n contractAddress: source.token.contractAddress,\n tokenLogo: source.token.logo || fallback?.logo,\n chainLogo: source.chain.logo || fallback?.chainLogo,\n symbol: source.token.symbol,\n chainName: getShortChainName(source.chain.id, source.chain.name),\n amount: source.amount,\n value: source.value,\n };\n });\n\n return mergeHistorySourceRows(\n displayDestinationSourceRow\n ? [displayDestinationSourceRow, ...sourceRows]\n : sourceRows\n );\n }\n\n const fallbackRows = entry.fromTokens.map((token, index) => ({\n key: `${token.chainId}-${token.contractAddress}-${index}`,\n chainId: token.chainId,\n contractAddress: token.contractAddress,\n tokenLogo: token.logo,\n chainLogo: token.chainLogo,\n symbol: token.symbol,\n chainName: getShortChainName(token.chainId, token.chainName),\n amount: token.userAmount || \"0\",\n value: token.balanceInFiat,\n }));\n\n return mergeHistorySourceRows(\n displayDestinationSourceRow\n ? [displayDestinationSourceRow, ...fallbackRows]\n : fallbackRows\n );\n};\n\nfunction SourceRowsList({\n entry,\n maxHeight = 236,\n borderTopFirst = true,\n scrollAfterRows = 4,\n}: {\n entry: SwapHistoryEntry;\n maxHeight?: number;\n borderTopFirst?: boolean;\n scrollAfterRows?: number;\n}) {\n const rows = getSourceRows(entry);\n const shouldScroll = rows.length > scrollAfterRows;\n const scrollRef = useRef(null);\n\n return (\n
\n \n {rows.map((row, index) => (\n 0 ? \"1px solid #E8E8E7\" : \"none\",\n display: \"flex\",\n justifyContent: \"space-between\",\n minHeight: \"64px\",\n padding: \"10px 20px\",\n }}\n >\n \n \n
\n \n {row.symbol}\n \n \n on {row.chainName || \"Unknown chain\"}\n \n
\n
\n \n \n {formatTokenDisplay(row.amount)} {row.symbol}\n \n \n {formatUsdDisplay(row.value)}\n \n \n \n ))}\n \n {shouldScroll && (\n \n scrollRef.current?.scrollBy({ top: 72, behavior: \"smooth\" })\n }\n style={{\n alignItems: \"center\",\n background: \"#FFFFFE\",\n border: \"1px solid #E8E8E7\",\n borderRadius: \"999px\",\n bottom: \"6px\",\n boxShadow: \"0 2px 8px rgba(22,22,21,0.08)\",\n display: \"flex\",\n height: \"22px\",\n justifyContent: \"center\",\n left: \"50%\",\n padding: 0,\n position: \"absolute\",\n transform: \"translateX(-50%)\",\n width: \"22px\",\n }}\n type=\"button\"\n >\n \n \n )}\n \n );\n}\n\nfunction SwapReceiptPanel({\n entry,\n onDone,\n}: {\n entry: SwapHistoryEntry;\n onDone: () => void;\n}) {\n const [showSourceDetails, setShowSourceDetails] = useState(false);\n const destination = entry.intentData?.destination;\n const isFailed = entry.status === \"failed\";\n const isTimeout = entry.status === \"timeout\";\n const isDeposit = entry.mode === \"deposit\";\n const isSend = entry.mode === \"send\";\n const isRecipientTransfer = isSend || Boolean(entry.recipientAddress);\n const tokenSymbol = destination?.token.symbol || entry.toToken?.symbol || \"\";\n const chainName = getShortChainName(\n destination?.chain.id ?? entry.toToken?.chainId,\n destination?.chain.name || entry.toToken?.chainName || \"\"\n );\n const depositVenue =\n entry.opportunity?.title || entry.opportunity?.protocol || chainName;\n const amount = destination?.amount || \"\";\n const destinationLogo =\n destination?.token.logo ||\n entry.toToken?.logo ||\n entry.opportunity?.tokenLogo ||\n (isDeposit ? entry.opportunity?.logo : undefined);\n const requestedExactOutAmount =\n (isDeposit || isSend) && entry.requestedToAmount\n ? entry.requestedToAmount\n : undefined;\n const requestedExactOutValue =\n (isDeposit || isSend) && entry.requestedToValue\n ? entry.requestedToValue\n : undefined;\n const value = requestedExactOutValue || destination?.value;\n const displayAmount = requestedExactOutAmount || amount;\n const showIntentExplorer = hasValidIntentExplorer(entry);\n const intentLabel = entry.intentId\n ? `Intent #${entry.intentId}`\n : \"View Explorer\";\n const sourceRows = getSourceRows(entry);\n const sourceCount = sourceRows.length;\n const sourceTotalUsd = sourceRows.reduce(\n (sum, source) => sum.plus(parseDecimalLoose(source.value) ?? 0),\n new Decimal(0)\n );\n const defaultSwapFailureHeadline = entry.autoRefundAvailable\n ? \"Swap Failed. Refund Initiated\"\n : \"Swap Failed\";\n const entryFailureMessage =\n entry.status === \"timeout\" ? TIMEOUT_LABEL : entry.failureMessage;\n const storedFailureMessage =\n !entry.autoRefundAvailable && entryFailureMessage?.includes(\"Refund\")\n ? undefined\n : entryFailureMessage;\n const failureHeadline =\n storedFailureMessage ||\n (isDeposit\n ? \"Deposit failed. Funds are in your wallet\"\n : isRecipientTransfer\n ? \"Send failed. Funds are in your wallet\"\n : defaultSwapFailureHeadline);\n const failureDescription = isFailed ? entry.failureDescription : undefined;\n const timeoutHeadline = TIMEOUT_LABEL;\n const timeoutDescription = isTimeout\n ? entry.failureDescription ||\n \"This transaction is still pending. Check the intent explorer for the latest status.\"\n : undefined;\n const receiptLocation = isDeposit ? depositVenue : chainName;\n const receiptSummary = receiptLocation ? `on ${receiptLocation}` : \"\";\n\n return (\n \n \n \n \n \n {isFailed ? \"x\" : isTimeout ? \"!\" : \"✓\"}\n \n \n
\n {isTimeout\n ? timeoutHeadline\n : isFailed\n ? failureHeadline\n : isDeposit\n ? \"You deposited\"\n : isRecipientTransfer\n ? \"You sent\"\n : \"You received\"}\n
\n {(failureDescription || timeoutDescription) && (\n \n {failureDescription || timeoutDescription}\n \n )}\n \n {displayAmount ? formatTokenDisplay(displayAmount) : \"--\"}\n \n {tokenSymbol}\n \n \n
\n ≈ {formatUsdDisplay(value)}\n
\n {receiptSummary && (\n \n {receiptSummary}\n \n )}\n \n\n \n \n \n {isDeposit || isSend ? \"You Paid\" : \"You Swapped\"}\n \n \n \n {formatUsdDisplay(sourceTotalUsd)}\n \n setShowSourceDetails((current) => !current)}\n style={{\n alignItems: \"center\",\n background: \"transparent\",\n border: \"none\",\n color: \"var(--foreground-brand)\",\n cursor: \"pointer\",\n display: \"inline-flex\",\n fontFamily: uiFont,\n fontSize: \"12px\",\n gap: \"4px\",\n padding: 0,\n }}\n type=\"button\"\n >\n {showSourceDetails\n ? \"Hide Details\"\n : `${sourceCount} asset${sourceCount === 1 ? \"\" : \"s\"}`}\n \n \n \n \n \n
\n \n
\n \n {isRecipientTransfer && entry.recipientAddress && (\n \n \n Recipient\n \n \n \n )}\n {showIntentExplorer && (\n \n \n Intent Explorer\n \n \n {intentLabel} ↗\n \n \n )}\n {entry.finalExplorerUrl && (\n \n \n Final Transaction\n \n \n View Explorer ↗\n \n \n )}\n \n \n Total Fees\n \n \n {formatUsdDisplay(entry.feeUsd)}\n \n \n \n\n \n Done\n \n \n );\n}\n\nconst getRelativeTime = (time: number, now: number) => {\n const seconds = Math.max(1, Math.floor((now - time) / 1000));\n if (seconds < 60) return `${seconds} second${seconds === 1 ? \"\" : \"s\"} ago`;\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes} minute${minutes === 1 ? \"\" : \"s\"} ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours} hour${hours === 1 ? \"\" : \"s\"} ago`;\n const days = Math.floor(hours / 24);\n return `${days} day${days === 1 ? \"\" : \"s\"} ago`;\n};\n\nfunction HistoryStatusPill({ status }: { status: SwapHistoryStatus }) {\n const config =\n status === \"fulfilled\"\n ? { label: \"Fulfilled\", bg: \"#E8F6EF\", fg: \"#168A47\" }\n : status === \"pending\"\n ? { label: \"Pending\", bg: \"#FFF3DE\", fg: \"#B7791F\" }\n : status === \"timeout\"\n ? { label: TIMEOUT_LABEL, bg: \"#FFF3DE\", fg: \"#B7791F\" }\n : status === \"refund-initiated\"\n ? { label: \"Refund Initiated\", bg: \"#FFF3DE\", fg: \"#B7791F\" }\n : { label: \"Failed\", bg: \"#FFE6EA\", fg: \"#E92C2C\" };\n\n return (\n \n {config.label}\n \n );\n}\n\nfunction SwapHistoryPanel({\n entries,\n now,\n}: {\n entries: SwapHistoryEntry[];\n now: number;\n}) {\n if (entries.length === 0) {\n return (\n \n \n \n ↻\n \n \n \n No transactions yet\n \n \n Your transaction history will appear here once you make your first\n swap, deposit, or send.\n \n \n );\n }\n\n const sortedEntries = sortSwapHistoryEntries(entries);\n const shouldScroll = sortedEntries.length > 5;\n\n return (\n \n {sortedEntries.map((entry) => {\n const destination = entry.intentData?.destination;\n const destinationLogo =\n destination?.token.logo ||\n entry.toToken?.logo ||\n entry.opportunity?.tokenLogo ||\n \"\";\n const destinationChainLogo =\n destination?.chain.logo || entry.toToken?.chainLogo || \"\";\n const destinationChainName = getShortChainName(\n destination?.chain.id ?? entry.toToken?.chainId,\n destination?.chain.name || entry.toToken?.chainName || \"\"\n );\n const destinationSymbol =\n destination?.token.symbol || entry.toToken?.symbol || \"\";\n const destinationValue =\n (entry.mode === \"deposit\" || entry.mode === \"send\") &&\n entry.requestedToValue\n ? entry.requestedToValue\n : destination?.value;\n const destinationAmount =\n (entry.mode === \"deposit\" || entry.mode === \"send\") &&\n entry.requestedToAmount\n ? entry.requestedToAmount\n : destination?.amount || \"\";\n const canShowRefund =\n entry.status === \"failed\" && Boolean(entry.autoRefundAvailable);\n const status = canShowRefund ? \"refund-initiated\" : entry.status;\n const sourceRows = getSourceRows(entry);\n const historyExplorerUrl = getHistoryExplorerUrl(entry);\n\n return (\n \n \n \n \n
\n \n {destinationAmount\n ? formatTokenDisplay(destinationAmount)\n : \"--\"}\n \n {destinationSymbol}\n \n
\n \n ≈ {formatUsdDisplay(destinationValue)}\n \n \n \n \n \n \n {getRelativeTime(entry.createdAt ?? entry.startedAt, now)}\n \n \n \n\n {canShowRefund && (\n \n \n Refund Initiated\n \n \n )}\n\n \n \n {sourceRows.length > 0 && (\n \n )}\n \n →\n \n \n \n {historyExplorerUrl && (\n \n \n View\n \n \n \n \n \n \n )}\n \n \n );\n })}\n \n );\n}\n\n// ---------------------------------------------------------------------------\n// NexusWidget\n// ---------------------------------------------------------------------------\n\nexport function NexusWidget(props: NexusWidgetProps) {\n return (\n \n
\n Something went wrong\n
\n \n An unexpected error occurred. Please refresh the page or try\n resetting the widget.\n \n window.location.reload()}\n style={{\n backgroundColor: \"var(--foreground-brand)\",\n border: \"none\",\n borderRadius: \"8px\",\n color: \"#FFFFFE\",\n cursor: \"pointer\",\n fontSize: \"15px\",\n fontWeight: 500,\n padding: \"8px 16px\",\n transition: \"background-color 0.15s ease-out\",\n }}\n type=\"button\"\n >\n Reload Page\n \n \n }\n >\n \n \n );\n}\n\nfunction NexusWidgetInner({\n config: rawConfig,\n embed = true,\n className,\n connectedAddress,\n open: controlledOpen,\n onOpenChange,\n defaultOpen = false,\n onComplete,\n onConnectClick,\n onStart,\n onError,\n onReceiveAssetChange,\n onClose,\n onConnectWallet,\n}: NexusWidgetProps) {\n const {\n nexusSDK,\n bridgableBalance,\n swapBalance,\n swapBalanceLoading,\n getFiatValue,\n resolveTokenUsdRate,\n swapSupportedChainsAndTokens,\n supportedChainsAndTokens,\n fetchSwapBalance,\n handleInit,\n swapIntent: providerSwapIntent,\n network,\n loading: nexusLoading,\n } = useNexus();\n const appConfig = useMemo(\n () => ({\n nexusNetwork: network ?? \"mainnet\",\n }),\n [network]\n );\n const isSwapBalancePending =\n Boolean(nexusSDK) && (swapBalanceLoading || swapBalance === null);\n\n const normalizedWidgetConfig = useMemo(\n () => normalizeNexusWidgetConfig(rawConfig),\n [rawConfig]\n );\n const config = normalizedWidgetConfig.config;\n const activeMode = normalizedWidgetConfig.activeMode;\n const amountInputConfig = normalizedWidgetConfig.amountInput;\n const appearanceConfig = normalizedWidgetConfig.appearance;\n const configuredDepositOptions = normalizedWidgetConfig.depositOptions;\n const isConfiguredAmountFixed = normalizedWidgetConfig.isAmountFixed;\n const isConfiguredRecipientLocked =\n normalizedWidgetConfig.isRecipientLocked;\n const primaryColor = normalizeNexusWidgetPrimaryColor(\n appearanceConfig?.primaryColor\n );\n const primaryButtonBackground =\n primaryColor ?? NEXUS_WIDGET_DEFAULT_PRIMARY_COLOR;\n const primaryButtonForeground = getReadableTextColor(\n primaryButtonBackground\n );\n const theme = useMemo(\n () => ({\n ...nexusWidgetTheme,\n colors: {\n ...nexusWidgetTheme.colors,\n primary: \"var(--foreground-brand)\",\n primaryText: \"var(--foreground-brand)\",\n },\n }),\n [primaryColor]\n );\n const configuredDeposit = getConfiguredDeposit(config);\n const configuredDepositIdentity = getDepositConfigIdentity(configuredDeposit);\n const configuredDestinationTokenOptions = useMemo(() => {\n const destination = rawConfig.destination;\n const destinationChainId = getConfiguredDestinationChainId(destination);\n const destinationTokens =\n rawConfig.mode === \"deposit\"\n ? configuredDepositOptions.map(toSwapTokenOptionFromDepositConfig)\n : destination?.tokens && destinationChainId\n ? destination.tokens\n .map((token) =>\n toSwapTokenOptionFromConfiguredDestinationToken(\n token,\n destinationChainId\n )\n )\n .filter((token): token is SwapTokenOption => Boolean(token))\n : [];\n\n const tokensByKey = new Map();\n for (const token of destinationTokens) {\n tokensByKey.set(getTokenSelectionKey(token), token);\n }\n return Array.from(tokensByKey.values());\n }, [configuredDepositOptions, rawConfig.destination]);\n if (activeMode === \"deposit\" && !configuredDeposit) {\n throw new Error(\n \"NexusWidget deposit mode requires destination.chain, at least one destination token, depositAddress, and executeDeposit.\"\n );\n }\n const showCloseButton = !embed && Boolean(onClose);\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const isControlledOpen = controlledOpen !== undefined;\n const isModalOpen = isControlledOpen ? controlledOpen : internalOpen;\n\n // Preload receive tokens once SDK is available\n useEffect(() => {\n if (nexusSDK) {\n console.log(\n \"[preloadReceiveTokens] Calling preloadReceiveTokens from NexusWidget useEffect (nexusSDK available)\"\n );\n preloadReceiveTokens();\n }\n }, [nexusSDK]);\n\n const {\n address: accountAddress,\n connector,\n status: walletStatus,\n } = useAccount();\n const {\n connectors,\n connectAsync,\n isPending: isWalletConnectPending,\n } = useConnect();\n const { data: walletClient } = useWalletClient();\n const { data: connectorClient } = useConnectorClient();\n const publicClient = usePublicClient();\n const walletClientAddress = walletClient?.account?.address;\n const ownerAddress =\n connectedAddress &&\n isAddress(connectedAddress) &&\n connectedAddress.toLowerCase() !== zeroAddress\n ? connectedAddress\n : accountAddress &&\n isAddress(accountAddress) &&\n accountAddress.toLowerCase() !== zeroAddress\n ? accountAddress\n : walletClientAddress &&\n isAddress(walletClientAddress) &&\n walletClientAddress.toLowerCase() !== zeroAddress\n ? walletClientAddress\n : undefined;\n const getEffectiveWalletProvider = useCallback(\n async (activeConnector = connector) => {\n let connectorProvider: unknown;\n try {\n connectorProvider = await activeConnector?.getProvider();\n } catch {\n connectorProvider = undefined;\n }\n\n const connectorClientProvider = connectorClient\n ? {\n request: (args: unknown) => connectorClient.request(args as any),\n }\n : undefined;\n const walletClientProvider = walletClient\n ? {\n request: (args: unknown) => walletClient.request(args as any),\n }\n : undefined;\n const windowProvider =\n typeof window !== \"undefined\"\n ? (window as Window & { ethereum?: EthereumProvider }).ethereum\n : undefined;\n const effectiveProvider =\n connectorProvider &&\n typeof (connectorProvider as EthereumProvider).request === \"function\"\n ? (connectorProvider as EthereumProvider)\n : (connectorClientProvider ?? walletClientProvider ?? windowProvider);\n\n return effectiveProvider &&\n typeof effectiveProvider.request === \"function\"\n ? (effectiveProvider as EthereumProvider)\n : undefined;\n },\n [connector, connectorClient, walletClient]\n );\n const historyStorageKey = getSwapHistoryStorageKey(ownerAddress);\n\n useEffect(() => {\n if (walletStatus !== \"connected\" || !ownerAddress) {\n return;\n }\n\n let cancelled = false;\n void (async () => {\n const effectiveProvider = await getEffectiveWalletProvider();\n if (cancelled || !effectiveProvider) {\n return;\n }\n await handleInit(effectiveProvider, ownerAddress);\n })().catch((error) => {\n if (!cancelled) {\n console.error(\"Unable to initialize Nexus for wallet address:\", error);\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [getEffectiveWalletProvider, handleInit, ownerAddress, walletStatus]);\n\n // Global form state\n const [amount, setAmount] = useState(\"\");\n const [recipientAddress, setRecipientAddress] = useState(\"\");\n const [editingAssetIndex, setEditingAssetIndex] = useState(\n null\n );\n const [txError, setTxError] = useState(null);\n const [walletActionPending, setWalletActionPending] = useState(false);\n const defaultRecipientAddress = ownerAddress ?? \"\";\n const effectiveRecipientAddress =\n activeMode === \"swap\"\n ? recipientAddress || defaultRecipientAddress\n : recipientAddress;\n const hasSameOwnerSendRecipient =\n activeMode === \"send\" &&\n Boolean(\n ownerAddress &&\n recipientAddress &&\n isAddress(recipientAddress) &&\n recipientAddress.toLowerCase() === ownerAddress.toLowerCase()\n );\n const hasCustomSwapRecipient =\n activeMode === \"swap\" &&\n Boolean(\n recipientAddress &&\n (!defaultRecipientAddress ||\n recipientAddress.toLowerCase() !==\n defaultRecipientAddress.toLowerCase())\n );\n const transferRecipientAddress =\n activeMode === \"send\"\n ? recipientAddress\n : hasCustomSwapRecipient\n ? recipientAddress\n : undefined;\n\n // Swap-specific\n const [swapType, setSwapType] = useState(\"exactIn\");\n const [swapStep, setSwapStep] = useState(\"idle\");\n const drawerCloseTimerRef = useRef | null>(\n null\n );\n const terminalBalanceRefreshTimerRef = useRef | null>(null);\n const [closingDrawerStep, setClosingDrawerStep] = useState(\n null\n );\n const rootContentRef = useRef(null);\n const [rootContentHeight, setRootContentHeight] = useState(\n null\n );\n const rootContentHeightRef = useRef(null);\n const [hasMeasuredRootContent, setHasMeasuredRootContent] = useState(false);\n const [shouldAnimateRootHeight, setShouldAnimateRootHeight] = useState(false);\n const [isPreviewTransitioning, setIsPreviewTransitioning] = useState(false);\n const rootHeightTransitionTimerRef = useRef | null>(null);\n const [fromTokens, setFromTokens] = useState([]);\n const [sourceSelectionTouched, setSourceSelectionTouched] = useState(false);\n const [sourceSelectionRevision, setSourceSelectionRevision] = useState(0);\n const [\n sourceSelectionRequiredUsdDisplay,\n setSourceSelectionRequiredUsdDisplay,\n ] = useState(undefined);\n const [sourcePickerDraftTokens, setSourcePickerDraftTokens] = useState<\n SwapTokenOption[] | null\n >(null);\n const sourcePickerDraftTokensRef = useRef(null);\n const sourcePickerDraftDepositFilterRef = useRef(\"all\");\n const sourcePickerDraftTouchedRef = useRef(false);\n const sourcePickerDraftModeRef = useRef<\"all\" | \"selected\">(\"all\");\n const [exactOutQuoteSourceMode, setExactOutQuoteSourceMode] = useState<\n \"all\" | \"selected\"\n >(\"all\");\n const exactOutQuoteSourceModeRef = useRef<\"all\" | \"selected\">(\"all\");\n const [toToken, setToToken] = useState(\n undefined\n );\n const [fromTokensQuoteKey, setFromTokensQuoteKey] = useState(\"\");\n\n useEffect(() => {\n const key = getSourceTokensQuoteKey(\n activeMode === \"swap\" && swapType === \"exactIn\"\n ? getReadyExactInSourceTokens(fromTokens)\n : fromTokens\n );\n setFromTokensQuoteKey(key);\n }, [activeMode, swapType, fromTokens]);\n const toTokenQuoteKey = getTokenQuoteKey(toToken);\n const appliedTokenPrefillRef = useRef(null);\n\n useEffect(() => {\n if (!toToken?.chainId || !toToken.contractAddress) return;\n\n let active = true;\n const selectedTokenKey = getTokenSelectionKey(toToken);\n const applyLoadedReceiveToken = () => {\n if (!active) return;\n const loadedToken = getCachedReceiveTokenMatch(toToken);\n if (!loadedToken) return;\n\n setToToken((current) => {\n if (!current || getTokenSelectionKey(current) !== selectedTokenKey) {\n return current;\n }\n\n const chainMeta = current.chainId\n ? CHAIN_METADATA[current.chainId]\n : undefined;\n const next = {\n ...current,\n chainLogo:\n loadedToken.chainLogo || current.chainLogo || chainMeta?.logo,\n chainName: getShortChainName(\n current.chainId,\n loadedToken.chainName || current.chainName || chainMeta?.name\n ),\n decimals: loadedToken.decimals ?? current.decimals,\n logo: loadedToken.logo || current.logo,\n name: loadedToken.name || current.name,\n priceUSD: loadedToken.priceUSD ?? current.priceUSD,\n symbol: loadedToken.symbol || current.symbol,\n };\n\n if (\n current.decimals === next.decimals &&\n current.chainLogo === next.chainLogo &&\n current.chainName === next.chainName &&\n current.logo === next.logo &&\n current.name === next.name &&\n current.priceUSD === next.priceUSD &&\n current.symbol === next.symbol\n ) {\n return current;\n }\n\n return next;\n });\n };\n\n applyLoadedReceiveToken();\n const receiveTokensPromise = preloadReceiveTokens();\n receiveTokensPromise?.then(applyLoadedReceiveToken).catch((error) => {\n if (active) {\n console.warn(\"Unable to refresh receive token metadata\", error);\n }\n });\n\n return () => {\n active = false;\n };\n }, [\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n const setExactOutQuoteSourceModeValue = useCallback(\n (mode: \"all\" | \"selected\") => {\n exactOutQuoteSourceModeRef.current = mode;\n setExactOutQuoteSourceMode(mode);\n },\n []\n );\n\n useEffect(() => {\n if (!nexusSDK) return;\n void fetchSwapBalance();\n }, [fetchSwapBalance, nexusSDK]);\n\n useEffect(() => {\n setSourceSelectionTouched(false);\n setExactOutQuoteSourceModeValue(\"all\");\n }, [activeMode, setExactOutQuoteSourceModeValue]);\n\n useEffect(() => {\n if (activeMode !== \"swap\") return;\n\n setRecipientAddress((current) => {\n if (!defaultRecipientAddress) return current ? \"\" : current;\n return current.toLowerCase() === defaultRecipientAddress.toLowerCase()\n ? current\n : defaultRecipientAddress;\n });\n }, [activeMode, defaultRecipientAddress]);\n\n const {\n steps,\n seed,\n onStepsList,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n const [progressEvents, setProgressEvents] = useState(\n []\n );\n const progressEventsRef = useRef([]);\n const swapStepsListRef = useRef([]);\n const [failedProgressStep, setFailedProgressStep] = useState<\n SwapStepType | BridgeStepType | null\n >(null);\n const [explorerUrls, setExplorerUrls] = useState<{\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n }>({ sourceExplorerUrl: null, destinationExplorerUrl: null });\n const swapRunIdRef = useRef(0);\n\n const widgetSessionIdRef = useRef(\n typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`\n );\n const widgetAttemptIdRef = useRef(null);\n const widgetOpenedTsRef = useRef(Date.now());\n const previewViewedTsRef = useRef(null);\n const previewConfirmedTsRef = useRef(null);\n const attemptCountRef = useRef(0);\n const fundsMovedRef = useRef(false);\n const intentUrlRef = useRef(null);\n const hadSimulationSuccessRef = useRef(false);\n const hadPreviewViewedRef = useRef(false);\n const widgetOpenedFiredRef = useRef(false);\n const reachedTerminalRef = useRef(false);\n const lastIntentSourceTokensRef = useRef([]);\n const lastAutoIntentSourceTokensRef = useRef([]);\n const immediateQuoteAfterSourceEditRef = useRef(false);\n const amountEnteredLastValueRef = useRef(\"\");\n const lastInputMethodRef = useRef<\n | \"typed\"\n | \"percent_20\"\n | \"percent_25\"\n | \"percent_50\"\n | \"percent_75\"\n | \"percent_max\"\n >(\"typed\");\n const prevSourceTouchedRef = useRef(false);\n const previousAutoSourceCountRef = useRef(0);\n const analyticsRef = useRef<{\n track: (event: string, properties?: Record) => void;\n } | null>(null);\n const selectedOpportunityRef = useRef(\n undefined\n );\n\n const newAttemptId = useCallback(() => {\n return typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`;\n }, []);\n\n const rotateAttempt = useCallback(() => {\n widgetAttemptIdRef.current = newAttemptId();\n previewViewedTsRef.current = null;\n previewConfirmedTsRef.current = null;\n fundsMovedRef.current = false;\n intentUrlRef.current = null;\n hadSimulationSuccessRef.current = false;\n hadPreviewViewedRef.current = false;\n reachedTerminalRef.current = false;\n }, [newAttemptId]);\n const [intentToAmount, setIntentToAmount] = useState(\n undefined\n );\n const [intentFeeUsd, setIntentFeeUsd] = useState(\n undefined\n );\n const [intentLoading, setIntentLoading] = useState(false);\n const [quoteRefreshing, setQuoteRefreshing] = useState(false);\n const [receiveMaxCalculating, setReceiveMaxCalculating] = useState(false);\n const [maxCalculationPercent, setMaxCalculationPercent] = useState<\n number | null\n >(null);\n const intentDestinationUsdRateCacheRef = useRef<\n Record\n >({});\n const intentSymbolUsdRateCacheRef = useRef<\n Record\n >({});\n const predictiveQuoteCacheRef = useRef<\n Record\n >({});\n const predictiveQuoteRunRef = useRef(0);\n const latestIntentFetchRunIdRef = useRef(0);\n const intentFetchTimingRef = useRef(null);\n const intentUiRenderLoggedRunIdRef = useRef(null);\n const [intentFetchTiming, setIntentFetchTiming] =\n useState(null);\n const [predictiveQuote, setPredictiveQuote] =\n useState(null);\n const maxPercentRunRef = useRef(0);\n const [previewQuoteRefreshing, setPreviewQuoteRefreshing] = useState(false);\n const [quoteRefreshProgress, setQuoteRefreshProgress] = useState(0);\n const [quoteRefreshSecondsRemaining, setQuoteRefreshSecondsRemaining] =\n useState(0);\n const [intentData, setIntentData] = useState(null);\n const [swapQuoteIssue, setSwapQuoteIssue] = useState(\n null\n );\n const [receiveAmountIssue, setReceiveAmountIssue] =\n useState(null);\n const receiveAmountIssueRef = useRef(null);\n const receiveAmountIssueKeyRef = useRef(\"\");\n const [transferExplorerUrl, setTransferExplorerUrl] = useState(\n null\n );\n const swapStepRef = useRef(swapStep);\n const syncingIntentSourcesRef = useRef(false);\n const lastSwapIntentRefreshAtRef = useRef(0);\n const [destinationBalance, setDestinationBalance] = useState(\n null\n );\n const [swapHistory, setSwapHistory] = useState(() =>\n readSwapHistoryFromStorage(historyStorageKey)\n );\n const [currentSwapId, setCurrentSwapId] = useState(null);\n const [historyNow, setHistoryNow] = useState(() => Date.now());\n const currentSwapIdRef = useRef(null);\n const currentSwapStartedAtRef = useRef(0);\n const historyStorageKeyRef = useRef(historyStorageKey);\n const skipNextHistoryPersistRef = useRef(false);\n const explorerUrlsRef = useRef<{\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n }>({ sourceExplorerUrl: null, destinationExplorerUrl: null });\n const activeQuoteInputKeyRef = useRef(\"\");\n\n // Ref to store swap intent hook allow/deny callbacks\n const swapIntentRef = useRef<{\n intent?: SwapIntentData;\n allow: () => void;\n deny: () => void;\n refresh: () => Promise;\n runId?: number;\n quoteInputKey?: string;\n } | null>(null);\n\n const startIntentFetchTiming = useCallback(\n ({\n background,\n operation,\n quoteInputKey,\n runId,\n }: {\n background: boolean;\n operation: string;\n quoteInputKey: string;\n runId: number;\n }) => {\n const startTimeMs = Date.now();\n const timing: IntentFetchTiming = {\n background,\n operation,\n quoteInputKey,\n runId,\n startTimeMs,\n walletAddress: ownerAddress ?? null,\n };\n\n latestIntentFetchRunIdRef.current = runId;\n intentFetchTimingRef.current = timing;\n intentUiRenderLoggedRunIdRef.current = null;\n setIntentFetchTiming(timing);\n console.log(\"[NexusWidget] intent fetch start\", {\n runId,\n walletAddress: timing.walletAddress,\n operation,\n quoteInputKey,\n background,\n dateNow: startTimeMs,\n date: new Date(startTimeMs),\n });\n return timing;\n },\n [ownerAddress]\n );\n\n const finishIntentFetchTiming = useCallback(\n (runId: number, status: \"resolved\" | \"failed\" = \"resolved\") => {\n const timing = intentFetchTimingRef.current;\n if (!timing || timing.runId !== runId) return;\n if (timing.endTimeMs !== undefined) return;\n\n const endTimeMs = Date.now();\n if (latestIntentFetchRunIdRef.current !== runId) {\n return;\n }\n\n const fetchDurationMs = endTimeMs - timing.startTimeMs;\n const completedTiming: IntentFetchTiming = {\n ...timing,\n endTimeMs,\n fetchDurationMs,\n };\n intentFetchTimingRef.current = completedTiming;\n setIntentFetchTiming(completedTiming);\n console.log(\"[NexusWidget] intent fetch end\", {\n runId,\n walletAddress: timing.walletAddress,\n operation: timing.operation,\n quoteInputKey: timing.quoteInputKey,\n status,\n dateNow: endTimeMs,\n date: new Date(endTimeMs),\n });\n console.log(\"[NexusWidget] intent fetch total\", {\n runId,\n walletAddress: timing.walletAddress,\n operation: timing.operation,\n quoteInputKey: timing.quoteInputKey,\n totalTimeSeconds: fetchDurationMs / 1000,\n elapsedMs: fetchDurationMs,\n dateNow: endTimeMs,\n });\n },\n []\n );\n\n useEffect(() => {\n swapStepRef.current = swapStep;\n }, [swapStep]);\n\n useEffect(() => {\n return () => {\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n }\n if (terminalBalanceRefreshTimerRef.current) {\n clearTimeout(terminalBalanceRefreshTimerRef.current);\n }\n if (rootHeightTransitionTimerRef.current) {\n clearTimeout(rootHeightTransitionTimerRef.current);\n }\n };\n }, []);\n\n const isQuoteEditLocked = useCallback(\n () => swapStepRef.current === \"choose-swap-asset\",\n []\n );\n\n const getQuoteRequestDelay = useCallback(() => {\n if (immediateQuoteAfterSourceEditRef.current) {\n immediateQuoteAfterSourceEditRef.current = false;\n return 0;\n }\n return EXACT_OUT_INPUT_DEBOUNCE_MS;\n }, []);\n\n const startRootHeightTransition = useCallback(() => {\n setShouldAnimateRootHeight(true);\n if (rootHeightTransitionTimerRef.current) {\n clearTimeout(rootHeightTransitionTimerRef.current);\n }\n rootHeightTransitionTimerRef.current = setTimeout(() => {\n setShouldAnimateRootHeight(false);\n rootHeightTransitionTimerRef.current = null;\n }, ROOT_HEIGHT_TRANSITION_MS);\n }, []);\n\n const closeDrawerToIdle = useCallback(() => {\n const isDrawerStep =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n swapStep === \"enter-recipient\";\n\n if (!isDrawerStep) {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n return;\n }\n\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n }\n\n setClosingDrawerStep(swapStep);\n drawerCloseTimerRef.current = setTimeout(() => {\n if (\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\"\n ) {\n startRootHeightTransition();\n }\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n setClosingDrawerStep(null);\n drawerCloseTimerRef.current = null;\n }, DRAWER_CLOSE_MS);\n }, [startRootHeightTransition, swapStep]);\n\n const openDrawerStep = useCallback((nextStep: SwapStep) => {\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n drawerCloseTimerRef.current = null;\n }\n if (\n nextStep === \"choose-swap-asset\" ||\n nextStep === \"choose-receive-asset\"\n ) {\n startRootHeightTransition();\n }\n setClosingDrawerStep(null);\n swapStepRef.current = nextStep;\n setSwapStep(nextStep);\n }, [startRootHeightTransition]);\n\n const syncRootContentHeight = useCallback((animate = false) => {\n const element = rootContentRef.current;\n if (!element) return;\n\n const nextHeight = Math.ceil(\n Math.max(element.getBoundingClientRect().height, element.scrollHeight)\n );\n if (nextHeight <= 0) return;\n\n if (rootContentHeightRef.current === nextHeight) {\n setHasMeasuredRootContent(true);\n return;\n }\n\n rootContentHeightRef.current = nextHeight;\n setShouldAnimateRootHeight(animate);\n if (rootHeightTransitionTimerRef.current) {\n clearTimeout(rootHeightTransitionTimerRef.current);\n rootHeightTransitionTimerRef.current = null;\n }\n if (animate) {\n rootHeightTransitionTimerRef.current = setTimeout(() => {\n setShouldAnimateRootHeight(false);\n rootHeightTransitionTimerRef.current = null;\n }, ROOT_HEIGHT_TRANSITION_MS);\n }\n setRootContentHeight(nextHeight);\n setHasMeasuredRootContent(true);\n }, []);\n\n useLayoutEffect(() => {\n syncRootContentHeight(true);\n\n const element = rootContentRef.current;\n if (!element || typeof ResizeObserver === \"undefined\") return;\n\n let frame = 0;\n const observer = new ResizeObserver(() => {\n if (frame) {\n window.cancelAnimationFrame(frame);\n }\n frame = window.requestAnimationFrame(() => {\n syncRootContentHeight(false);\n });\n });\n\n observer.observe(element);\n\n return () => {\n if (frame) {\n window.cancelAnimationFrame(frame);\n }\n observer.disconnect();\n };\n }, [activeMode, swapStep, syncRootContentHeight]);\n\n useEffect(() => {\n currentSwapIdRef.current = currentSwapId;\n }, [currentSwapId]);\n\n useEffect(() => {\n if (historyStorageKeyRef.current === historyStorageKey) return;\n historyStorageKeyRef.current = historyStorageKey;\n skipNextHistoryPersistRef.current = true;\n setSwapHistory(readSwapHistoryFromStorage(historyStorageKey));\n }, [historyStorageKey]);\n\n useEffect(() => {\n if (skipNextHistoryPersistRef.current) {\n skipNextHistoryPersistRef.current = false;\n return;\n }\n\n writeSwapHistoryToStorage(historyStorageKey, swapHistory);\n }, [historyStorageKey, swapHistory]);\n\n useEffect(() => {\n if (swapStep !== \"history\") return;\n const timer = window.setInterval(() => setHistoryNow(Date.now()), 30000);\n return () => window.clearInterval(timer);\n }, [swapStep]);\n\n const normalizeAddress = (value?: string | null) =>\n (value ?? \"\").toLowerCase();\n\n const getTokenLookupAddress = (value?: string | null) => {\n if (!value) return \"\";\n return isNativeTokenAddress(value) ? zeroAddress : value.toLowerCase();\n };\n\n const findBalanceTokenLogo = (\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId) return \"\";\n const lookupAddress = getTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n let symbolMatchLogo = \"\";\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== chainId) continue;\n const breakdownAddress = getTokenLookupAddress(\n breakdown.contractAddress\n );\n const addressMatches =\n lookupAddress && breakdownAddress === lookupAddress;\n const symbolMatches =\n lookupSymbol &&\n [breakdown.symbol, asset.symbol]\n .filter(Boolean)\n .some((candidate) => candidate.toUpperCase() === lookupSymbol);\n const logo = getLogoFromMetadata(breakdown) || getLogoFromMetadata(asset);\n\n if (addressMatches && logo) return logo;\n if (!symbolMatchLogo && symbolMatches && logo) {\n symbolMatchLogo = logo;\n }\n }\n }\n\n return symbolMatchLogo;\n };\n\n const findSupportedTokenLogo = (\n chains: any[] | null | undefined,\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId) return \"\";\n const chain = chains?.find(\n (item: any) => Number(item?.id ?? item?.chainId) === chainId\n );\n const tokens = chain?.tokens ?? chain?.assets ?? [];\n const lookupAddress = getTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n\n const addressMatch =\n lookupAddress &&\n tokens.find((token: any) => {\n const tokenAddress = getTokenLookupAddress(\n token?.contractAddress ?? token?.address ?? token?.tokenAddress\n );\n return tokenAddress === lookupAddress;\n });\n const symbolMatch =\n lookupSymbol &&\n tokens.find((token: any) => {\n const tokenSymbol = (token?.symbol ?? token?.tokenSymbol ?? \"\")\n .toString()\n .toUpperCase();\n return tokenSymbol === lookupSymbol;\n });\n\n return getLogoFromMetadata(addressMatch) || getLogoFromMetadata(symbolMatch);\n };\n\n const resolveTokenLogo = (\n token:\n | {\n address?: string;\n contractAddress?: string;\n logo?: string;\n symbol?: string;\n tokenAddress?: string;\n tokenSymbol?: string;\n }\n | undefined,\n chainId?: number\n ) => {\n const explicitLogo = getLogoFromMetadata(token);\n if (explicitLogo) return explicitLogo;\n\n const contractAddress =\n token?.contractAddress ?? token?.address ?? token?.tokenAddress;\n const symbol = token?.symbol ?? token?.tokenSymbol;\n const citreaToken = findCitreaReceiveToken({\n address: contractAddress,\n chainId,\n symbol,\n });\n const tokenMeta = symbol\n ? TOKEN_METADATA[symbol as keyof typeof TOKEN_METADATA]\n : undefined;\n\n return (\n findBalanceTokenLogo(chainId, contractAddress, symbol) ||\n findSupportedTokenLogo(\n swapSupportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol\n ) ||\n findSupportedTokenLogo(\n supportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol\n ) ||\n citreaToken?.logo ||\n tokenMeta?.logo ||\n (contractAddress && isNativeTokenAddress(contractAddress)\n ? CHAIN_METADATA[chainId ?? 0]?.logo\n : \"\") ||\n \"\"\n );\n };\n\n const enrichIntentToken = <\n T extends {\n address?: string;\n contractAddress?: string;\n logo?: string;\n symbol?: string;\n tokenAddress?: string;\n tokenSymbol?: string;\n },\n >(\n token: T,\n chainId?: number\n ): T => {\n const logo = resolveTokenLogo(token, chainId);\n return logo && logo !== token.logo ? { ...token, logo } : token;\n };\n\n const enrichTokenOptionLogo = (\n token?: SwapTokenOption\n ): SwapTokenOption | undefined => {\n if (!token) return undefined;\n\n const chainMeta = token.chainId ? CHAIN_METADATA[token.chainId] : undefined;\n const logo = resolveTokenLogo(token, token.chainId);\n const next = {\n ...token,\n chainLogo: token.chainLogo || chainMeta?.logo,\n logo: token.logo || logo,\n };\n\n if (next.chainLogo === token.chainLogo && next.logo === token.logo) {\n return token;\n }\n\n return next;\n };\n\n const enrichSwapIntentTokenMetadata = (\n intent: SwapIntentData\n ): SwapIntentData => ({\n ...intent,\n destination: {\n ...intent.destination,\n token: enrichIntentToken(\n intent.destination.token,\n intent.destination.chain.id\n ),\n },\n sources: (intent.sources ?? []).map((source) => ({\n ...source,\n token: enrichIntentToken(source.token, source.chain.id),\n })),\n });\n\n const buildIntentSourceToken = (\n source: SwapIntentData[\"sources\"][number]\n ): SwapTokenOption => {\n let matchedAsset: any;\n let matchedBreakdown: any;\n const sourceAddress = normalizeAddress(source.token.contractAddress);\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const addressMatches =\n normalizeAddress(breakdown.contractAddress) === sourceAddress;\n const symbolMatches =\n breakdown.symbol === source.token.symbol ||\n asset.symbol === source.token.symbol;\n if (\n breakdown.chain?.id === source.chain.id &&\n (addressMatches || symbolMatches)\n ) {\n matchedAsset = asset;\n matchedBreakdown = breakdown;\n break;\n }\n }\n if (matchedBreakdown) break;\n }\n\n const chainMeta = CHAIN_METADATA[source.chain.id];\n const sourceValue = Number((source as any).value ?? 0);\n const isNativeSource = isNativeTokenAddress(source.token.contractAddress);\n const nativeCurrency = chainMeta?.nativeCurrency;\n const sourceSymbol =\n isNativeSource && (!source.token.symbol || !matchedAsset?.logo)\n ? nativeCurrency?.symbol || source.token.symbol\n : source.token.symbol || nativeCurrency?.symbol || \"\";\n const sourceDecimals =\n isNativeSource && nativeCurrency?.decimals !== undefined\n ? nativeCurrency.decimals\n : source.token.decimals;\n const sourceLogo = resolveTokenLogo(source.token, source.chain.id);\n\n return {\n contractAddress: source.token.contractAddress,\n symbol: sourceSymbol,\n name: sourceSymbol,\n logo: sourceLogo ?? \"\",\n decimals: sourceDecimals,\n balance: matchedBreakdown?.balance\n ? `${matchedBreakdown.balance} ${sourceSymbol}`\n : `${source.amount} ${sourceSymbol}`,\n balanceInFiat:\n matchedBreakdown?.balanceInFiat != null\n ? `$${Number(matchedBreakdown.balanceInFiat).toFixed(2)}`\n : Number.isFinite(sourceValue)\n ? `$${sourceValue.toFixed(2)}`\n : \"$0.00\",\n chainId: source.chain.id,\n chainName: getShortChainName(\n source.chain.id,\n chainMeta?.name ?? source.chain.name\n ),\n chainLogo: chainMeta?.logo ?? source.chain.logo,\n userAmount: source.amount,\n userAmountUsd: Number.isFinite(sourceValue) ? source.value : undefined,\n userAmountMode: \"token\",\n };\n };\n\n const clearPendingSwapIntent = (\n clearQuote = true,\n options: { keepQuoteRefreshing?: boolean } = {}\n ) => {\n swapRunIdRef.current += 1;\n swapIntentRef.current?.deny();\n swapIntentRef.current = null;\n setIntentLoading(false);\n setTxError(null);\n if (!options.keepQuoteRefreshing) {\n setQuoteRefreshing(false);\n }\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setSwapQuoteIssue(null);\n resetProgressEvents();\n if (swapStepsListRef.current.length > 0 || steps.length > 0) {\n swapStepsListRef.current = [];\n resetSteps();\n } else {\n swapStepsListRef.current = [];\n }\n if (clearQuote) {\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n if (!options.keepQuoteRefreshing) {\n setPredictiveQuote(null);\n }\n }\n };\n\n const clearSelectedSources = () => {\n setFromTokens((current) => (current.length === 0 ? current : []));\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setExactOutQuoteSourceModeValue(\"all\");\n };\n\n const resetExactOutSourcesToAuto = () => {\n setFromTokens((current) => (current.length === 0 ? current : []));\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setExactOutQuoteSourceModeValue(\"all\");\n setSourceSelectionRevision((current) => current + 1);\n };\n\n const getSourceAmountInput = (tokens: SwapTokenOption[]) => {\n const total = tokens.reduce(\n (sum, token) => sum + Number(token.userAmount || 0),\n 0\n );\n return total > 0 ? String(total) : \"\";\n };\n\n const parseFiatNumber = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = normalizeDecimalInputText(value);\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n };\n\n const minimumSourceUsd = new Decimal(1);\n const hasMinimumSourceUsdValue = (value: unknown) =>\n (parseFiatNumber(value) ?? new Decimal(0)).gte(minimumSourceUsd);\n const hasMinimumSourceUsdBalance = (\n token: Pick\n ) => hasMinimumSourceUsdValue(token.balanceInFiat);\n const filterMinimumSourceUsdTokens = (tokens: SwapTokenOption[]) =>\n tokens.filter(hasMinimumSourceUsdBalance);\n\n const getTokenUsdRateCacheKeyFromParts = (\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId || !symbol) return \"\";\n return [\n chainId,\n (contractAddress || zeroAddress).toLowerCase(),\n symbol.toUpperCase(),\n ].join(\":\");\n };\n\n const getTokenUsdRateCacheKey = (\n token?: Pick\n ) =>\n getTokenUsdRateCacheKeyFromParts(\n token?.chainId,\n token?.contractAddress,\n token?.symbol\n );\n\n const getSymbolUsdRateCacheKey = (symbol?: string) =>\n symbol ? symbol.trim().toUpperCase() : \"\";\n\n const getCachedIntentUsdRate = (\n token?: Pick\n ) => {\n const tokenKey = getTokenUsdRateCacheKey(token);\n const cached = tokenKey\n ? intentDestinationUsdRateCacheRef.current[tokenKey]\n : undefined;\n const rate = parseFiatNumber(cached?.rate);\n return rate && rate.gt(0) ? rate : undefined;\n };\n\n const cacheDestinationUsdRateFromIntent = (\n intent?: SwapIntentData | null\n ) => {\n const destination = intent?.destination;\n const amount = parseFiatNumber(destination?.amount);\n const value = parseFiatNumber(destination?.value);\n const chainId = destination?.chain?.id;\n const symbol = destination?.token?.symbol;\n\n if (\n !amount ||\n !value ||\n amount.lte(0) ||\n value.lte(0) ||\n !chainId ||\n !symbol\n ) {\n return;\n }\n\n const rate = value.div(amount);\n if (!rate.isFinite() || rate.lte(0)) return;\n\n const cached: CachedIntentUsdRate = {\n amount: amount.toFixed(),\n rate: rate.toDecimalPlaces(18).toFixed(),\n updatedAt: Date.now(),\n value: value.toFixed(),\n };\n const tokenKey = getTokenUsdRateCacheKeyFromParts(\n chainId,\n destination?.token?.contractAddress,\n symbol\n );\n if (tokenKey) {\n intentDestinationUsdRateCacheRef.current[tokenKey] = cached;\n }\n\n const symbolKey = getSymbolUsdRateCacheKey(symbol);\n if (symbolKey) {\n intentSymbolUsdRateCacheRef.current[symbolKey] = cached;\n }\n };\n\n const getSwapBalanceTotalUsd = () =>\n (swapBalance ?? []).reduce((sum, asset) => {\n const breakdown = asset.breakdown ?? [];\n if (breakdown.length > 0) {\n return sum.plus(\n breakdown.reduce((breakdownSum, item) => {\n const value = parseFiatNumber(item.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd)\n ? breakdownSum.plus(value)\n : breakdownSum;\n }, new Decimal(0))\n );\n }\n\n const value = parseFiatNumber(asset.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd) ? sum.plus(value) : sum;\n }, new Decimal(0));\n\n const getTokenUsdRate = (token: SwapTokenOption) => {\n const tokenBalance = parseFiatNumber(token.balance) ?? new Decimal(0);\n const fiatBalance = parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n if (tokenBalance.gt(0) && fiatBalance.gt(0)) {\n return fiatBalance.div(tokenBalance);\n }\n\n const priceUsd = parseFiatNumber(token.priceUSD);\n if (priceUsd && priceUsd.gt(0)) {\n return priceUsd;\n }\n\n const fallbackRate = getFiatValue(1, token.symbol);\n if (Number.isFinite(fallbackRate) && fallbackRate > 0) {\n return new Decimal(fallbackRate);\n }\n\n return getCachedIntentUsdRate(token) ?? new Decimal(0);\n };\n const getUsdRateForSymbol = (symbol?: string) => {\n if (!symbol) return new Decimal(0);\n const fiat = getFiatValue(1, symbol);\n if (Number.isFinite(fiat) && fiat > 0) {\n return new Decimal(fiat);\n }\n\n const cached =\n intentSymbolUsdRateCacheRef.current[getSymbolUsdRateCacheKey(symbol)];\n const rate = parseFiatNumber(cached?.rate);\n return rate && rate.gt(0) ? rate : new Decimal(0);\n };\n\n const getFeeTokenLookupAddress = (value?: string | null) => {\n const lower = (value ?? \"\").toLowerCase();\n if (!lower || lower === \"0x\" || isNativeTokenAddress(lower)) {\n return zeroAddress;\n }\n return lower;\n };\n\n const getTokenUsdRateFromBalances = (\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId) return undefined;\n const lookupAddress = getFeeTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== chainId) continue;\n const breakdownAddress = getFeeTokenLookupAddress(\n breakdown.contractAddress\n );\n const addressMatches =\n lookupAddress && breakdownAddress === lookupAddress;\n const symbolMatches =\n lookupSymbol &&\n [breakdown.symbol, asset.symbol]\n .filter(Boolean)\n .some((candidate) => candidate.toUpperCase() === lookupSymbol);\n if (!addressMatches && !symbolMatches) continue;\n\n const balance = parseFiatNumber(breakdown.balance);\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n if (balance && fiatBalance && balance.gt(0) && fiatBalance.gt(0)) {\n return fiatBalance.div(balance);\n }\n }\n }\n\n return undefined;\n };\n\n const getTokenUsdRateFromSupportedChains = (\n chains: any[] | null | undefined,\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId) return undefined;\n const chain = chains?.find(\n (item: any) => Number(item?.id ?? item?.chainId) === chainId\n );\n const tokens = chain?.tokens ?? chain?.assets ?? [];\n const lookupAddress = getFeeTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n const matchedToken = tokens.find((token: any) => {\n const tokenAddress = getFeeTokenLookupAddress(\n token?.contractAddress ?? token?.address ?? token?.tokenAddress\n );\n const addressMatches = lookupAddress && tokenAddress === lookupAddress;\n const tokenSymbol = (token?.symbol ?? token?.tokenSymbol ?? \"\")\n .toString()\n .toUpperCase();\n const symbolMatches = lookupSymbol && tokenSymbol === lookupSymbol;\n return addressMatches || symbolMatches;\n });\n const priceUsd = parseFiatNumber(\n matchedToken?.priceUSD ??\n matchedToken?.priceUsd ??\n matchedToken?.usdPrice ??\n matchedToken?.price\n );\n return priceUsd && priceUsd.gt(0) ? priceUsd : undefined;\n };\n\n const getIntentDestinationGasUsdValue = (\n intent?: SwapIntentData | null\n ) => {\n const gas = intent?.destination?.gas;\n const explicitValue = parseFiatNumber(gas?.value);\n if (explicitValue && explicitValue.gt(0)) return explicitValue;\n\n const amount = parseFiatNumber(gas?.amount);\n if (!amount || amount.lte(0)) return undefined;\n\n const chainId = intent?.destination?.chain?.id;\n const contractAddress = gas?.token?.contractAddress;\n const symbol = gas?.token?.symbol;\n const rate =\n getTokenUsdRateFromBalances(chainId, contractAddress, symbol) ??\n getTokenUsdRateFromSupportedChains(\n swapSupportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol\n ) ??\n getTokenUsdRateFromSupportedChains(\n supportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol\n ) ??\n getUsdRateForSymbol(symbol);\n\n return rate.gt(0) ? amount.mul(rate) : undefined;\n };\n\n const getCachedDestinationUsdRate = (token?: SwapTokenOption) => {\n return getCachedIntentUsdRate(token);\n };\n\n const resolveUsdRateForSymbol = async (symbol?: string) => {\n if (!symbol) return new Decimal(0);\n\n const localRate = getUsdRateForSymbol(symbol);\n if (localRate.gt(0)) return localRate;\n\n try {\n const resolvedRate = await resolveTokenUsdRate(symbol);\n return resolvedRate && resolvedRate > 0\n ? new Decimal(resolvedRate)\n : new Decimal(0);\n } catch {\n return new Decimal(0);\n }\n };\n\n const getTokenUsdValue = (\n token: SwapTokenOption,\n fallbackAmount?: string\n ) => {\n const amountNumber =\n parseFiatNumber(token.userAmount || fallbackAmount) ?? new Decimal(0);\n if (amountNumber.lte(0)) return new Decimal(0);\n const quotedUsd = parseFiatNumber(token.userAmountUsd);\n if (quotedUsd && quotedUsd.gte(0)) return quotedUsd;\n if (token.userAmountMode === \"usd\") return amountNumber;\n\n const rate = getTokenUsdRate(token);\n return rate.gt(0) ? amountNumber.mul(rate) : new Decimal(0);\n };\n\n const getTokenBalanceAmount = (token: SwapTokenOption) =>\n parseFiatNumber(token.balance) ?? new Decimal(0);\n\n const getTokenBalanceUsd = (token: SwapTokenOption) =>\n parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n\n const getSdkExactOutSourcePriority = (token: SwapTokenOption) => {\n const isSameChain =\n Boolean(token.chainId && toToken?.chainId) &&\n token.chainId === toToken?.chainId;\n const isSameToken =\n Boolean(token.contractAddress && toToken?.contractAddress) &&\n ((isNativeTokenAddress(token.contractAddress) &&\n isNativeTokenAddress(toToken?.contractAddress)) ||\n token.contractAddress.toLowerCase() ===\n toToken?.contractAddress.toLowerCase());\n const isStable = isSdkExactOutStableSymbol(token.symbol);\n const isGas = isNativeTokenAddress(token.contractAddress);\n const isEthereum = token.chainId === ETHEREUM_MAINNET_CHAIN_ID;\n\n if (isSameChain) {\n if (isSameToken) return 1;\n if (isStable) return 2;\n if (isGas) return 3;\n return 4;\n }\n\n if (isEthereum) {\n if (isSameToken) return 8;\n if (isStable) return 9;\n if (isGas) return 10;\n return 11;\n }\n\n if (isSameToken) return 5;\n if (isStable) return 6;\n return 7;\n };\n\n const sortExactOutSourcesBySdkPriority = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const priorityDiff =\n getSdkExactOutSourcePriority(a) - getSdkExactOutSourcePriority(b);\n if (priorityDiff !== 0) return priorityDiff;\n\n const usdDiff = getTokenBalanceUsd(b).cmp(getTokenBalanceUsd(a));\n if (usdDiff !== 0) return usdDiff;\n\n return `${a.symbol} ${a.chainName ?? \"\"}`.localeCompare(\n `${b.symbol} ${b.chainName ?? \"\"}`\n );\n });\n\n const getTokenAmountForUsd = (token: SwapTokenOption, usdAmount: Decimal) => {\n const rate = getTokenUsdRate(token);\n if (rate.lte(0) || usdAmount.lte(0)) return new Decimal(0);\n return usdAmount.div(rate);\n };\n\n const getUsdForTokenAmount = (\n token: SwapTokenOption,\n tokenAmount: Decimal\n ) => {\n const rate = getTokenUsdRate(token);\n if (rate.lte(0) || tokenAmount.lte(0)) return new Decimal(0);\n return tokenAmount.mul(rate);\n };\n\n const getExactOutDestinationBalanceCoverage = ({\n requestedAmount,\n requestedUsd,\n producedAmount,\n producedUsd,\n token = toToken,\n }: {\n requestedAmount?: Decimal;\n requestedUsd?: Decimal;\n producedAmount?: Decimal;\n producedUsd?: Decimal;\n token?: SwapTokenOption;\n }) => {\n if (\n (activeMode !== \"deposit\" && activeMode !== \"send\") ||\n !token ||\n !requestedAmount ||\n requestedAmount.lte(0)\n ) {\n return null;\n }\n\n const balanceAmount =\n parseFiatNumber(destinationBalance) ??\n parseFiatNumber(token.balance) ??\n new Decimal(0);\n if (balanceAmount.lte(0)) return null;\n\n const externalAmount =\n producedAmount && producedAmount.gt(0) ? producedAmount : new Decimal(0);\n const uncoveredAmount = Decimal.max(\n requestedAmount.minus(externalAmount),\n new Decimal(0)\n );\n const coveredAmount = Decimal.min(balanceAmount, uncoveredAmount);\n if (coveredAmount.lte(0)) return null;\n\n const requestedRate =\n requestedUsd && requestedUsd.gt(0)\n ? requestedUsd.div(requestedAmount)\n : undefined;\n const producedRate =\n producedUsd && producedUsd.gt(0) && producedAmount && producedAmount.gt(0)\n ? producedUsd.div(producedAmount)\n : undefined;\n const fallbackRate = getTokenUsdRate(token);\n const usdRate =\n requestedRate && requestedRate.gt(0)\n ? requestedRate\n : producedRate && producedRate.gt(0)\n ? producedRate\n : fallbackRate.gt(0)\n ? fallbackRate\n : undefined;\n\n return {\n amount: coveredAmount,\n usd: usdRate ? coveredAmount.mul(usdRate) : undefined,\n };\n };\n\n const buildDestinationBalanceDisplayToken = (\n coverage: ReturnType,\n token?: SwapTokenOption\n ): SwapTokenOption | null => {\n if (!coverage || !token || coverage.amount.lte(0)) return null;\n\n const amount = coverage.amount\n .toDecimalPlaces(Math.max(0, token.decimals ?? 18), Decimal.ROUND_DOWN)\n .toFixed();\n const usd = coverage.usd?.toDecimalPlaces(6, Decimal.ROUND_DOWN).toFixed();\n const balanceUsd = coverage.usd\n ? `$${coverage.usd.toDecimalPlaces(2, Decimal.ROUND_DOWN).toFixed()}`\n : token.balanceInFiat || \"$0.00\";\n\n return {\n ...token,\n balance: `${amount} ${token.symbol}`,\n balanceInFiat: balanceUsd,\n userAmount: amount,\n userAmountMode: \"token\",\n userAmountUsd: usd,\n };\n };\n\n const cacheSymbolUsdRate = (symbol: string | undefined, rate: Decimal) => {\n const symbolKey = getSymbolUsdRateCacheKey(symbol);\n if (!symbolKey || rate.lte(0)) return;\n\n intentSymbolUsdRateCacheRef.current[symbolKey] = {\n amount: \"1\",\n rate: rate.toDecimalPlaces(18).toFixed(),\n updatedAt: Date.now(),\n value: rate.toFixed(),\n };\n };\n\n const getPredictiveDestinationKey = (token?: SwapTokenOption) => {\n const tokenKey = getTokenUsdRateCacheKey(token);\n return tokenKey ? `destination:${tokenKey}` : \"\";\n };\n\n const getPredictiveSourceKey = (token: SwapTokenOption) =>\n [\n token.chainId ?? \"unknown\",\n (token.contractAddress || zeroAddress).toLowerCase(),\n token.symbol.toUpperCase(),\n ].join(\":\");\n\n const getPredictiveQuoteCacheKey = (\n mode = activeMode,\n type = swapType,\n destination = toToken,\n sources = fromTokens\n ) => {\n const destinationKey = getPredictiveDestinationKey(destination);\n if (!destinationKey) return \"\";\n if (mode !== \"swap\" || type !== \"exactIn\") {\n return `exactOut:${destinationKey}`;\n }\n\n const sourceKey = getExpandedSourceTokens(sources)\n .map(getPredictiveSourceKey)\n .sort()\n .join(\"+\");\n return sourceKey ? `exactIn:${sourceKey}->${destinationKey}` : \"\";\n };\n\n const getPredictiveDisplayAmount = (\n amount: Decimal,\n token?: Pick\n ) => {\n const decimals = Math.min(\n PREDICTIVE_QUOTE_DISPLAY_DECIMALS,\n Math.max(0, token?.decimals ?? 18)\n );\n return amount.toDecimalPlaces(decimals, Decimal.ROUND_DOWN).toFixed();\n };\n\n const resolveUsdRateForToken = async (token?: SwapTokenOption) => {\n if (!token?.symbol) return new Decimal(0);\n\n const localRate = getTokenUsdRate(token);\n if (localRate.gt(0)) return localRate;\n\n const resolvedRate = await resolveUsdRateForSymbol(token.symbol);\n if (resolvedRate.gt(0)) {\n cacheSymbolUsdRate(token.symbol, resolvedRate);\n }\n return resolvedRate;\n };\n\n const getDestinationReceiveLimitUsd = (token?: SwapTokenOption) => {\n if (!token?.chainId) return undefined;\n const limit = DESTINATION_RECEIVE_LIMIT_USD_BY_CHAIN_ID[token.chainId];\n return limit ? new Decimal(limit) : undefined;\n };\n\n const getImmediateDestinationReceiveUsdRate = (token?: SwapTokenOption) => {\n const priceUsd = parseFiatNumber(token?.priceUSD);\n if (priceUsd && priceUsd.gt(0)) return priceUsd;\n\n const cachedRate = getCachedDestinationUsdRate(token);\n if (cachedRate && cachedRate.gt(0)) return cachedRate;\n\n if (!token) return undefined;\n const localRate = getTokenUsdRate(token);\n return localRate.gt(0) ? localRate : undefined;\n };\n\n const getExactInSourceUsdForReceiveLimit = (\n sourceTokens: SwapTokenOption[],\n inputAmount: string\n ) => {\n if (sourceTokens.length === 0) return undefined;\n let hasPositiveSourceAmount = false;\n let totalUsd = new Decimal(0);\n\n for (const token of sourceTokens) {\n const fallbackAmount =\n sourceTokens.length === 1 ? inputAmount : undefined;\n const sourceAmount = parseFiatNumber(token.userAmount || fallbackAmount);\n if (!sourceAmount || sourceAmount.lte(0)) continue;\n\n hasPositiveSourceAmount = true;\n const sourceUsd = getTokenUsdValue(token, fallbackAmount);\n if (sourceUsd.lte(0)) return undefined;\n totalUsd = totalUsd.plus(sourceUsd);\n }\n\n return hasPositiveSourceAmount ? totalUsd : undefined;\n };\n\n const buildReceiveAmountIssue = ({\n destinationRate,\n destinationToken = toToken,\n inputAmount = amount,\n mode = activeMode,\n sourceTokens = fromTokens,\n type = swapType,\n }: {\n destinationRate?: Decimal;\n destinationToken?: SwapTokenOption;\n inputAmount?: string;\n mode?: NexusWidgetMode;\n sourceTokens?: SwapTokenOption[];\n type?: SwapType;\n } = {}): ReceiveAmountIssue | null => {\n const limit = getDestinationReceiveLimitUsd(destinationToken);\n if (!limit || !destinationToken) return null;\n\n const parsedAmount = parseFiatNumber(inputAmount);\n if (!parsedAmount || parsedAmount.lte(0)) return null;\n if (mode === \"swap\" && type === \"exactIn\" && sourceTokens.length === 0) {\n return null;\n }\n\n const chainName = getShortChainName(\n destinationToken.chainId,\n destinationToken.chainName\n );\n const resolvedDestinationRate =\n destinationRate ??\n getImmediateDestinationReceiveUsdRate(destinationToken);\n\n if (!resolvedDestinationRate || resolvedDestinationRate.lte(0)) {\n return {\n ctaLabel: \"Price unavailable\",\n message: `Unable to price ${destinationToken.symbol} on ${chainName}. Select another receive token.`,\n type: \"unpricedReceiveToken\",\n };\n }\n\n let receiveUsd: Decimal | undefined;\n if (mode === \"swap\" && type === \"exactIn\") {\n receiveUsd = getExactInSourceUsdForReceiveLimit(\n sourceTokens,\n inputAmount\n );\n if (!receiveUsd || receiveUsd.lte(0)) {\n return {\n ctaLabel: \"Price unavailable\",\n message: `Unable to price selected assets for ${chainName}'s receive limit.`,\n type: \"unpricedReceiveToken\",\n };\n }\n } else if (mode === \"deposit\" && depositAmountMode === \"usd\") {\n receiveUsd = parsedAmount;\n } else {\n receiveUsd = parsedAmount.mul(resolvedDestinationRate);\n }\n\n if (receiveUsd.gt(limit)) {\n return {\n ctaLabel: \"Receive limit exceeded\",\n message: `Maximum receive amount on ${chainName} is ${formatUsdDisplay(limit)}.`,\n type: \"receiveLimitExceeded\",\n };\n }\n\n return null;\n };\n\n const buildConfiguredAmountIssue = (\n inputAmount = amount\n ): ReceiveAmountIssue | null => {\n if (!amountInputConfig || !inputAmount) return null;\n const parsedAmount = parseFiatNumber(inputAmount);\n if (!parsedAmount || parsedAmount.lte(0)) return null;\n\n const minAmount = parseFiatNumber(amountInputConfig.min);\n if (minAmount && parsedAmount.lt(minAmount)) {\n return {\n ctaLabel: \"Below minimum\",\n message: `Minimum amount is ${minAmount.toFixed()}.`,\n type: \"configuredAmountLimit\",\n };\n }\n\n const maxAmount = parseFiatNumber(amountInputConfig.max);\n if (maxAmount && parsedAmount.gt(maxAmount)) {\n return {\n ctaLabel: \"Above maximum\",\n message: `Maximum amount is ${maxAmount.toFixed()}.`,\n type: \"configuredAmountLimit\",\n };\n }\n\n return null;\n };\n\n const clearPreviewForBlockingAmountIssue = () => {\n clearPendingSwapIntent(true);\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setTxError(null);\n setSwapQuoteIssue(null);\n };\n\n const applyReceiveAmountIssue = (issue: ReceiveAmountIssue | null) => {\n const key = issue ? `${issue.type}:${issue.message}` : \"\";\n receiveAmountIssueRef.current = issue;\n if (receiveAmountIssueKeyRef.current !== key) {\n receiveAmountIssueKeyRef.current = key;\n setReceiveAmountIssue(issue);\n }\n if (!issue) return;\n\n clearPendingSwapIntent(true);\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setTxError(null);\n };\n\n const getPredictiveExactInSourceTokens = () => {\n const expanded = getExpandedSourceTokens(fromTokens);\n if (expanded.length === 0) return [];\n\n return expanded\n .map((token) => {\n const userAmount =\n token.userAmount ||\n (expanded.length === 1 && hasPositiveDecimalInput(amount)\n ? amount\n : \"\");\n return { ...token, userAmount };\n })\n .filter((token) => hasPositiveDecimalInput(token.userAmount));\n };\n\n const sortUnifiedSourceTokens = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const fiatDiff = getTokenBalanceUsd(b).cmp(getTokenBalanceUsd(a));\n if (fiatDiff !== 0) return fiatDiff;\n return getTokenBalanceAmount(b).cmp(getTokenBalanceAmount(a));\n });\n\n const allocateUnifiedExactInToken = (\n token: SwapTokenOption,\n fallbackAmount?: string\n ) => {\n if (!token.isUnified || !token.sourceTokens?.length) return [token];\n\n const rawAmount =\n parseFiatNumber(token.userAmount || fallbackAmount) ?? new Decimal(0);\n if (rawAmount.lte(0)) return [];\n\n const sortedSources = sortUnifiedSourceTokens(token.sourceTokens).filter(\n (source) =>\n source.chainId &&\n source.contractAddress &&\n getTokenBalanceAmount(source).gt(0) &&\n hasMinimumSourceUsdBalance(source)\n );\n const allocated: SwapTokenOption[] = [];\n\n if (token.userAmountMode === \"usd\") {\n let remainingUsd = rawAmount;\n\n for (const source of sortedSources) {\n if (remainingUsd.lte(0)) break;\n\n const availableUsd = getTokenBalanceUsd(source);\n if (availableUsd.lte(0)) continue;\n\n const targetUsd = Decimal.min(remainingUsd, availableUsd);\n const tokenAmount = getTokenAmountForUsd(\n source,\n targetUsd\n ).toDecimalPlaces(\n Math.max(0, source.decimals || 18),\n Decimal.ROUND_DOWN\n );\n if (tokenAmount.lte(0)) continue;\n\n const actualUsd = getUsdForTokenAmount(source, tokenAmount);\n allocated.push({\n ...source,\n userAmount: tokenAmount.toFixed(),\n userAmountMode: \"token\",\n userAmountUsd: actualUsd\n .toDecimalPlaces(6, Decimal.ROUND_DOWN)\n .toFixed(),\n });\n remainingUsd = remainingUsd.minus(targetUsd);\n }\n\n return allocated;\n }\n\n let remainingTokenAmount = rawAmount;\n\n for (const source of sortedSources) {\n if (remainingTokenAmount.lte(0)) break;\n\n const availableTokenAmount = getTokenBalanceAmount(source);\n if (availableTokenAmount.lte(0)) continue;\n\n const tokenAmount = Decimal.min(\n remainingTokenAmount,\n availableTokenAmount\n ).toDecimalPlaces(Math.max(0, source.decimals || 18), Decimal.ROUND_DOWN);\n if (tokenAmount.lte(0)) continue;\n\n const actualUsd = getUsdForTokenAmount(source, tokenAmount);\n allocated.push({\n ...source,\n userAmount: tokenAmount.toFixed(),\n userAmountMode: \"token\",\n userAmountUsd: actualUsd\n .toDecimalPlaces(6, Decimal.ROUND_DOWN)\n .toFixed(),\n });\n remainingTokenAmount = remainingTokenAmount.minus(tokenAmount);\n }\n\n return allocated;\n };\n\n const getExactInSourceTokens = (\n tokens: SwapTokenOption[],\n fallbackAmount?: string\n ) =>\n tokens\n .flatMap((token) =>\n token.isUnified\n ? allocateUnifiedExactInToken(token, fallbackAmount)\n : [token]\n )\n .filter(hasMinimumSourceUsdBalance);\n\n const hasPositiveDecimalInput = (value: unknown) =>\n Boolean(parseFiatNumber(value)?.gt(0));\n\n const hasPositiveSourceSpend = (token: SwapTokenOption) =>\n hasPositiveDecimalInput(token.userAmount) ||\n hasPositiveDecimalInput(token.userAmountUsd);\n\n const getReadyExactInSourceTokens = (tokens: SwapTokenOption[]) =>\n getExactInSourceTokens(tokens).filter(\n (token) =>\n Boolean(token.chainId && token.contractAddress) &&\n hasPositiveDecimalInput(token.userAmount)\n );\n\n const hasReadyExactInSwapInput = (\n tokens: SwapTokenOption[],\n destination?: SwapTokenOption\n ) =>\n Boolean(\n destination?.chainId &&\n destination.contractAddress &&\n getReadyExactInSourceTokens(tokens).length > 0\n );\n\n const getExpandedSourceTokens = (tokens: SwapTokenOption[]) => {\n const expanded = tokens.flatMap((token) =>\n token.isUnified && token.sourceTokens?.length\n ? token.sourceTokens\n : [token]\n );\n const seen = new Set();\n return expanded.filter((token) => {\n if (!token.chainId || !token.contractAddress) return false;\n if (\n !isSwapSupportedBySdkChainList(\n token.chainId,\n swapSupportedChainsAndTokens\n )\n ) {\n return false;\n }\n const key = `${token.chainId}-${token.contractAddress.toLowerCase()}`;\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n });\n };\n\n const getNativeGasBalanceForChain = (chainId: number) => {\n const nativeSymbol =\n CHAIN_METADATA[chainId]?.nativeCurrency?.symbol?.toUpperCase();\n let balance = new Decimal(0);\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== chainId) continue;\n const breakdownSymbol = (\n breakdown.symbol ??\n asset.symbol ??\n \"\"\n ).toUpperCase();\n const assetSymbol = (asset.symbol ?? \"\").toUpperCase();\n const isNativeBalance =\n isNativeTokenAddress(breakdown.contractAddress) ||\n Boolean(\n nativeSymbol &&\n (breakdownSymbol === nativeSymbol || assetSymbol === nativeSymbol)\n );\n\n if (!isNativeBalance) continue;\n balance = balance.plus(\n parseFiatNumber(breakdown.balance) ?? new Decimal(0)\n );\n }\n }\n\n return balance;\n };\n\n const hasGasForSource = (token: SwapTokenOption) => {\n if (!token.chainId || !token.contractAddress) return false;\n const tokenBalance = parseFiatNumber(token.balance) ?? new Decimal(0);\n if (tokenBalance.lte(0)) return false;\n if (isNativeTokenAddress(token.contractAddress)) return true;\n return getNativeGasBalanceForChain(token.chainId).gt(0);\n };\n\n const getGasCapableBalanceSourceTokens = () => {\n const tokens: SwapTokenOption[] = [];\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const chainId = breakdown.chain?.id;\n const contractAddress = breakdown.contractAddress;\n const balance = parseFiatNumber(breakdown.balance) ?? new Decimal(0);\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n if (\n !chainId ||\n !contractAddress ||\n balance.lte(0) ||\n !fiatBalance ||\n fiatBalance.lt(minimumSourceUsd)\n )\n continue;\n\n const chainMeta = CHAIN_METADATA[chainId];\n const symbol = breakdown.symbol ?? asset.symbol;\n tokens.push({\n chainId,\n chainLogo: chainMeta?.logo ?? breakdown.chain?.logo,\n chainName: getShortChainName(\n chainId,\n chainMeta?.name ?? breakdown.chain?.name\n ),\n contractAddress,\n decimals: breakdown.decimals ?? asset.decimals ?? 18,\n logo: asset.logo ?? \"\",\n name: symbol,\n symbol,\n balance: `${breakdown.balance} ${symbol}`,\n balanceInFiat:\n fiatBalance !== undefined\n ? `$${fiatBalance.toDecimalPlaces(2).toFixed()}`\n : \"$0.00\",\n });\n }\n }\n\n return getExpandedSourceTokens(tokens).filter(hasGasForSource);\n };\n\n const getMinimumBalanceSourceTokens = () =>\n filterMinimumSourceUsdTokens(\n getExpandedSourceTokens(\n swapBalance\n ? deriveTokenOptions(swapBalance, swapSupportedChainsAndTokens)\n : []\n )\n );\n const getHeldDestinationTokenOption = () => {\n if (!toToken?.chainId || !toToken.contractAddress) return undefined;\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const chainId = breakdown.chain?.id;\n if (chainId !== toToken.chainId) continue;\n\n const breakdownAddress = breakdown.contractAddress;\n const addressMatches =\n breakdownAddress &&\n (breakdownAddress.toLowerCase() ===\n toToken.contractAddress.toLowerCase() ||\n (isNativeTokenAddress(breakdownAddress) &&\n isNativeTokenAddress(toToken.contractAddress)));\n const symbolMatches =\n (breakdown.symbol ?? asset.symbol ?? \"\").toUpperCase() ===\n toToken.symbol.toUpperCase();\n\n if (!addressMatches && !symbolMatches) continue;\n\n const balanceAmount = parseFiatNumber(breakdown.balance);\n if (!balanceAmount || balanceAmount.lte(0)) continue;\n\n const chainMeta = CHAIN_METADATA[chainId];\n const symbol = breakdown.symbol ?? asset.symbol ?? toToken.symbol;\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n\n return {\n chainId,\n chainLogo:\n chainMeta?.logo ?? breakdown.chain?.logo ?? toToken.chainLogo,\n chainName: getShortChainName(\n chainId,\n chainMeta?.name ?? breakdown.chain?.name ?? toToken.chainName\n ),\n contractAddress: breakdown.contractAddress ?? toToken.contractAddress,\n decimals:\n breakdown.decimals ?? asset.decimals ?? toToken.decimals ?? 18,\n logo: asset.logo ?? toToken.logo,\n name: symbol,\n symbol,\n balance: `${breakdown.balance} ${symbol}`,\n balanceInFiat:\n fiatBalance !== undefined\n ? `$${fiatBalance.toDecimalPlaces(2).toFixed()}`\n : \"$0.00\",\n } satisfies SwapTokenOption;\n }\n }\n\n return undefined;\n };\n\n const getDepositDestinationForSourceSelection = () => {\n const destination =\n activeMode === \"deposit\" ? selectedOpportunity : toToken;\n const chainId = destination?.chainId;\n const tokenAddress =\n activeMode === \"deposit\"\n ? selectedOpportunity?.tokenAddress\n : toToken?.contractAddress;\n const tokenSymbol =\n activeMode === \"deposit\"\n ? selectedOpportunity?.tokenSymbol\n : toToken?.symbol;\n\n if (!chainId || !tokenAddress || !tokenSymbol) return undefined;\n\n return {\n chainId,\n tokenAddress: tokenAddress as `0x${string}`,\n tokenSymbol,\n };\n };\n const getDestinationSourceIdForDeposit = () => {\n const destination = getDepositDestinationForSourceSelection();\n return destination\n ? getDepositSourceId(destination.tokenAddress, destination.chainId)\n : undefined;\n };\n const getDepositSourceTargetUsd = () => {\n if (activeMode !== \"deposit\") return undefined;\n const requestedUsd = depositUsdDecimal;\n if (!requestedUsd || requestedUsd.lte(0)) return undefined;\n\n const coverage = getExactOutDestinationBalanceCoverage({\n requestedAmount: depositTokenAmountForQuote,\n requestedUsd,\n token: toToken,\n });\n return Decimal.max(\n requestedUsd.minus(coverage?.usd ?? new Decimal(0)),\n new Decimal(0)\n );\n };\n\n const getDepositSourceIdsFromTokens = (tokens: SwapTokenOption[]) =>\n getExpandedSourceTokens(tokens)\n .filter((token) => token.chainId && token.contractAddress)\n .map((token) =>\n getDepositSourceId(token.contractAddress, token.chainId!)\n );\n\n const getDepositTokenOptionsBySourceId = () => {\n const map = new Map();\n const sourceTokens = [\n ...(swapBalance\n ? deriveTokenOptions(swapBalance, swapSupportedChainsAndTokens)\n : []),\n ...fromTokens,\n ];\n\n for (const token of getExpandedSourceTokens(sourceTokens)) {\n if (!token.chainId || !token.contractAddress) continue;\n const id = getDepositSourceId(token.contractAddress, token.chainId);\n if (!map.has(id)) {\n map.set(id, {\n ...token,\n userAmount: \"\",\n });\n }\n }\n\n return map;\n };\n\n const getDepositSourceTokensForIds = (sourceIds: string[]) => {\n const tokenBySourceId = getDepositTokenOptionsBySourceId();\n return sourceIds\n .map((sourceId) => tokenBySourceId.get(sourceId))\n .filter((token): token is SwapTokenOption => Boolean(token))\n .map((token) => ({ ...token, userAmount: \"\" }));\n };\n\n const getResolvedDepositSourceSelection = (options?: {\n filter?: DepositSourceFilter;\n selectedTokens?: SwapTokenOption[];\n isManualSelection?: boolean;\n targetAmountUsd?: Decimal;\n }) => {\n const destination = getDepositDestinationForSourceSelection();\n if (!destination) {\n return { sourcePoolIds: [], selectedSourceIds: [], fromSources: [] };\n }\n\n const manualSelection =\n options?.isManualSelection ?? sourceSelectionTouched;\n const selectedTokensForResolution = options?.selectedTokens ?? fromTokens;\n const selectedSourceIds = getDepositSourceIdsFromTokens(\n selectedTokensForResolution\n );\n const destinationSourceId = getDestinationSourceIdForDeposit();\n const targetAmountUsd =\n options?.targetAmountUsd ??\n (activeMode === \"deposit\"\n ? getDepositSourceTargetUsd()\n : activeMode === \"send\"\n ? new Decimal(sendAmountUsd || 0)\n : undefined);\n\n return resolveDepositSourceSelection({\n swapBalance,\n destination,\n filter: manualSelection\n ? \"custom\"\n : (options?.filter ?? depositSourceFilter),\n selectedSourceIds,\n isManualSelection: manualSelection,\n minimumBalanceUsd: minimumSourceUsd.toNumber(),\n targetAmountUsd: targetAmountUsd?.toNumber(),\n excludedSourceIds: destinationSourceId ? [destinationSourceId] : [],\n });\n };\n\n const getExactOutSourceTokens = (\n mode: \"all\" | \"selected\" = exactOutQuoteSourceModeRef.current,\n targetAmountUsd?: Decimal\n ) => {\n if (activeMode === \"deposit\") {\n const selection = getResolvedDepositSourceSelection({ targetAmountUsd });\n return getDepositSourceTokensForIds(\n mode === \"all\" ? selection.sourcePoolIds : selection.selectedSourceIds\n );\n }\n\n if (activeMode === \"send\" && mode === \"selected\" && fromTokens.length > 0) {\n return filterMinimumSourceUsdTokens(\n getExpandedSourceTokens(fromTokens)\n ).filter(hasGasForSource);\n }\n\n return getGasCapableBalanceSourceTokens();\n };\n\n const buildExplicitSourcesPayload = (tokens: SwapTokenOption[]) => {\n const eligibleTokens = filterMinimumSourceUsdTokens(tokens).filter(\n (token) => token.chainId && token.contractAddress\n );\n return {\n sources: eligibleTokens.map((token) => ({\n chainId: token.chainId!,\n tokenAddress: token.contractAddress as `0x${string}`,\n })),\n };\n };\n\n const getSdkSourceKey = (source: {\n chainId: number;\n tokenAddress: `0x${string}`;\n }) => {\n const normalizedAddress = isNativeTokenAddress(source.tokenAddress)\n ? zeroAddress\n : source.tokenAddress.toLowerCase();\n return `${source.chainId}:${normalizedAddress}`;\n };\n\n const dedupeSdkSources = (\n sources: Array<\n | {\n chainId: number;\n tokenAddress: `0x${string}`;\n }\n | undefined\n >\n ) => {\n const seen = new Set();\n return sources.filter(\n (\n source\n ): source is {\n chainId: number;\n tokenAddress: `0x${string}`;\n } => {\n if (!source) return false;\n const key = getSdkSourceKey(source);\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n }\n );\n };\n\n const getHeldDestinationTokenSource = () => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n if (\n !heldDestinationToken?.chainId ||\n !heldDestinationToken.contractAddress\n ) {\n return undefined;\n }\n\n return {\n chainId: heldDestinationToken.chainId,\n tokenAddress: heldDestinationToken.contractAddress as `0x${string}`,\n };\n };\n\n const shouldSendExactOutSourceAllowlist = () => {\n if (activeMode === \"deposit\") {\n return sourceSelectionTouched || depositSourceFilter !== \"all\";\n }\n\n if (activeMode === \"send\") {\n return exactOutQuoteSourceModeRef.current === \"selected\";\n }\n\n return true;\n };\n\n const buildExactOutSourcesPayload = (tokens: SwapTokenOption[]) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n return buildExplicitSourcesPayload(tokens);\n }\n\n const sourceTokens = shouldSendExactOutSourceAllowlist()\n ? tokens\n : getExactOutSourceTokens(\"all\");\n const explicitSources = buildExplicitSourcesPayload(sourceTokens).sources;\n const sources = dedupeSdkSources([\n ...explicitSources,\n getHeldDestinationTokenSource(),\n ]);\n\n return sources.length > 0 ? { sources } : {};\n };\n\n const buildPredictiveExactOutSources = async (requiredSourceUsd: Decimal) => {\n if (requiredSourceUsd.lte(0)) return [];\n\n const destinationKey = getTokenSelectionKey(toToken);\n const candidates = sortExactOutSourcesBySdkPriority(\n getExactOutSourceTokens(\n exactOutQuoteSourceModeRef.current,\n requiredSourceUsd\n )\n .filter((token) => getTokenSelectionKey(token) !== destinationKey)\n .filter(hasMinimumSourceUsdBalance)\n );\n const sources: SwapTokenOption[] = [];\n let remainingUsd = requiredSourceUsd;\n\n for (const token of candidates) {\n if (remainingUsd.lte(0)) break;\n\n const availableUsd = getTokenBalanceUsd(token);\n if (availableUsd.lt(minimumSourceUsd)) continue;\n\n const rate = await resolveUsdRateForToken(token);\n if (rate.lte(0)) continue;\n\n const targetUsd = Decimal.min(remainingUsd, availableUsd);\n const tokenAmount = targetUsd\n .div(rate)\n .toDecimalPlaces(Math.max(0, token.decimals || 18), Decimal.ROUND_DOWN);\n if (tokenAmount.lte(0)) continue;\n\n sources.push({\n ...token,\n userAmount: tokenAmount.toFixed(),\n userAmountMode: \"token\",\n userAmountUsd: targetUsd\n .toDecimalPlaces(6, Decimal.ROUND_DOWN)\n .toFixed(),\n });\n remainingUsd = remainingUsd.minus(targetUsd);\n }\n\n return remainingUsd.gt(0.01) ? [] : sources;\n };\n\n const getErrorText = (error: unknown) => {\n const err = error as any;\n const parts = [\n err?.message,\n typeof error === \"string\" ? error : undefined,\n err?.code,\n ];\n\n try {\n if (err?.data) parts.push(JSON.stringify(err.data));\n } catch {\n // Ignore non-serializable SDK error metadata.\n }\n\n return parts.filter(Boolean).join(\" \");\n };\n\n const isInsufficientSourcesError = (error: unknown) => {\n const err = error as any;\n const message = getErrorText(error).toLowerCase();\n\n return (\n err?.code === ERROR_CODES.INSUFFICIENT_BALANCE ||\n message.includes(\"insufficient balance\") ||\n message.includes(\"sources are not enough\") ||\n (message.includes(\"source\") && message.includes(\"not enough\"))\n );\n };\n\n const isViemInvalidDecimalError = (error: unknown) =>\n getErrorText(error).toLowerCase().includes(\"not a valid decimal number\");\n\n const parseLabeledErrorDecimal = (text: string, label: string) => {\n const match = text.match(\n new RegExp(`${label}\\\\s*:\\\\s*\\\\$?\\\\s*([0-9][0-9,]*(?:\\\\.[0-9]+)?)`, \"i\")\n );\n return match ? parseFiatNumber(match[1]) : undefined;\n };\n\n const getExactOutRequestedUsd = () => {\n const amountNumber = parseFiatNumber(amount);\n if (!amountNumber || amountNumber.lte(0) || !toToken?.symbol) {\n return undefined;\n }\n\n if (activeMode === \"deposit\" && depositAmountMode === \"usd\") {\n return amountNumber;\n }\n\n const fiatValue = getFiatValue(amountNumber.toNumber(), toToken.symbol);\n return Number.isFinite(fiatValue) && fiatValue > 0\n ? new Decimal(fiatValue)\n : undefined;\n };\n\n const getExactOutAvailableSourceUsd = (\n sourceTokensOverride?: SwapTokenOption[]\n ): Decimal => {\n const sumTokensWithDestinationCredit = (\n tokens: SwapTokenOption[]\n ): Decimal => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n const heldDestinationKey = getTokenSelectionKey(heldDestinationToken);\n let hasHeldDestinationToken = false;\n\n const sourceTotal = tokens.reduce((sum, token) => {\n const value = parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n const isHeldDestinationToken =\n heldDestinationKey &&\n getTokenSelectionKey(token) === heldDestinationKey;\n if (isHeldDestinationToken) {\n hasHeldDestinationToken = true;\n }\n\n return value.gt(0) &&\n (isHeldDestinationToken || value.gte(minimumSourceUsd))\n ? sum.plus(value)\n : sum;\n }, new Decimal(0));\n\n if (!heldDestinationToken || hasHeldDestinationToken) {\n return sourceTotal;\n }\n\n const heldDestinationUsd =\n parseFiatNumber(heldDestinationToken.balanceInFiat) ?? new Decimal(0);\n return heldDestinationUsd.gt(0)\n ? sourceTotal.plus(heldDestinationUsd)\n : sourceTotal;\n };\n\n if (\n sourceTokensOverride ||\n exactOutQuoteSourceModeRef.current === \"selected\"\n ) {\n const selectedSourceTokens = sourceTokensOverride ?? fromTokens;\n return sumTokensWithDestinationCredit(selectedSourceTokens);\n }\n\n return getExactOutTotalSourceBalanceUsd();\n };\n const getExactOutTotalSourceBalanceUsd = (): Decimal => {\n const allSourceTotal: Decimal = getExactOutAvailableSourceUsd(\n getMinimumBalanceSourceTokens()\n );\n\n return allSourceTotal.gt(0) ? allSourceTotal : getSwapBalanceTotalUsd();\n };\n\n const getExactOutIntentSourceUsd = () => {\n const intentSourceUsd = (intentData?.sources ?? []).reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0)\n );\n return intentSourceUsd.gt(0) ? intentSourceUsd : undefined;\n };\n\n const getExactOutHeldDestinationUsd = () => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n const value = parseFiatNumber(heldDestinationToken?.balanceInFiat);\n return value && value.gt(0) ? value : new Decimal(0);\n };\n\n const exactOutIntentIncludesHeldDestination = () => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n const heldDestinationKey = getTokenSelectionKey(heldDestinationToken);\n if (!heldDestinationKey) return false;\n\n return (intentData?.sources ?? []).some((source) => {\n const chainId = source.chain?.id;\n const tokenAddress = source.token?.contractAddress;\n if (!chainId || !tokenAddress) return false;\n\n return (\n getTokenSelectionKey({\n chainId,\n contractAddress: tokenAddress,\n } as SwapTokenOption) === heldDestinationKey\n );\n });\n };\n\n const getExactOutRequiredSourceUsd = () => {\n const intentSourceUsd = getExactOutIntentSourceUsd();\n if (intentSourceUsd) return intentSourceUsd;\n return undefined;\n };\n\n const getExactOutRequiredFundingUsd = () => {\n const sourceRequiredUsd = getExactOutRequiredSourceUsd();\n if (sourceRequiredUsd) {\n const destinationCreditUsd = exactOutIntentIncludesHeldDestination()\n ? new Decimal(0)\n : getExactOutHeldDestinationUsd();\n return sourceRequiredUsd.plus(destinationCreditUsd);\n }\n\n return getExactOutRequestedUsd();\n };\n\n const getExactInSourceDeficitUsd = () => {\n if (swapType !== \"exactIn\" || fromTokens.length === 0) return undefined;\n\n return fromTokens.reduce((sum, token) => {\n const requestedAmount = parseFiatNumber(token.userAmount);\n if (!requestedAmount || requestedAmount.lte(0)) return sum;\n\n if (token.userAmountMode === \"usd\") {\n const availableUsd = parseFiatNumber(token.balanceInFiat);\n if (!availableUsd || requestedAmount.lte(availableUsd)) return sum;\n return sum.plus(requestedAmount.minus(availableUsd));\n }\n\n const availableTokenAmount = parseFiatNumber(token.balance);\n if (!availableTokenAmount || requestedAmount.lte(availableTokenAmount)) {\n return sum;\n }\n\n const missingTokenAmount = requestedAmount.minus(availableTokenAmount);\n const fiatBalance = parseFiatNumber(token.balanceInFiat);\n if (fiatBalance && availableTokenAmount.gt(0)) {\n return sum.plus(\n missingTokenAmount.mul(fiatBalance.div(availableTokenAmount))\n );\n }\n\n return sum;\n }, new Decimal(0));\n };\n\n const buildInsufficientSourcesIssue = (error: unknown): SwapQuoteIssue => {\n const errorText = getErrorText(error);\n const details =\n (error as any)?.data?.details ?? (error as any)?.details ?? {};\n const requiredFromError =\n parseFiatNumber(\n details.requiredUsd ??\n details.requiredUSD ??\n details.requiredAmountUsd ??\n details.requiredAmount ??\n details.required\n ) ?? parseLabeledErrorDecimal(errorText, \"required\");\n const availableFromError =\n parseFiatNumber(\n details.availableUsd ??\n details.availableUSD ??\n details.availableAmountUsd ??\n details.availableAmount ??\n details.available\n ) ?? parseLabeledErrorDecimal(errorText, \"available\");\n const requestedUsd = getExactOutRequiredFundingUsd();\n const availableUsd = getExactOutAvailableSourceUsd();\n const exactInSourceDeficitUsd = getExactInSourceDeficitUsd();\n\n let missingUsd =\n exactInSourceDeficitUsd && exactInSourceDeficitUsd.gt(0)\n ? exactInSourceDeficitUsd\n : requiredFromError && availableFromError\n ? requiredFromError.minus(availableFromError)\n : undefined;\n\n if (\n requestedUsd &&\n (!missingUsd || missingUsd.lte(0) || missingUsd.gt(requestedUsd.mul(5)))\n ) {\n missingUsd = requestedUsd.minus(availableUsd);\n }\n\n if (missingUsd && missingUsd.gt(0)) {\n const formattedMissing =\n missingUsd.gt(0) && missingUsd.lt(0.01)\n ? \"<$0.01\"\n : formatUsdDisplay(missingUsd);\n\n return {\n type: \"insufficientSources\",\n missingUsd: missingUsd.toDecimalPlaces(2).toFixed(),\n message: `Need ${formattedMissing} more across your assets`,\n };\n }\n\n return {\n type: \"insufficientSources\",\n message: \"Add more source balance across your assets\",\n };\n };\n\n const buildExactOutSourceBalanceIssue = (\n sourceTokensOverride?: SwapTokenOption[]\n ): SwapQuoteIssue | null => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return null;\n const requestedUsd = getExactOutRequiredFundingUsd();\n const availableUsd = getExactOutAvailableSourceUsd(sourceTokensOverride);\n if (!requestedUsd || requestedUsd.lte(0) || !availableUsd) return null;\n\n const missingUsd = requestedUsd.minus(availableUsd);\n if (missingUsd.lte(0.01)) return null;\n\n const formattedMissing =\n missingUsd.gt(0) && missingUsd.lt(0.01)\n ? \"<$0.01\"\n : formatUsdDisplay(missingUsd);\n\n return {\n type: \"insufficientSources\",\n missingUsd: missingUsd.toDecimalPlaces(2).toFixed(),\n message: `Need ${formattedMissing} more across your assets`,\n };\n };\n\n const isNativeTokenAddress = (address?: string) =>\n !address ||\n address.toLowerCase() === \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\" ||\n address.toLowerCase() === \"0x0000000000000000000000000000000000000000\";\n\n const formatReadableTokenAmount = (rawAmount: bigint, decimals: number) =>\n new Decimal(rawAmount.toString())\n .div(new Decimal(10).pow(decimals))\n .toFixed();\n\n const formatReadableTokenBalanceAmount = (\n rawAmount: bigint,\n decimals: number\n ) =>\n new Decimal(rawAmount.toString())\n .div(new Decimal(10).pow(decimals))\n .toDecimalPlaces(6)\n .toFixed();\n\n const trimDecimalString = (value: string) =>\n value.replace(/(\\.\\d*?)0+$/, \"$1\").replace(/\\.$/, \"\");\n\n const buildSourceTokenSnapshotMap = useCallback(\n (balances: UserAsset[] | null | undefined) => {\n const snapshots = new Map();\n\n for (const asset of balances ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const chainId = breakdown.chain?.id;\n const contractAddress = breakdown.contractAddress;\n const symbol = breakdown.symbol ?? asset.symbol;\n\n if (\n !chainId ||\n !contractAddress ||\n !symbol ||\n !isSwapSupportedBySdkChainList(\n chainId,\n swapSupportedChainsAndTokens\n )\n ) {\n continue;\n }\n\n const chainMeta = CHAIN_METADATA[chainId];\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n const snapshot: SwapTokenOption = {\n balance: `${breakdown.balance ?? \"0\"} ${symbol}`,\n balanceInFiat: fiatBalance\n ? formatUsdDisplay(fiatBalance)\n : \"$0.00\",\n chainId,\n chainLogo: chainMeta?.logo ?? breakdown.chain?.logo,\n chainName: getShortChainName(\n chainId,\n chainMeta?.name ?? breakdown.chain?.name\n ),\n contractAddress,\n decimals: breakdown.decimals ?? asset.decimals ?? 18,\n logo: asset.logo ?? \"\",\n name: symbol,\n symbol,\n };\n snapshots.set(getTokenSelectionKey(snapshot), snapshot);\n }\n }\n\n return snapshots;\n },\n [swapSupportedChainsAndTokens]\n );\n\n const patchSourceTokensWithBalances = useCallback(\n (tokens: SwapTokenOption[], balances: UserAsset[]) => {\n const snapshots = buildSourceTokenSnapshotMap(balances);\n\n const updateToken = (token: SwapTokenOption): SwapTokenOption => {\n const preservedAmounts = {\n userAmount: token.userAmount,\n userAmountMode: token.userAmountMode,\n userAmountUsd: token.userAmountUsd,\n };\n\n if (token.isUnified) {\n const sourceTokens = (token.sourceTokens ?? []).map(updateToken);\n const totalBalance = sourceTokens.reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber(source.balance) ?? new Decimal(0)),\n new Decimal(0)\n );\n const totalFiat = sourceTokens.reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber(source.balanceInFiat) ?? new Decimal(0)),\n new Decimal(0)\n );\n\n return {\n ...token,\n ...preservedAmounts,\n balance: totalBalance.toDecimalPlaces(8).toFixed(),\n balanceInFiat: formatUsdDisplay(totalFiat),\n sourceTokens,\n };\n }\n\n const snapshot = snapshots.get(getTokenSelectionKey(token));\n if (!snapshot) {\n return {\n ...token,\n ...preservedAmounts,\n balance: `0 ${token.symbol}`,\n balanceInFiat: \"$0.00\",\n chainLogo:\n token.chainLogo ??\n (token.chainId ? CHAIN_METADATA[token.chainId]?.logo : undefined),\n chainName: getShortChainName(token.chainId, token.chainName),\n };\n }\n\n return {\n ...token,\n ...snapshot,\n ...preservedAmounts,\n };\n };\n\n return tokens.map(updateToken);\n },\n [buildSourceTokenSnapshotMap]\n );\n\n const refreshSelectedSourceBalances = useCallback(async () => {\n const refreshedBalance = await fetchSwapBalance();\n const balances = refreshedBalance ?? swapBalance;\n if (!balances) return;\n\n setFromTokens((current) =>\n current.length === 0\n ? current\n : patchSourceTokensWithBalances(current, balances)\n );\n setSourceSelectionRevision((current) => current + 1);\n }, [fetchSwapBalance, patchSourceTokensWithBalances, swapBalance]);\n\n const receiveMaxSafetyMultiplier = new Decimal(\"0.9\");\n const currentSwapEntry =\n currentSwapId !== null\n ? swapHistory.find((entry) => entry.id === currentSwapId)\n : undefined;\n\n const scheduleTerminalBalanceRefresh = () => {\n if (terminalBalanceRefreshTimerRef.current) {\n clearTimeout(terminalBalanceRefreshTimerRef.current);\n }\n\n terminalBalanceRefreshTimerRef.current = setTimeout(() => {\n terminalBalanceRefreshTimerRef.current = null;\n void fetchSwapBalance();\n }, BALANCE_REFRESH_AFTER_TERMINAL_MS);\n };\n\n const patchSwapHistoryEntry = (\n id: string | null | undefined,\n patch: Partial\n ) => {\n if (!id) return;\n setSwapHistory((prev) =>\n sortSwapHistoryEntries(\n prev.map((entry) => (entry.id === id ? { ...entry, ...patch } : entry))\n )\n );\n };\n\n const patchCurrentSwapHistoryEntry = (patch: Partial) => {\n patchSwapHistoryEntry(currentSwapIdRef.current, patch);\n };\n\n const patchCurrentIntentExplorerUrl = (url?: string | null) => {\n if (!isHttpUrl(url)) return;\n if (intentUrlRef.current === url) return;\n\n intentUrlRef.current = url;\n const intentId = extractIntentIdFromUrl(url);\n patchCurrentSwapHistoryEntry({\n intentExplorerUrl: url,\n ...(intentId ? { intentId } : {}),\n });\n };\n\n const resetExplorerUrls = () => {\n const next = { sourceExplorerUrl: null, destinationExplorerUrl: null };\n explorerUrlsRef.current = next;\n setExplorerUrls(next);\n };\n\n const mergeExplorerUrls = (\n patch: Partial<{\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n }>\n ) => {\n const next = { ...explorerUrlsRef.current, ...patch };\n explorerUrlsRef.current = next;\n setExplorerUrls(next);\n patchCurrentSwapHistoryEntry({\n sourceExplorerUrl: next.sourceExplorerUrl,\n finalExplorerUrl: next.destinationExplorerUrl ?? next.sourceExplorerUrl,\n });\n };\n\n const resetProgressEvents = () => {\n progressEventsRef.current = [];\n setProgressEvents((current) => (current.length === 0 ? current : []));\n setFailedProgressStep((current) => (current === null ? current : null));\n };\n\n const appendProgressEvent = (\n name: string,\n step: SwapStepType | BridgeStepType | undefined,\n defaultCompleted: boolean,\n event?: unknown\n ) => {\n if (!step) return;\n const completed =\n typeof (step as any).completed === \"boolean\"\n ? Boolean((step as any).completed)\n : defaultCompleted;\n\n setProgressEvents((prev) => {\n const next = [\n ...prev,\n {\n id: `${Date.now()}-${prev.length}-${(step as any).typeID ?? (step as any).type ?? name}`,\n name,\n completed,\n event,\n step,\n },\n ];\n progressEventsRef.current = next;\n return next;\n });\n };\n\n const appendProgressListEvent = (\n name: string,\n stepList: Array\n ) => {\n if (stepList.length === 0) return;\n\n setProgressEvents((prev) => {\n const next = [\n ...prev,\n {\n id: `${Date.now()}-${prev.length}-${name}`,\n name,\n completed: false,\n step: stepList[0],\n steps: stepList,\n },\n ];\n progressEventsRef.current = next;\n return next;\n });\n };\n\n const startSwapHistoryEntry = () => {\n const id = `${Date.now()}-${swapRunIdRef.current}`;\n const now = Date.now();\n const resolvedToToken =\n toToken && destinationBalance\n ? { ...toToken, balance: destinationBalance }\n : toToken;\n const entry: SwapHistoryEntry = {\n id,\n mode: activeMode,\n status: \"pending\",\n createdAt: now,\n startedAt: now,\n intentData,\n fromTokens,\n toToken: resolvedToToken,\n requestedToAmount:\n activeMode === \"deposit\" || activeMode === \"send\"\n ? previewDestinationAmount\n : undefined,\n requestedToValue:\n activeMode === \"deposit\" || activeMode === \"send\"\n ? previewToAmountUsd\n : undefined,\n recipientAddress: transferRecipientAddress,\n opportunity: selectedOpportunity,\n feeUsd: intentFeeUsd,\n sourceExplorerUrl: null,\n finalExplorerUrl: null,\n intentExplorerUrl: isHttpUrl(intentUrlRef.current)\n ? intentUrlRef.current\n : null,\n intentId: extractIntentIdFromUrl(intentUrlRef.current),\n autoRefundAvailable: false,\n };\n\n currentSwapStartedAtRef.current = 0;\n currentSwapIdRef.current = id;\n setCurrentSwapId(id);\n setSwapHistory((prev) => sortSwapHistoryEntries([entry, ...prev]));\n return id;\n };\n\n const finishCurrentSwapHistoryEntry = (\n status: \"fulfilled\" | \"failed\" | \"timeout\",\n patch: Partial = {}\n ) => {\n const now = Date.now();\n const startedAt = currentSwapStartedAtRef.current || now;\n patchSwapHistoryEntry(currentSwapIdRef.current, {\n status,\n endedAt: now,\n durationSeconds: Math.max(1, Math.round((now - startedAt) / 1000)),\n sourceExplorerUrl: explorerUrlsRef.current.sourceExplorerUrl,\n finalExplorerUrl:\n explorerUrlsRef.current.destinationExplorerUrl ??\n explorerUrlsRef.current.sourceExplorerUrl,\n ...patch,\n });\n scheduleTerminalBalanceRefresh();\n };\n\n const markSwapExecutionStarted = () => {\n if (currentSwapStartedAtRef.current > 0) return;\n const now = Date.now();\n currentSwapStartedAtRef.current = now;\n patchCurrentSwapHistoryEntry({ startedAt: now });\n };\n\n const enterSkippedSwapProgress = () => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n\n const shouldInitializeProgress = swapStepRef.current !== \"progress\";\n if (!currentSwapIdRef.current) {\n onStart?.();\n startSwapHistoryEntry();\n }\n\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setPreviewQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setSwapQuoteIssue(null);\n\n if (shouldInitializeProgress) {\n resetProgressEvents();\n swapStepsListRef.current = [];\n resetSteps();\n swapStepRef.current = \"progress\";\n setSwapStep(\"progress\");\n }\n };\n\n const cachePredictiveBaselineFromIntent = (intent: SwapIntentData) => {\n const destinationAmount = parseFiatNumber(intent.destination?.amount);\n const destinationValue = parseFiatNumber(intent.destination?.value);\n const sourceUsd = (intent.sources ?? []).reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0)\n );\n\n if (!destinationAmount || destinationAmount.lte(0)) return;\n\n const destinationUsdRate =\n destinationValue && destinationValue.gt(0)\n ? destinationValue.div(destinationAmount)\n : getUsdRateForSymbol(intent.destination?.token?.symbol);\n if (destinationUsdRate.lte(0)) return;\n\n cacheSymbolUsdRate(intent.destination?.token?.symbol, destinationUsdRate);\n\n const key = getPredictiveQuoteCacheKey();\n if (!key) return;\n\n const baseline: PredictiveQuoteBaseline = {\n destinationUsdRate: destinationUsdRate.toDecimalPlaces(18).toFixed(),\n updatedAt: Date.now(),\n };\n\n if (activeMode === \"swap\" && swapType === \"exactIn\" && sourceUsd.gt(0)) {\n baseline.exactInDestinationAmountPerSourceUsd = destinationAmount\n .div(sourceUsd)\n .toDecimalPlaces(18)\n .toFixed();\n }\n\n const resolvedDestinationValue =\n destinationValue && destinationValue.gt(0)\n ? destinationValue\n : destinationAmount.mul(destinationUsdRate);\n if (\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n resolvedDestinationValue.gt(0) &&\n sourceUsd.gt(0)\n ) {\n baseline.exactOutSourceUsdPerDestinationUsd = sourceUsd\n .div(resolvedDestinationValue)\n .toDecimalPlaces(18)\n .toFixed();\n }\n\n predictiveQuoteCacheRef.current[key] = baseline;\n };\n\n const applySwapIntent = useCallback(\n (intent: SwapIntentData) => {\n const sortedIntent = {\n ...intent,\n sources: sortIntentSourcesByUsdDesc(intent.sources ?? []),\n };\n const enrichedIntent = enrichSwapIntentTokenMetadata(sortedIntent);\n const sortedIntentSourceTokens = sortSwapTokensByUsdDesc(\n (enrichedIntent.sources ?? []).map(buildIntentSourceToken)\n );\n\n lastSwapIntentRefreshAtRef.current = Date.now();\n lastIntentSourceTokensRef.current = sortedIntentSourceTokens;\n if (\n !sourceSelectionTouched &&\n (activeMode === \"send\" ||\n (activeMode === \"deposit\" && swapType === \"exactOut\"))\n ) {\n lastAutoIntentSourceTokensRef.current = sortedIntentSourceTokens;\n }\n cacheDestinationUsdRateFromIntent(enrichedIntent);\n cachePredictiveBaselineFromIntent(enrichedIntent);\n setIntentData(enrichedIntent);\n setIntentToAmount(enrichedIntent.destination?.amount || undefined);\n setSwapQuoteIssue(null);\n\n if (\n !sourceSelectionTouched &&\n swapStepRef.current !== \"choose-swap-asset\" &&\n (activeMode === \"send\" ||\n (activeMode === \"deposit\" && swapType === \"exactOut\"))\n ) {\n syncingIntentSourcesRef.current = true;\n setFromTokens(sortedIntentSourceTokens);\n }\n\n try {\n const bridgeFees = enrichedIntent.feesAndBuffer?.bridge;\n const bridgeFeeData =\n bridgeFees && typeof bridgeFees === \"object\" ? bridgeFees : undefined;\n const collectionFee = parseFiatNumber(bridgeFeeData?.collection);\n const fulfilmentFee = parseFiatNumber(bridgeFeeData?.fulfilment);\n const executionGasFee =\n parseFiatNumber(bridgeFeeData?.caGas) ??\n (collectionFee !== undefined || fulfilmentFee !== undefined\n ? (collectionFee ?? new Decimal(0)).plus(\n fulfilmentFee ?? new Decimal(0)\n )\n : undefined);\n const bridgeGasSuppliedFee = parseFiatNumber(\n bridgeFeeData?.gasSupplied\n );\n const destinationGasSuppliedFee =\n getIntentDestinationGasUsdValue(enrichedIntent);\n const gasSuppliedFee =\n bridgeGasSuppliedFee ?? destinationGasSuppliedFee;\n const bridgeComponentsTotal = bridgeFeeData\n ? [\n executionGasFee,\n parseFiatNumber(bridgeFeeData.protocol),\n parseFiatNumber(bridgeFeeData.solver),\n gasSuppliedFee,\n ].reduce(\n (sum, value) => sum.plus(value ?? new Decimal(0)),\n new Decimal(0)\n )\n : undefined;\n const rawBridgeTotal =\n typeof bridgeFees === \"string\"\n ? parseFiatNumber(bridgeFees)\n : parseFiatNumber(bridgeFeeData?.total);\n const bridgeTotal =\n rawBridgeTotal &&\n !bridgeGasSuppliedFee &&\n destinationGasSuppliedFee &&\n destinationGasSuppliedFee.gt(0)\n ? rawBridgeTotal.plus(destinationGasSuppliedFee)\n : (rawBridgeTotal ??\n (bridgeComponentsTotal && bridgeComponentsTotal.gt(0)\n ? bridgeComponentsTotal\n : destinationGasSuppliedFee));\n\n if (bridgeTotal !== undefined) {\n setIntentFeeUsd(\n bridgeTotal.gt(0) ? bridgeTotal.toDecimalPlaces(6).toFixed() : \"0\"\n );\n } else {\n setIntentFeeUsd(undefined);\n }\n } catch (err) {\n console.warn(\"Could not resolve bridge fee total\", err);\n setIntentFeeUsd(undefined);\n }\n },\n [\n activeMode,\n fromTokens,\n getFiatValue,\n sourceSelectionTouched,\n swapType,\n swapBalance,\n swapSupportedChainsAndTokens,\n supportedChainsAndTokens,\n toToken,\n ]\n );\n\n const handleSwapIntentCallback = useCallback(\n (data: any, runId: number, quoteInputKey: string) => {\n const { intent, allow, deny, refresh } = data;\n const bridgeProvider = normalizeBridgeProvider(\n data?.bridgeProvider ??\n intent?.bridgeProvider ??\n intent?.normalizedIntent?.bridgeProvider ??\n intent?.swap?.bridgeProvider\n );\n const intentWithBridgeProvider = normalizeRenderableSwapIntentData(\n intent,\n bridgeProvider\n );\n logSdkIntentEvent(\"onIntent\", data, {\n bridgeProvider,\n currentRunId: swapRunIdRef.current,\n normalizedIntent: intentWithBridgeProvider,\n isCurrentRun: swapRunIdRef.current === runId,\n quoteInputKey,\n runId,\n });\n if (swapRunIdRef.current !== runId) {\n logSdkIntentEvent(\"ignored stale onIntent\", data, {\n currentRunId: swapRunIdRef.current,\n quoteInputKey,\n runId,\n });\n deny();\n return;\n }\n if (quoteInputKey && activeQuoteInputKeyRef.current !== quoteInputKey) {\n logSdkIntentEvent(\"ignored stale quote input onIntent\", data, {\n currentQuoteInputKey: activeQuoteInputKeyRef.current,\n quoteInputKey,\n runId,\n });\n deny();\n return;\n }\n if (!intentWithBridgeProvider) {\n console.warn(\"[NexusWidget SDK][intent] Unsupported intent shape\", {\n intent,\n raw: data,\n });\n finishIntentFetchTiming(runId, \"failed\");\n deny();\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setTxError(\"Quote unavailable\");\n return;\n }\n const resolvedQuoteInputKey = quoteInputKey;\n const normalizedRefresh =\n typeof refresh === \"function\"\n ? async (...args: unknown[]) => {\n const refreshed = await refresh(...args);\n const refreshedBridgeProvider = normalizeBridgeProvider(\n refreshed?.bridgeProvider ??\n refreshed?.normalizedIntent?.bridgeProvider ??\n refreshed?.swap?.bridgeProvider ??\n bridgeProvider\n );\n return (\n normalizeRenderableSwapIntentData(\n refreshed,\n refreshedBridgeProvider\n ) ?? refreshed\n );\n }\n : refresh;\n finishIntentFetchTiming(runId);\n providerSwapIntent.current = {\n intent: intentWithBridgeProvider as any,\n allow,\n deny,\n refresh: normalizedRefresh,\n };\n swapIntentRef.current = {\n intent: intentWithBridgeProvider,\n allow,\n deny,\n refresh: normalizedRefresh,\n runId,\n quoteInputKey: resolvedQuoteInputKey,\n };\n flushSync(() => {\n applySwapIntent(intentWithBridgeProvider);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n });\n },\n [applySwapIntent, finishIntentFetchTiming, providerSwapIntent]\n );\n\n // Deposit-specific\n const [selectedOpportunity, setSelectedOpportunity] = useState<\n NexusWidgetDepositOpportunityConfig | undefined\n >(() => (activeMode === \"deposit\" ? configuredDeposit : undefined));\n const selectedOpportunityIdentity =\n getDepositConfigIdentity(selectedOpportunity);\n const [depositAmountMode, setDepositAmountMode] = useState<\"token\" | \"usd\">(\n \"token\"\n );\n const [depositSourceFilter, setDepositSourceFilter] =\n useState(\"all\");\n\n useEffect(() => {\n const immediateIssue = buildReceiveAmountIssue();\n applyReceiveAmountIssue(immediateIssue);\n\n if (!toToken || !getDestinationReceiveLimitUsd(toToken)) return;\n if (!parseFiatNumber(amount)?.gt(0)) return;\n if (getImmediateDestinationReceiveUsdRate(toToken)?.gt(0)) return;\n\n let cancelled = false;\n void resolveUsdRateForToken(toToken)\n .then((resolvedRate) => {\n if (cancelled) return;\n const issue = buildReceiveAmountIssue({\n destinationRate: resolvedRate.gt(0) ? resolvedRate : undefined,\n });\n applyReceiveAmountIssue(issue);\n })\n .catch(() => {\n if (!cancelled) {\n applyReceiveAmountIssue(buildReceiveAmountIssue());\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [\n activeMode,\n amount,\n depositAmountMode,\n fromTokens,\n swapType,\n toToken?.chainId,\n toToken?.chainName,\n toToken?.contractAddress,\n toToken?.priceUSD,\n toToken?.symbol,\n ]);\n\n const trackDeposit = useCallback(\n (event: string, props?: Record) => {\n const analytics = nexusSDK?.analytics;\n if (!analytics) return;\n analytics.track(event, {\n widgetSessionId: widgetSessionIdRef.current,\n widgetAttemptId: widgetAttemptIdRef.current,\n opportunityProtocol: selectedOpportunity?.protocol ?? null,\n destinationChainId: selectedOpportunity?.chainId ?? null,\n destinationToken: selectedOpportunity?.tokenSymbol ?? null,\n ...props,\n });\n },\n [nexusSDK, selectedOpportunity]\n );\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (!nexusSDK?.analytics) return;\n if (widgetOpenedFiredRef.current) return;\n widgetOpenedFiredRef.current = true;\n widgetOpenedTsRef.current = Date.now();\n rotateAttempt();\n trackDeposit(\"deposit_widget_opened\", {\n embed: Boolean(embed),\n depositConfigured: Boolean(configuredDeposit),\n prefillAmountPresent: Boolean(config.prefill?.amount),\n });\n }, [\n activeMode,\n nexusSDK,\n embed,\n configuredDeposit,\n config.prefill,\n rotateAttempt,\n trackDeposit,\n ]);\n\n useEffect(() => {\n analyticsRef.current = nexusSDK?.analytics ?? null;\n }, [nexusSDK]);\n\n useEffect(() => {\n selectedOpportunityRef.current = selectedOpportunity;\n }, [selectedOpportunity]);\n\n useEffect(() => {\n return () => {\n if (!widgetOpenedFiredRef.current) return;\n const analytics = analyticsRef.current;\n if (!analytics) return;\n const opp = selectedOpportunityRef.current;\n analytics.track(\"deposit_widget_closed\", {\n widgetSessionId: widgetSessionIdRef.current,\n widgetAttemptId: widgetAttemptIdRef.current,\n opportunityProtocol: opp?.protocol ?? null,\n lastStep: swapStepRef.current,\n reachedTerminal: reachedTerminalRef.current,\n hadSimulationSuccess: hadSimulationSuccessRef.current,\n hadPreviewViewed: hadPreviewViewedRef.current,\n timeInWidgetMs: Date.now() - widgetOpenedTsRef.current,\n });\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const toTokenFromOpportunity = (\n opp: NexusWidgetDepositOpportunityMetadata\n ): SwapTokenOption => {\n const citreaToken = findCitreaReceiveToken({\n address: opp.tokenAddress,\n chainId: opp.chainId,\n symbol: opp.tokenSymbol,\n });\n const chainTokens = supportedChainsAndTokens?.find(\n (chain) => chain.id === opp.chainId\n )?.tokens;\n const matchedToken = chainTokens?.find(\n (token) =>\n token.contractAddress.toLowerCase() ===\n opp.tokenAddress.toLowerCase() || token.symbol === opp.tokenSymbol\n );\n const tokenSymbol =\n citreaToken?.symbol ?? matchedToken?.symbol ?? opp.tokenSymbol;\n const tokenMeta =\n TOKEN_METADATA[tokenSymbol as keyof typeof TOKEN_METADATA];\n\n return {\n chainId: opp.chainId,\n contractAddress: citreaToken?.contractAddress ?? opp.tokenAddress,\n symbol: tokenSymbol,\n name: matchedToken?.name || citreaToken?.name || tokenSymbol,\n balance: \"0\",\n balanceInFiat: \"$0.00\",\n decimals:\n matchedToken?.decimals ??\n citreaToken?.decimals ??\n opp.tokenDecimals ??\n tokenMeta?.decimals ??\n 18,\n logo:\n opp.tokenLogo ||\n matchedToken?.logo ||\n citreaToken?.logo ||\n tokenMeta?.logo,\n chainName: getShortChainName(\n opp.chainId,\n CHAIN_METADATA[opp.chainId]?.name ?? citreaToken?.chainName\n ),\n chainLogo: CHAIN_METADATA[opp.chainId]?.logo ?? citreaToken?.chainLogo,\n };\n };\n\n const getDestinationBalanceFromSwapBalances = (token?: SwapTokenOption) => {\n if (!token?.chainId || !token.contractAddress) return null;\n\n const targetAddress = token.contractAddress.toLowerCase();\n const targetSymbol = token.symbol.toUpperCase();\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== token.chainId) continue;\n\n const breakdownAddress = breakdown.contractAddress?.toLowerCase();\n const addressMatches =\n (breakdownAddress && breakdownAddress === targetAddress) ||\n (isNativeTokenAddress(breakdownAddress) &&\n isNativeTokenAddress(targetAddress));\n const symbolMatches =\n (breakdown.symbol ?? asset.symbol ?? \"\").toUpperCase() ===\n targetSymbol;\n\n if (!addressMatches && !symbolMatches) continue;\n\n const balance = parseFiatNumber(breakdown.balance);\n if (!balance) return null;\n\n return `${balance.toDecimalPlaces(6).toFixed()} ${token.symbol}`;\n }\n }\n\n return null;\n };\n\n const resolvePrefillToken = useCallback(\n (pair?: RuntimeDestinationPair) => {\n if (!pair?.token || !pair.chain) return undefined;\n\n const normalizeAddress = (address?: string) => {\n if (!address) return \"\";\n return isNativeTokenAddress(address)\n ? zeroAddress\n : address.toLowerCase();\n };\n const targetAddress = normalizeAddress(pair.token);\n\n const balanceToken = deriveTokenOptions(\n swapBalance ?? [],\n swapSupportedChainsAndTokens\n ).find(\n (token) =>\n token.chainId === pair.chain &&\n normalizeAddress(token.contractAddress) === targetAddress\n );\n if (balanceToken) return balanceToken;\n\n const chain = supportedChainsAndTokens?.find(\n (item) => item.id === pair.chain\n );\n const matchedToken = chain?.tokens?.find(\n (token) => normalizeAddress(token.contractAddress) === targetAddress\n );\n const citreaToken = findCitreaReceiveToken({\n address: pair.token,\n chainId: pair.chain,\n });\n const tokenAddressSymbol = Object.entries(\n TOKEN_CONTRACT_ADDRESSES as Record>\n ).find(\n ([, addresses]) =>\n normalizeAddress(addresses[pair.chain]) === targetAddress\n )?.[0];\n const chainMeta = CHAIN_METADATA[pair.chain];\n const isNativePrefill = isNativeTokenAddress(pair.token);\n const tokenSymbol =\n matchedToken?.symbol ??\n citreaToken?.symbol ??\n pair.symbol ??\n tokenAddressSymbol ??\n (isNativePrefill ? chainMeta?.nativeCurrency?.symbol : undefined) ??\n \"Token\";\n const tokenMeta =\n TOKEN_METADATA[tokenSymbol as keyof typeof TOKEN_METADATA];\n\n if (\n !chain &&\n !matchedToken &&\n !citreaToken &&\n !tokenAddressSymbol &&\n !pair.symbol &&\n !isNativePrefill\n ) {\n return undefined;\n }\n\n return {\n chainId: pair.chain,\n contractAddress: citreaToken?.contractAddress ?? pair.token,\n symbol: tokenSymbol,\n name: matchedToken?.name || citreaToken?.name || tokenSymbol,\n balance: `0 ${tokenSymbol}`,\n balanceInFiat: \"$0.00\",\n decimals:\n matchedToken?.decimals ??\n citreaToken?.decimals ??\n pair.decimals ??\n tokenMeta?.decimals ??\n (isNativePrefill ? chainMeta?.nativeCurrency?.decimals : undefined) ??\n 18,\n logo:\n matchedToken?.logo ||\n citreaToken?.logo ||\n pair.logo ||\n tokenMeta?.logo,\n chainName: getShortChainName(\n pair.chain,\n chain?.name ?? chainMeta?.name ?? citreaToken?.chainName\n ),\n chainLogo: chainMeta?.logo ?? chain?.logo ?? citreaToken?.chainLogo,\n } satisfies SwapTokenOption;\n },\n [supportedChainsAndTokens, swapBalance]\n );\n\n useEffect(() => {\n if (activeMode !== \"swap\") return;\n\n const sourcePrefill = config.prefill?.source;\n const destinationPrefill = config.prefill?.destination;\n if (!sourcePrefill && !destinationPrefill) return;\n\n const prefillKey = [\n sourcePrefill\n ? `source:${sourcePrefill.chain}:${sourcePrefill.token.toLowerCase()}`\n : \"\",\n destinationPrefill\n ? `destination:${destinationPrefill.chain}:${destinationPrefill.token.toLowerCase()}`\n : \"\",\n config.prefill?.amount ? `amount:${config.prefill.amount}` : \"\",\n ].join(\"|\");\n\n if (appliedTokenPrefillRef.current === prefillKey) return;\n\n const sourceToken = resolvePrefillToken(sourcePrefill);\n const destinationToken = resolvePrefillToken(destinationPrefill);\n\n if (sourcePrefill && !sourceToken) return;\n if (destinationPrefill && !destinationToken) return;\n\n if (sourceToken) {\n setFromTokens((current) => {\n const nextSourceToken = {\n ...sourceToken,\n userAmount: config.prefill?.amount ?? \"\",\n };\n const currentSourceToken = current[0];\n if (\n current.length === 1 &&\n isSameTokenSelection(currentSourceToken, nextSourceToken) &&\n currentSourceToken.userAmount === nextSourceToken.userAmount\n ) {\n return current;\n }\n return [nextSourceToken];\n });\n setSourceSelectionTouched(true);\n }\n if (destinationToken) {\n setToToken((current) =>\n isSameTokenSelection(current, destinationToken)\n ? current\n : destinationToken\n );\n }\n setSwapType(\"exactIn\");\n appliedTokenPrefillRef.current = prefillKey;\n }, [\n activeMode,\n config.prefill?.amount,\n config.prefill?.destination?.chain,\n config.prefill?.destination?.decimals,\n config.prefill?.destination?.logo,\n config.prefill?.destination?.symbol,\n config.prefill?.destination?.token,\n config.prefill?.source?.chain,\n config.prefill?.source?.decimals,\n config.prefill?.source?.logo,\n config.prefill?.source?.symbol,\n config.prefill?.source?.token,\n resolvePrefillToken,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"send\") return;\n\n const sendPrefill =\n config.prefill?.token && config.prefill?.chain\n ? {\n token: config.prefill.token,\n chain: config.prefill.chain,\n }\n : config.prefill?.destination;\n if (!sendPrefill) return;\n\n const prefillKey = `send:${sendPrefill.chain}:${sendPrefill.token.toLowerCase()}`;\n if (appliedTokenPrefillRef.current === prefillKey) return;\n\n const token = resolvePrefillToken(sendPrefill);\n if (!token) return;\n\n setToToken(token);\n setSwapType(\"exactOut\");\n appliedTokenPrefillRef.current = prefillKey;\n }, [\n activeMode,\n config.prefill?.chain,\n config.prefill?.destination?.chain,\n config.prefill?.destination?.decimals,\n config.prefill?.destination?.logo,\n config.prefill?.destination?.symbol,\n config.prefill?.destination?.token,\n config.prefill?.token,\n resolvePrefillToken,\n ]);\n\n useEffect(() => {\n if (config.prefill?.amount) setAmount(config.prefill.amount);\n if (config.prefill?.recipient)\n setRecipientAddress(config.prefill.recipient);\n }, [config.prefill?.amount, config.prefill?.recipient]);\n\n useEffect(() => {\n setDestinationBalance(null);\n\n const balanceToken =\n toToken ??\n (activeMode === \"deposit\" && selectedOpportunity\n ? toTokenFromOpportunity(selectedOpportunity)\n : undefined);\n\n if (!balanceToken?.chainId || !ownerAddress) return;\n\n const swapBalanceValue =\n getDestinationBalanceFromSwapBalances(balanceToken);\n if (swapBalanceValue) {\n setDestinationBalance(swapBalanceValue);\n }\n }, [\n activeMode,\n ownerAddress,\n selectedOpportunity?.chainId,\n selectedOpportunity?.tokenAddress,\n selectedOpportunity?.tokenLogo,\n selectedOpportunity?.tokenSymbol,\n swapBalance,\n toToken?.chainId,\n toToken?.chainName,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || !configuredDeposit) return;\n setSelectedOpportunity((current) =>\n isSameDepositConfig(current, configuredDeposit)\n ? current\n : configuredDeposit\n );\n setSwapType(\"exactOut\");\n setToToken((current) => {\n const next = {\n ...toTokenFromOpportunity(configuredDeposit),\n balance: current?.balance ?? \"0\",\n balanceInFiat: current?.balanceInFiat ?? \"$0.00\",\n };\n if (\n current &&\n current.chainId === next.chainId &&\n current.contractAddress.toLowerCase() ===\n next.contractAddress.toLowerCase() &&\n current.symbol === next.symbol &&\n current.decimals === next.decimals &&\n current.logo === next.logo &&\n current.chainLogo === next.chainLogo &&\n current.chainName === next.chainName &&\n current.balance === next.balance &&\n current.balanceInFiat === next.balanceInFiat\n ) {\n return current;\n }\n return next;\n });\n }, [\n activeMode,\n configuredDepositIdentity,\n configuredDeposit?.chainId,\n configuredDeposit?.tokenAddress,\n configuredDeposit?.tokenLogo,\n configuredDeposit?.tokenSymbol,\n supportedChainsAndTokens,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || !selectedOpportunity) return;\n setToToken((current) => {\n const next = {\n ...toTokenFromOpportunity(selectedOpportunity),\n balance: current?.balance ?? \"0\",\n balanceInFiat: current?.balanceInFiat ?? \"$0.00\",\n };\n if (\n current &&\n current.chainId === next.chainId &&\n current.contractAddress.toLowerCase() ===\n next.contractAddress.toLowerCase() &&\n current.symbol === next.symbol &&\n current.decimals === next.decimals &&\n current.logo === next.logo &&\n current.chainLogo === next.chainLogo &&\n current.chainName === next.chainName &&\n current.balance === next.balance &&\n current.balanceInFiat === next.balanceInFiat\n ) {\n return current;\n }\n return next;\n });\n }, [activeMode, selectedOpportunity, supportedChainsAndTokens]);\n\n useEffect(() => {\n if (activeMode !== \"send\") return;\n setSwapType(\"exactOut\");\n }, [activeMode]);\n\n useEffect(() => {\n if (activeMode === \"swap\" && swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n }, [activeMode, swapType]);\n\n useEffect(() => {\n if (!toToken?.symbol) return;\n if (getFiatValue(1, toToken.symbol) > 0) return;\n\n let cancelled = false;\n void resolveTokenUsdRate(toToken.symbol).catch((error) => {\n if (!cancelled) {\n console.warn(\"Unable to resolve Nexus Widget token USD rate\", {\n symbol: toToken.symbol,\n error,\n });\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [activeMode, getFiatValue, resolveTokenUsdRate, toToken?.symbol]);\n\n // Balance helpers\n const activeBalanceArray = swapBalance;\n const selectedToken = config.prefill?.token ?? \"USDC\";\n const currentAsset =\n activeBalanceArray?.find((a) => a.symbol === selectedToken) ||\n activeBalanceArray?.[0];\n const maxBalance = currentAsset?.balance\n ? String(currentAsset.balance)\n : undefined;\n const usdValue = getFiatValue(\n Number(amount) || 0,\n currentAsset?.symbol || \"USDC\"\n );\n const getDepositTokenUsdRate = () => {\n if (selectedOpportunity?.tokenSymbol) {\n const fiat = getFiatValue(1, selectedOpportunity.tokenSymbol);\n if (Number.isFinite(fiat) && fiat > 0) {\n return new Decimal(fiat);\n }\n }\n\n if (toToken) {\n const tokenRate = getTokenUsdRate(toToken);\n if (tokenRate.gt(0)) return tokenRate;\n }\n\n return getCachedDestinationUsdRate(toToken) ?? new Decimal(0);\n };\n const getDepositTokenAmountForQuote = () => {\n const parsedAmount = parseFiatNumber(amount) ?? new Decimal(0);\n if (parsedAmount.lte(0)) return undefined;\n if (depositAmountMode === \"token\") return parsedAmount;\n\n const rate = getDepositTokenUsdRate();\n if (rate.lte(0)) return undefined;\n return parsedAmount.div(rate);\n };\n const depositTokenAmountForQuote = getDepositTokenAmountForQuote();\n const depositQuoteAmountKey = depositTokenAmountForQuote?.toFixed() ?? \"\";\n const depositUsdDecimal =\n depositAmountMode === \"usd\"\n ? (parseFiatNumber(amount) ?? new Decimal(0))\n : depositTokenAmountForQuote\n ? depositTokenAmountForQuote.mul(getDepositTokenUsdRate())\n : new Decimal(0);\n const depositUsdDisplay = depositUsdDecimal.toDecimalPlaces(2).toFixed();\n const depositTokenDisplay =\n depositTokenAmountForQuote\n ?.toDecimalPlaces(\n getCappedTokenDisplayDecimals(toToken?.decimals),\n Decimal.ROUND_DOWN\n )\n .toFixed() ?? \"0\";\n const isExactOutMode = activeMode === \"deposit\" || activeMode === \"send\";\n const getActiveTotalBalanceUsd = () =>\n isExactOutMode\n ? getExactOutTotalSourceBalanceUsd()\n : getSwapBalanceTotalUsd();\n const getTotalBalancePercentUsdAmount = (pct: number) => {\n const ratio =\n isExactOutMode && pct === 100\n ? receiveMaxSafetyMultiplier\n : new Decimal(pct).div(100);\n return getActiveTotalBalanceUsd().mul(ratio);\n };\n const getExactOutDestinationUsdRate = (token: SwapTokenOption) => {\n if (activeMode === \"deposit\") {\n return getDepositTokenUsdRate();\n }\n return getTokenUsdRate(token);\n };\n const getExactOutPercentAmountFromBalance = (\n token: SwapTokenOption,\n pct: number,\n preferUsd: boolean\n ) => {\n const usdAmount = getTotalBalancePercentUsdAmount(pct);\n if (usdAmount.lte(0)) return undefined;\n\n if (preferUsd) {\n return {\n amount: usdAmount.toDecimalPlaces(2, Decimal.ROUND_DOWN).toFixed(),\n mode: \"usd\" as const,\n };\n }\n\n const rate = getExactOutDestinationUsdRate(token);\n if (rate.lte(0)) return undefined;\n\n return {\n amount: usdAmount\n .div(rate)\n .toDecimalPlaces(\n getCappedTokenDisplayDecimals(token.decimals),\n Decimal.ROUND_DOWN\n )\n .toFixed(),\n mode: \"token\" as const,\n };\n };\n const depositSourceTargetUsdKey =\n activeMode === \"deposit\"\n ? (getDepositSourceTargetUsd()?.toFixed() ?? \"\")\n : \"\";\n const normalizedQuoteAmountKey = parseFiatNumber(amount)?.toFixed() ?? \"\";\n const quoteRecipientKey =\n activeMode === \"swap\"\n ? effectiveRecipientAddress\n : activeMode === \"send\"\n ? recipientAddress\n : \"\";\n const activeQuoteInputKey = [\n activeMode,\n swapType,\n normalizedQuoteAmountKey,\n toTokenQuoteKey,\n quoteRecipientKey.toLowerCase(),\n activeMode === \"swap\" ? fromTokensQuoteKey : \"\",\n activeMode === \"deposit\"\n ? [\n depositAmountMode,\n depositQuoteAmountKey,\n selectedOpportunityIdentity,\n depositSourceTargetUsdKey,\n depositSourceFilter,\n sourceSelectionTouched ? \"manual\" : \"auto\",\n sourceSelectionRevision,\n exactOutQuoteSourceMode,\n ].join(\":\")\n : \"\",\n activeMode === \"send\"\n ? [\n sourceSelectionTouched ? \"manual\" : \"auto\",\n sourceSelectionRevision,\n exactOutQuoteSourceMode,\n ].join(\":\")\n : \"\",\n ].join(\"|\");\n\n const exactOutAutoSourceInputKey = [\n activeMode,\n normalizedQuoteAmountKey,\n toTokenQuoteKey,\n quoteRecipientKey.toLowerCase(),\n activeMode === \"deposit\"\n ? [\n depositAmountMode,\n depositQuoteAmountKey,\n selectedOpportunityIdentity,\n depositSourceTargetUsdKey,\n ].join(\":\")\n : \"\",\n ].join(\"|\");\n\n activeQuoteInputKeyRef.current = activeQuoteInputKey;\n\n useEffect(() => {\n lastAutoIntentSourceTokensRef.current = [];\n }, [exactOutAutoSourceInputKey]);\n\n useEffect(() => {\n setTxError(null);\n }, [activeQuoteInputKey]);\n const hasCurrentQuoteIntent = Boolean(\n intentData &&\n swapIntentRef.current &&\n swapIntentRef.current.runId === swapRunIdRef.current &&\n swapIntentRef.current.quoteInputKey === activeQuoteInputKey\n );\n\n useEffect(() => {\n if (swapStep !== \"idle\") return;\n if (!hasCurrentQuoteIntent) return;\n if (sourceSelectionTouched) return;\n if (\n activeMode !== \"send\" &&\n !(activeMode === \"deposit\" && swapType === \"exactOut\")\n ) {\n return;\n }\n\n const nextTokens = lastIntentSourceTokensRef.current;\n if (nextTokens.length === 0) return;\n\n setFromTokens((current) => {\n if (\n getSourceTokenListSyncKey(current) ===\n getSourceTokenListSyncKey(nextTokens)\n ) {\n return current;\n }\n\n syncingIntentSourcesRef.current = true;\n return nextTokens;\n });\n }, [\n activeMode,\n hasCurrentQuoteIntent,\n sourceSelectionTouched,\n swapStep,\n swapType,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (!nexusSDK?.analytics) return;\n const parsed = parseFiatNumber(amount);\n if (!parsed || parsed.lte(0)) return;\n if (amount === amountEnteredLastValueRef.current) return;\n const timeout = setTimeout(() => {\n amountEnteredLastValueRef.current = amount;\n trackDeposit(\"deposit_amount_entered\", {\n amountToken: depositTokenDisplay,\n amountUsd: Number(depositUsdDisplay) || 0,\n inputMethod: lastInputMethodRef.current,\n });\n lastInputMethodRef.current = \"typed\";\n }, 500);\n return () => clearTimeout(timeout);\n }, [\n amount,\n activeMode,\n nexusSDK,\n depositTokenDisplay,\n depositUsdDisplay,\n trackDeposit,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (intentData) hadSimulationSuccessRef.current = true;\n }, [intentData, activeMode]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (sourceSelectionTouched) return;\n previousAutoSourceCountRef.current = (intentData?.sources ?? []).length;\n }, [intentData, activeMode, sourceSelectionTouched]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n const prev = prevSourceTouchedRef.current;\n const curr = sourceSelectionTouched;\n if (prev === curr) return;\n prevSourceTouchedRef.current = curr;\n if (!prev && curr) {\n trackDeposit(\"deposit_source_selection_changed\", {\n sourceCount: fromTokens.length,\n sourceChainIds: fromTokens.map((t) => t.chainId).filter(Boolean),\n sourceTokenSymbols: fromTokens.map((t) => t.symbol).filter(Boolean),\n previousSourceCount: previousAutoSourceCountRef.current,\n });\n } else if (prev && !curr) {\n trackDeposit(\"deposit_source_selection_reverted_to_auto\", {\n previousSourceCount: fromTokens.length,\n });\n }\n }, [sourceSelectionTouched, activeMode, fromTokens, trackDeposit]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (swapStep !== \"preview-intent\") return;\n if (intentLoading) return;\n if (!intentData) return;\n if (hadPreviewViewedRef.current) return;\n hadPreviewViewedRef.current = true;\n previewViewedTsRef.current = Date.now();\n trackDeposit(\"deposit_preview_viewed\", {\n totalFeeUsd: Number(intentFeeUsd) || 0,\n toAmountUsd: Number(depositUsdDisplay) || 0,\n sourceCount: (intentData?.sources ?? []).length,\n });\n }, [\n swapStep,\n intentLoading,\n intentData,\n activeMode,\n intentFeeUsd,\n depositUsdDisplay,\n trackDeposit,\n ]);\n const requiredDestinationTokenAmount =\n activeMode === \"deposit\"\n ? depositTokenAmountForQuote\n : activeMode === \"send\"\n ? parseFiatNumber(amount)\n : undefined;\n const canRefreshExactOutQuote = () =>\n activeMode === \"deposit\"\n ? Boolean(\n hasPositiveDecimalInput(amount) &&\n toToken &&\n selectedOpportunity &&\n depositTokenAmountForQuote &&\n depositTokenAmountForQuote.gt(0)\n )\n : activeMode === \"send\"\n ? Boolean(hasPositiveDecimalInput(amount) && toToken)\n : false;\n const invalidateExactOutQuoteForRefresh = (options?: {\n sourceTokens?: SwapTokenOption[];\n }) => {\n immediateQuoteAfterSourceEditRef.current = true;\n const receiveIssue = buildReceiveAmountIssue({\n sourceTokens: options?.sourceTokens,\n });\n applyReceiveAmountIssue(receiveIssue);\n const configuredIssue = buildConfiguredAmountIssue();\n if (configuredIssue) {\n clearPreviewForBlockingAmountIssue();\n }\n const sourceBalanceIssue = buildExactOutSourceBalanceIssue(\n options?.sourceTokens\n );\n if (sourceBalanceIssue) {\n clearPreviewForBlockingAmountIssue();\n setSwapQuoteIssue(sourceBalanceIssue);\n }\n const shouldLoadQuote = Boolean(\n !receiveIssue &&\n !configuredIssue &&\n !sourceBalanceIssue &&\n nexusSDK &&\n canRefreshExactOutQuote()\n );\n if (!receiveIssue && !configuredIssue && !sourceBalanceIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n setTxError(null);\n setSwapQuoteIssue(null);\n }\n return shouldLoadQuote;\n };\n\n useEffect(() => {\n if (\n activeMode !== \"swap\" ||\n swapStep !== \"idle\" ||\n swapType !== \"exactIn\"\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n\n const sources = getPredictiveExactInSourceTokens();\n const key = getPredictiveQuoteCacheKey();\n if (!toToken || sources.length === 0 || !key) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n\n const runId = ++predictiveQuoteRunRef.current;\n let cancelled = false;\n\n void (async () => {\n const baseline = predictiveQuoteCacheRef.current[key];\n const cachedDestinationRate = parseFiatNumber(\n baseline?.destinationUsdRate\n );\n const destinationRate =\n cachedDestinationRate && cachedDestinationRate.gt(0)\n ? cachedDestinationRate\n : await resolveUsdRateForToken(toToken);\n\n if (cancelled || runId !== predictiveQuoteRunRef.current) return;\n if (destinationRate.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n\n let sourceUsd = new Decimal(0);\n for (const source of sources) {\n const sourceAmount =\n parseFiatNumber(source.userAmount) ?? new Decimal(0);\n if (sourceAmount.lte(0)) continue;\n\n if (source.userAmountMode === \"usd\") {\n sourceUsd = sourceUsd.plus(sourceAmount);\n continue;\n }\n\n const sourceRate = await resolveUsdRateForToken(source);\n if (cancelled || runId !== predictiveQuoteRunRef.current) return;\n if (sourceRate.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n sourceUsd = sourceUsd.plus(sourceAmount.mul(sourceRate));\n }\n\n if (sourceUsd.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n\n const cachedAmountPerSourceUsd = parseFiatNumber(\n baseline?.exactInDestinationAmountPerSourceUsd\n );\n const predictedDestinationAmount =\n cachedAmountPerSourceUsd && cachedAmountPerSourceUsd.gt(0)\n ? sourceUsd.mul(cachedAmountPerSourceUsd)\n : sourceUsd\n .mul(BASIS_POINTS - PREDICTIVE_EXACT_IN_DISCOUNT_BPS)\n .div(BASIS_POINTS)\n .div(destinationRate);\n const predictedDestinationUsd =\n cachedAmountPerSourceUsd && cachedAmountPerSourceUsd.gt(0)\n ? predictedDestinationAmount.mul(destinationRate)\n : sourceUsd\n .mul(BASIS_POINTS - PREDICTIVE_EXACT_IN_DISCOUNT_BPS)\n .div(BASIS_POINTS);\n\n if (\n cancelled ||\n runId !== predictiveQuoteRunRef.current ||\n predictedDestinationAmount.lte(0)\n ) {\n return;\n }\n\n setPredictiveQuote({\n key,\n mode: \"exactIn\",\n toAmount: getPredictiveDisplayAmount(\n predictedDestinationAmount,\n toToken\n ),\n toUsd: predictedDestinationUsd.toDecimalPlaces(6).toFixed(),\n });\n })();\n\n return () => {\n cancelled = true;\n };\n }, [\n activeMode,\n amount,\n fromTokens,\n swapStep,\n swapType,\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n useEffect(() => {\n if (\n (activeMode !== \"deposit\" && activeMode !== \"send\") ||\n swapStep !== \"idle\" ||\n swapType !== \"exactOut\" ||\n !nexusSDK\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current\n );\n return;\n }\n\n const parsedAmount = parseFiatNumber(amount);\n const key = getPredictiveQuoteCacheKey();\n if (\n !toToken ||\n !parsedAmount ||\n parsedAmount.lte(0) ||\n !key ||\n (activeMode === \"deposit\" && !selectedOpportunity)\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current\n );\n return;\n }\n\n const runId = ++predictiveQuoteRunRef.current;\n let cancelled = false;\n\n void (async () => {\n const baseline = predictiveQuoteCacheRef.current[key];\n const cachedDestinationRate = parseFiatNumber(\n baseline?.destinationUsdRate\n );\n const destinationRate =\n cachedDestinationRate && cachedDestinationRate.gt(0)\n ? cachedDestinationRate\n : await resolveUsdRateForToken(toToken);\n\n if (cancelled || runId !== predictiveQuoteRunRef.current) return;\n if (destinationRate.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current\n );\n return;\n }\n\n const destinationAmount =\n activeMode === \"deposit\" && depositAmountMode === \"usd\"\n ? parsedAmount.div(destinationRate)\n : parsedAmount;\n const destinationUsd =\n activeMode === \"deposit\" && depositAmountMode === \"usd\"\n ? parsedAmount\n : destinationAmount.mul(destinationRate);\n const destinationCoverage = getExactOutDestinationBalanceCoverage({\n requestedAmount: destinationAmount,\n requestedUsd: destinationUsd,\n token: toToken,\n });\n const destinationUsdNeedingSources = Decimal.max(\n destinationUsd.minus(destinationCoverage?.usd ?? new Decimal(0)),\n new Decimal(0)\n );\n const cachedSourceUsdRatio = parseFiatNumber(\n baseline?.exactOutSourceUsdPerDestinationUsd\n );\n const requiredSourceUsd = getPredictiveExactOutSourceTargetUsd(\n destinationUsdNeedingSources,\n cachedSourceUsdRatio\n );\n const sources = requiredSourceUsd.gt(0)\n ? await buildPredictiveExactOutSources(requiredSourceUsd)\n : [];\n\n if (\n cancelled ||\n runId !== predictiveQuoteRunRef.current ||\n (requiredSourceUsd.gt(0) && sources.length === 0)\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current\n );\n return;\n }\n\n setPredictiveQuote({\n key,\n mode: \"exactOut\",\n sources,\n toAmount: getPredictiveDisplayAmount(destinationAmount, toToken),\n toUsd: destinationUsd.toDecimalPlaces(6).toFixed(),\n });\n })();\n\n return () => {\n cancelled = true;\n };\n }, [\n activeMode,\n amount,\n depositAmountMode,\n destinationBalance,\n fromTokensQuoteKey,\n nexusSDK,\n selectedOpportunityIdentity,\n sourceSelectionRevision,\n swapBalance,\n swapStep,\n swapType,\n toToken?.balance,\n toToken?.balanceInFiat,\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n const resolvedDepositSourceTokens = useMemo(() => {\n if (activeMode !== \"deposit\" || !swapBalance) return [];\n const selection = getResolvedDepositSourceSelection();\n return getDepositSourceTokensForIds(selection.selectedSourceIds);\n }, [\n activeMode,\n depositSourceFilter,\n depositQuoteAmountKey,\n depositSourceTargetUsdKey,\n depositUsdDecimal.toFixed(),\n fromTokensQuoteKey,\n selectedOpportunity?.chainId,\n selectedOpportunity?.tokenAddress,\n selectedOpportunity?.tokenSymbol,\n sourceSelectionRevision,\n sourceSelectionTouched,\n swapBalance,\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.symbol,\n ]);\n const lockedDestinationSourceTokens = useMemo(() => {\n if (\n (activeMode !== \"deposit\" && activeMode !== \"send\") ||\n !toToken?.chainId ||\n !requiredDestinationTokenAmount ||\n requiredDestinationTokenAmount.lte(0)\n ) {\n return [];\n }\n\n const heldDestinationToken = getHeldDestinationTokenOption();\n return heldDestinationToken ? [heldDestinationToken] : [];\n }, [\n activeMode,\n requiredDestinationTokenAmount?.toFixed(),\n swapBalance,\n toToken?.chainId,\n toToken?.chainLogo,\n toToken?.chainName,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.logo,\n toToken?.symbol,\n ]);\n\n const setSourcePickerDraftSelection = useCallback(\n (tokens: SwapTokenOption[]) => {\n const nextTokens = tokens.map((token) => ({\n ...token,\n userAmount: \"\",\n }));\n sourcePickerDraftTokensRef.current = nextTokens;\n setSourcePickerDraftTokens(nextTokens);\n },\n []\n );\n\n const getAutoExactOutSourceTokensForPicker = useCallback(() => {\n const autoIntentTokens = lastAutoIntentSourceTokensRef.current;\n if (autoIntentTokens.length > 0) {\n return autoIntentTokens;\n }\n\n return sourceSelectionTouched ? [] : fromTokens;\n }, [fromTokens, sourceSelectionTouched]);\n\n const resetSourcePickerDraft = useCallback(() => {\n sourcePickerDraftTokensRef.current = null;\n setSourcePickerDraftTokens(null);\n }, []);\n\n const beginSourcePickerEdit = useCallback(() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n resetSourcePickerDraft();\n return;\n }\n\n sourcePickerDraftDepositFilterRef.current = depositSourceFilter;\n sourcePickerDraftTouchedRef.current = sourceSelectionTouched;\n sourcePickerDraftModeRef.current = exactOutQuoteSourceModeRef.current;\n setSourcePickerDraftSelection(fromTokens);\n }, [\n activeMode,\n depositSourceFilter,\n fromTokens,\n resetSourcePickerDraft,\n setSourcePickerDraftSelection,\n sourceSelectionTouched,\n ]);\n\n const handleSourcePickerCancel = useCallback(() => {\n resetSourcePickerDraft();\n closeDrawerToIdle();\n }, [closeDrawerToIdle, resetSourcePickerDraft]);\n\n const handleSourcePickerDraftSelectionChange = useCallback(\n (tokens: SwapTokenOption[]) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n\n setSourcePickerDraftSelection(tokens);\n sourcePickerDraftTouchedRef.current = true;\n sourcePickerDraftModeRef.current = \"selected\";\n if (activeMode === \"deposit\") {\n sourcePickerDraftDepositFilterRef.current = \"custom\";\n }\n },\n [activeMode, setSourcePickerDraftSelection]\n );\n\n const handleSourcePickerFilterTabSelect = useCallback(\n (tab: Exclude) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n\n const nextFilter: DepositSourceFilter =\n tab === \"stables\" ? \"stablecoins\" : tab;\n\n if (tab === \"all\") {\n sourcePickerDraftDepositFilterRef.current = nextFilter;\n sourcePickerDraftTouchedRef.current = false;\n sourcePickerDraftModeRef.current = \"all\";\n setSourcePickerDraftSelection(getAutoExactOutSourceTokensForPicker());\n return;\n }\n\n if (activeMode !== \"deposit\") return;\n\n const selection = getResolvedDepositSourceSelection({\n filter: nextFilter,\n isManualSelection: false,\n });\n const sourcePoolTokens = getDepositSourceTokensForIds(\n selection.sourcePoolIds\n );\n\n sourcePickerDraftDepositFilterRef.current = nextFilter;\n sourcePickerDraftTouchedRef.current = false;\n sourcePickerDraftModeRef.current = \"all\";\n setSourcePickerDraftSelection(sourcePoolTokens);\n },\n [\n activeMode,\n getAutoExactOutSourceTokensForPicker,\n getDepositSourceTokensForIds,\n getResolvedDepositSourceSelection,\n setSourcePickerDraftSelection,\n ]\n );\n\n const commitSourcePickerDraft = useCallback(\n (tokens?: SwapTokenOption[]) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n resetSourcePickerDraft();\n closeDrawerToIdle();\n return;\n }\n\n const nextTokens =\n tokens ?? sourcePickerDraftTokensRef.current ?? fromTokens;\n const normalizedTokens = nextTokens.map((token) => ({\n ...token,\n userAmount: \"\",\n }));\n\n setSourceSelectionTouched(sourcePickerDraftTouchedRef.current);\n setExactOutQuoteSourceModeValue(sourcePickerDraftModeRef.current);\n if (activeMode === \"deposit\") {\n setDepositSourceFilter(sourcePickerDraftDepositFilterRef.current);\n }\n invalidateExactOutQuoteForRefresh({\n sourceTokens: normalizedTokens,\n });\n setSourceSelectionRevision((current) => current + 1);\n setFromTokens(normalizedTokens);\n resetSourcePickerDraft();\n closeDrawerToIdle();\n },\n [\n activeMode,\n closeDrawerToIdle,\n fromTokens,\n invalidateExactOutQuoteForRefresh,\n resetSourcePickerDraft,\n setExactOutQuoteSourceModeValue,\n ]\n );\n\n const sourcePickerSelectedTokens =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n swapStep === \"choose-swap-asset\"\n ? (sourcePickerDraftTokens ?? fromTokens)\n : fromTokens;\n\n useEffect(() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n if (lockedDestinationSourceTokens.length === 0) return;\n if (activeMode === \"deposit\" && !sourceSelectionTouched) return;\n\n setFromTokens((current) => {\n const missing = lockedDestinationSourceTokens.filter(\n (locked) =>\n !current.some(\n (token) =>\n getTokenSelectionKey(token) === getTokenSelectionKey(locked)\n )\n );\n if (missing.length === 0) return current;\n return [\n ...current,\n ...missing.map((token) => ({ ...token, userAmount: \"\" })),\n ];\n });\n }, [activeMode, lockedDestinationSourceTokens, sourceSelectionTouched]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (sourceSelectionTouched) return;\n if (\n !toToken ||\n !depositTokenAmountForQuote ||\n depositTokenAmountForQuote.lte(0)\n ) {\n return;\n }\n if (resolvedDepositSourceTokens.length === 0) {\n return;\n }\n\n setFromTokens((current) => {\n const canInitialize = current.length === 0;\n if (!canInitialize) return current;\n\n const next: SwapTokenOption[] = [];\n const seen = new Set();\n for (const token of resolvedDepositSourceTokens) {\n const key = getTokenSelectionKey(token);\n if (!key || seen.has(key)) continue;\n seen.add(key);\n next.push({ ...token, userAmount: \"\" });\n }\n\n const currentKeys = current.map(getTokenSelectionKey).sort().join(\"|\");\n const nextKeys = next.map(getTokenSelectionKey).sort().join(\"|\");\n if (currentKeys === nextKeys) return current;\n return next;\n });\n }, [\n activeMode,\n depositQuoteAmountKey,\n resolvedDepositSourceTokens,\n sourceSelectionTouched,\n toTokenQuoteKey,\n ]);\n\n // ---------------------------------------------------------------------------\n // Handlers\n // ---------------------------------------------------------------------------\n\n const applyConfiguredPrefillsAfterReset = () => {\n const prefillAmount = config.prefill?.amount ?? \"\";\n const prefillRecipient = config.prefill?.recipient ?? \"\";\n\n appliedTokenPrefillRef.current = null;\n syncingIntentSourcesRef.current = false;\n maxPercentRunRef.current += 1;\n setAmount(prefillAmount);\n setRecipientAddress(prefillRecipient);\n setTxError(null);\n setSwapQuoteIssue(null);\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n setPredictiveQuote(null);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setMaxCalculationPercent(null);\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setExactOutQuoteSourceModeValue(\"all\");\n setSourceSelectionRevision((current) => current + 1);\n setDepositAmountMode(\"token\");\n\n if (activeMode === \"swap\") {\n const sourcePrefill = config.prefill?.source;\n const destinationPrefill = config.prefill?.destination;\n const sourceToken = resolvePrefillToken(sourcePrefill);\n const destinationToken = resolvePrefillToken(destinationPrefill);\n\n setFromTokens(\n sourceToken\n ? [\n {\n ...sourceToken,\n userAmount: prefillAmount,\n },\n ]\n : []\n );\n setSourceSelectionTouched(Boolean(sourceToken));\n setToToken(destinationToken);\n setSelectedOpportunity(undefined);\n setSwapType(\"exactIn\");\n\n if (\n (!sourcePrefill || sourceToken) &&\n (!destinationPrefill || destinationToken) &&\n (sourcePrefill || destinationPrefill)\n ) {\n appliedTokenPrefillRef.current = [\n sourcePrefill\n ? `source:${sourcePrefill.chain}:${sourcePrefill.token.toLowerCase()}`\n : \"\",\n destinationPrefill\n ? `destination:${destinationPrefill.chain}:${destinationPrefill.token.toLowerCase()}`\n : \"\",\n config.prefill?.amount ? `amount:${config.prefill.amount}` : \"\",\n ].join(\"|\");\n }\n return;\n }\n\n if (activeMode === \"send\") {\n const sendPrefill =\n config.prefill?.token && config.prefill?.chain\n ? {\n token: config.prefill.token,\n chain: config.prefill.chain,\n }\n : config.prefill?.destination;\n const token = resolvePrefillToken(sendPrefill);\n\n setFromTokens([]);\n setSelectedOpportunity(undefined);\n setToToken(token);\n setSwapType(\"exactOut\");\n\n if (sendPrefill && token) {\n appliedTokenPrefillRef.current = `send:${sendPrefill.chain}:${sendPrefill.token.toLowerCase()}`;\n }\n return;\n }\n\n const destinationPrefill = config.prefill?.destination;\n const configuredPrefillDeposit = destinationPrefill\n ? configuredDepositOptions.find(\n (deposit) =>\n deposit.chainId === destinationPrefill.chain &&\n deposit.tokenAddress.toLowerCase() ===\n destinationPrefill.token.toLowerCase()\n )\n : undefined;\n const nextDeposit = configuredPrefillDeposit ?? configuredDeposit;\n\n setFromTokens([]);\n setSelectedOpportunity(nextDeposit);\n setToToken(nextDeposit ? toTokenFromOpportunity(nextDeposit) : undefined);\n setSwapType(\"exactOut\");\n };\n\n const handleReset = () => {\n clearPendingSwapIntent();\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n amountEnteredLastValueRef.current = config.prefill?.amount ?? \"\";\n applyConfiguredPrefillsAfterReset();\n rotateAttempt();\n };\n\n const resetSourceSelectionAfterFailure = () => {\n lastAutoIntentSourceTokensRef.current = [];\n lastIntentSourceTokensRef.current = [];\n resetSourcePickerDraft();\n resetExactOutSourcesToAuto();\n };\n\n const handleFailureBack = () => {\n clearPendingSwapIntent();\n resetSourceSelectionAfterFailure();\n setTxError(null);\n void refreshSelectedSourceBalances();\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n rotateAttempt();\n };\n\n const clearTerminalInputState = () => {\n setAmount(\"\");\n setRecipientAddress(\"\");\n setTxError(null);\n setSwapQuoteIssue(null);\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n setFromTokens((current) => (current.length === 0 ? current : []));\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setDepositAmountMode(\"token\");\n };\n\n const resetInputsAfterSuccessfulExecution = () => {\n clearTerminalInputState();\n if (activeMode === \"deposit\") {\n setSelectedOpportunity(configuredDeposit);\n setToToken(\n configuredDeposit\n ? toTokenFromOpportunity(configuredDeposit)\n : undefined\n );\n } else {\n setToToken(undefined);\n }\n };\n\n const handleTerminalReceiptDone = () => {\n clearPendingSwapIntent();\n clearTerminalInputState();\n void refreshSelectedSourceBalances();\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n amountEnteredLastValueRef.current = \"\";\n rotateAttempt();\n };\n\n const handleModalOpenChange = useCallback(\n (open: boolean) => {\n if (!open && swapStepRef.current === \"progress\") return;\n if (!isControlledOpen) {\n setInternalOpen(open);\n }\n onOpenChange?.(open);\n if (!open) {\n clearPendingSwapIntent();\n onClose?.();\n }\n },\n [clearPendingSwapIntent, isControlledOpen, onClose, onOpenChange]\n );\n\n const handleClose = () => {\n if (!embed) {\n handleModalOpenChange(false);\n return;\n }\n clearPendingSwapIntent();\n onClose?.();\n };\n\n const handleConnectWallet = async () => {\n if (walletActionPending || nexusLoading) return;\n\n const clickHandler = onConnectClick || onConnectWallet;\n if (clickHandler) {\n setWalletActionPending(true);\n setTxError(null);\n try {\n await clickHandler();\n } catch (error: any) {\n setTxError(error?.message || \"Unable to connect wallet.\");\n } finally {\n setWalletActionPending(false);\n }\n return;\n }\n\n if (isWalletConnectPending) return;\n\n setWalletActionPending(true);\n setTxError(null);\n try {\n let activeConnector = connector;\n\n if (walletStatus !== \"connected\") {\n const nextConnector = connectors[0];\n if (!nextConnector) {\n throw new Error(\"No wallet connector available.\");\n }\n await connectAsync({ connector: nextConnector });\n return;\n }\n\n const effectiveProvider = await getEffectiveWalletProvider(\n activeConnector\n );\n\n if (\n !effectiveProvider ||\n typeof effectiveProvider.request !== \"function\"\n ) {\n throw new Error(\"Wallet provider is not ready yet.\");\n }\n\n await handleInit(effectiveProvider as EthereumProvider, ownerAddress);\n } catch (error: any) {\n setTxError(error?.message || \"Unable to connect wallet.\");\n } finally {\n setWalletActionPending(false);\n }\n };\n\n const handleOpenRecipientEditor = () => {\n if (activeMode === \"swap\" && !recipientAddress && defaultRecipientAddress) {\n setRecipientAddress(defaultRecipientAddress);\n }\n setTxError(null);\n openDrawerStep(\"enter-recipient\");\n };\n\n const handleResetRecipientToDefault = () => {\n setRecipientAddress(defaultRecipientAddress);\n setTxError(null);\n };\n\n const handleSaveRecipient = () => {\n const next = recipientAddress.trim();\n if (!next) {\n setTxError(\"Recipient address is required\");\n return;\n }\n if (!next.endsWith(\".eth\") && !isAddress(next)) {\n setTxError(\"Incorrect address\");\n return;\n }\n if (\n activeMode === \"send\" &&\n ownerAddress &&\n isAddress(next) &&\n next.toLowerCase() === ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\n setRecipientAddress(next);\n setTxError(null);\n closeDrawerToIdle();\n };\n\n /** Start swap flow — v2 SDK per-operation onIntent hooks populate preview. */\n const handleEnterPreview = async (options: { background?: boolean } = {}) => {\n const { background = false } = options;\n const isExactOutFlow = activeMode === \"deposit\" || activeMode === \"send\";\n const quoteInputKey = activeQuoteInputKeyRef.current;\n const isCurrentQuoteInput = () =>\n activeQuoteInputKeyRef.current === quoteInputKey;\n\n if (!toToken) {\n return;\n }\n\n if (isExactOutFlow) {\n if (!hasPositiveDecimalInput(amount)) {\n return;\n }\n } else if (!hasReadyExactInSwapInput(fromTokens, toToken)) {\n if (!background) {\n setTxError(null);\n setSwapQuoteIssue(null);\n }\n return;\n }\n\n const receiveIssue = buildReceiveAmountIssue();\n if (receiveIssue) {\n applyReceiveAmountIssue(receiveIssue);\n if (!background && swapStepRef.current !== \"idle\") {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n }\n return;\n }\n\n const configuredIssue = buildConfiguredAmountIssue();\n if (configuredIssue) {\n clearPreviewForBlockingAmountIssue();\n if (!background && swapStepRef.current !== \"idle\") {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n }\n return;\n }\n\n const sourceBalanceIssue = isExactOutFlow\n ? buildExactOutSourceBalanceIssue()\n : null;\n if (sourceBalanceIssue) {\n clearPreviewForBlockingAmountIssue();\n setSwapQuoteIssue(sourceBalanceIssue);\n if (!background && swapStepRef.current !== \"idle\") {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n }\n return;\n }\n\n if (!background && activeMode === \"deposit\") {\n trackDeposit(\"deposit_confirm_clicked\", {\n amountToken: depositTokenDisplay,\n amountUsd: Number(depositUsdDisplay) || 0,\n selectionMode: sourceSelectionTouched ? \"manual\" : \"auto\",\n sourceCount: (intentData?.sources ?? []).length,\n });\n }\n\n setTxError(null);\n setSwapQuoteIssue(null);\n\n if (\n !background &&\n swapIntentRef.current?.runId === swapRunIdRef.current &&\n swapIntentRef.current?.quoteInputKey === quoteInputKey &&\n intentData &&\n (activeMode !== \"send\" || Boolean(recipientAddress)) &&\n ((activeMode !== \"deposit\" && activeMode !== \"send\") ||\n (intentData.sources ?? []).length > 0 ||\n Boolean(intentData.destination))\n ) {\n swapStepRef.current = \"preview-intent\";\n setSwapStep(\"preview-intent\");\n return;\n }\n\n let resolvedRecipientAddress =\n activeMode === \"swap\" ? effectiveRecipientAddress : recipientAddress;\n\n if (!background && activeMode === \"send\" && !resolvedRecipientAddress) {\n setTxError(\"Recipient address is required\");\n return;\n }\n\n if ((!background && activeMode === \"send\") || hasCustomSwapRecipient) {\n if (!resolvedRecipientAddress) {\n setTxError(\"Recipient address is required\");\n return;\n }\n\n if (\n activeMode === \"send\" &&\n ownerAddress &&\n isAddress(resolvedRecipientAddress) &&\n resolvedRecipientAddress.toLowerCase() === ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\n\n if (resolvedRecipientAddress.endsWith(\".eth\")) {\n try {\n const mainnetClient =\n publicClient?.chain?.id === 1\n ? publicClient\n : createPublicClient({\n chain: mainnet,\n transport: http(),\n });\n const ensAddr = await mainnetClient.getEnsAddress({\n name: normalize(resolvedRecipientAddress),\n });\n if (!ensAddr) {\n setTxError(\"Could not resolve ENS name to an address.\");\n return;\n }\n resolvedRecipientAddress = ensAddr;\n } catch (e: any) {\n setTxError(e.message || \"Failed to resolve ENS name.\");\n return;\n }\n } else {\n if (!isAddress(resolvedRecipientAddress)) {\n setTxError(\"Invalid recipient address.\");\n return;\n }\n }\n\n if (\n activeMode === \"send\" &&\n ownerAddress &&\n isAddress(resolvedRecipientAddress) &&\n resolvedRecipientAddress.toLowerCase() === ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\n }\n\n if (!isCurrentQuoteInput()) {\n return;\n }\n\n if (!background) {\n swapStepRef.current = \"preview-intent\";\n setSwapStep(\"preview-intent\");\n }\n setIntentLoading(true);\n setQuoteRefreshing(background);\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n swapIntentRef.current?.deny();\n swapIntentRef.current = null;\n if (!background) {\n resetProgressEvents();\n swapStepsListRef.current = [];\n resetSteps();\n }\n\n if (!nexusSDK) {\n setTxError(\"SDK not initialized\");\n if (!background) {\n setSwapStep(\"idle\");\n }\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n swapRunIdRef.current += 1;\n const runId = swapRunIdRef.current;\n\n const isActionPlanStep = (step: SwapStepType | BridgeStepType) => {\n const type = getProgressStepType(step);\n return (\n type === \"APPROVAL\" ||\n type === \"TRANSACTION_SENT\" ||\n type === \"TRANSACTION_CONFIRMED\"\n );\n };\n\n const hasSwapPlanSteps = (stepList: Array) =>\n stepList.some((step) => !isActionPlanStep(step));\n\n const handleProgressStepSideEffects = (\n event: any,\n step: SwapStepType | BridgeStepType,\n completed: boolean\n ) => {\n const type = getProgressStepType(step);\n const rawStepType = String(\n event?.stepType ?? (step as any)?.type ?? (step as any)?.typeID ?? \"\"\n ).toLowerCase();\n const rawState = String(event?.state ?? \"\").toLowerCase();\n const explorerUrl = getPlanStepExplorerUrl(event, step);\n const sourceMovementStep = isSourceMovementProgressStep(type, rawStepType);\n const hasSourceMovementTransaction = Boolean(\n explorerUrl || getPlanStepTransactionHash(event, step)\n );\n const intentExplorerUrl = getEventIntentExplorerUrl(\n appConfig.nexusNetwork,\n event,\n step\n );\n\n patchCurrentIntentExplorerUrl(intentExplorerUrl);\n\n if (\n type === \"TRANSACTION_SENT\" ||\n type === \"TRANSACTION_CONFIRMED\" ||\n type === \"SOURCE_SWAP\" ||\n type === \"BRIDGE_DEPOSIT\" ||\n type === \"BRIDGE_INTENT_SUBMISSION\" ||\n type === \"BRIDGE_FILL\" ||\n type === \"DESTINATION_SWAP\" ||\n type === \"SWAP_COMPLETE\" ||\n type === \"SWAP_SKIPPED\"\n ) {\n markSwapExecutionStarted();\n }\n\n if (\n sourceMovementStep &&\n hasSourceMovementTransaction &&\n PLAN_STEP_FUNDS_MAY_HAVE_MOVED_STATES.has(rawState)\n ) {\n fundsMovedRef.current = true;\n }\n\n if (explorerUrl) {\n if (\n rawStepType === \"destination_swap\" ||\n rawStepType === \"execute_transaction\" ||\n type.includes(\"DESTINATION_SWAP\") ||\n type === \"TRANSACTION_SENT\" ||\n type === \"TRANSACTION_CONFIRMED\"\n ) {\n mergeExplorerUrls({ destinationExplorerUrl: explorerUrl });\n } else if (\n sourceMovementStep\n ) {\n mergeExplorerUrls({ sourceExplorerUrl: explorerUrl });\n }\n\n if (\n !intentUrlRef.current &&\n (rawStepType === \"bridge_intent_submission\" ||\n rawStepType === \"request_submission\" ||\n type === \"BRIDGE_INTENT_SUBMISSION\")\n ) {\n patchCurrentIntentExplorerUrl(explorerUrl);\n }\n }\n\n if (completed) {\n onStepComplete(step as SwapStepType);\n }\n };\n\n const handlePlanEvent = (event: any) => {\n if (event.type === \"plan_preview\" || event.type === \"plan_confirmed\") {\n const stepList = Array.isArray(event.plan?.steps)\n ? event.plan.steps.map((step: any) =>\n normalizePlanStep(step, step?.type, undefined, false)\n )\n : [];\n logSwapPlanSteps(event.type, stepList, event.plan?.steps);\n if (stepList.length === 0) return;\n\n if (hasSwapPlanSteps(stepList)) {\n swapStepsListRef.current = stepList as SwapStepType[];\n appendProgressListEvent(\n PROGRESS_EVENT_NAMES.SWAP_PLAN_LIST,\n stepList\n );\n } else {\n appendProgressListEvent(\n PROGRESS_EVENT_NAMES.BRIDGE_PLAN_LIST,\n stepList\n );\n }\n onStepsList(stepList as SwapStepType[]);\n return;\n }\n\n if (event.type !== \"plan_progress\") {\n logSdkSwapEvent(\"unhandled typed event\", event);\n return;\n }\n\n const state = String(event.state ?? \"\").toLowerCase();\n const completed = PLAN_FINAL_STATES.has(state);\n const step = normalizePlanStep(\n event.step,\n event.stepType,\n event.state,\n completed\n );\n const eventName = isActionPlanStep(step)\n ? PROGRESS_EVENT_NAMES.BRIDGE_PLAN_PROGRESS\n : PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS;\n\n logSwapPlanProgress(event, step, eventName, completed);\n appendProgressEvent(eventName, step, completed, event);\n handleProgressStepSideEffects(event, step, completed);\n };\n\n const appendSkippedSwapProgress = () => {\n const step = {\n completed: true,\n type: \"SWAP_SKIPPED\",\n typeID: \"SWAP_SKIPPED\",\n } as SwapStepType;\n enterSkippedSwapProgress();\n appendProgressEvent(PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS, step, true);\n onStepComplete(step);\n };\n\n const handleSwapEvent = (event: any) => {\n if (!event || typeof event !== \"object\") return;\n if (typeof event.type === \"string\") {\n handlePlanEvent(event);\n return;\n }\n logSdkSwapEvent(\"ignored event without string type\", event);\n };\n\n const onEvent = (event: any) => {\n const isCurrentRun = swapRunIdRef.current === runId;\n const isCurrentQuote = isCurrentQuoteInput();\n logSdkSwapEvent(\"onEvent\", event, {\n currentRunId: swapRunIdRef.current,\n isCurrentQuote,\n isCurrentRun,\n quoteInputKey,\n runId,\n });\n if (!isCurrentRun || !isCurrentQuote) {\n logSdkSwapEvent(\"ignored stale onEvent\", event, {\n currentRunId: swapRunIdRef.current,\n isCurrentQuote,\n quoteInputKey,\n runId,\n });\n return;\n }\n patchCurrentIntentExplorerUrl(\n getEventIntentExplorerUrl(appConfig.nexusNetwork, event)\n );\n handleSwapEvent(event);\n };\n\n const buildRecipientTransferExecuteConfig = (transferAmount: bigint) => {\n if (!resolvedRecipientAddress) {\n throw new Error(\"Recipient address is required\");\n }\n\n const isNative =\n !toToken.contractAddress ||\n toToken.contractAddress.toLowerCase() ===\n \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\" ||\n toToken.contractAddress ===\n \"0x0000000000000000000000000000000000000000\";\n\n if (isNative) {\n return {\n to: resolvedRecipientAddress as `0x${string}`,\n value: transferAmount,\n gas: BigInt(100000),\n };\n }\n\n return {\n to: toToken.contractAddress as `0x${string}`,\n data: encodeFunctionData({\n abi: erc20Abi,\n functionName: \"transfer\",\n args: [resolvedRecipientAddress as `0x${string}`, transferAmount],\n }),\n gas: BigInt(100000),\n };\n };\n\n const executeRecipientTransfer = async (transferAmount: bigint) => {\n const result = await nexusSDK.execute(\n {\n toChainId: toToken.chainId!,\n ...buildRecipientTransferExecuteConfig(transferAmount),\n },\n { onEvent }\n );\n const finalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result\n );\n if (finalExplorerUrl) {\n setTransferExplorerUrl(finalExplorerUrl);\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n return finalExplorerUrl;\n };\n\n try {\n if (!isExactOutFlow) {\n const fromPayload: {\n chainId: number;\n tokenAddress: `0x${string}`;\n amountRaw: bigint;\n }[] = [];\n\n const exactInSourceTokens = getReadyExactInSourceTokens(fromTokens);\n\n for (const token of exactInSourceTokens) {\n // Determine the amount to use for this specific token\n let rawAmountStr = token.userAmount;\n if (!rawAmountStr && exactInSourceTokens.length === 1) {\n rawAmountStr = amount; // fallback for single-token case\n }\n\n let cleanAmount = parseFiatNumber(rawAmountStr) ?? new Decimal(0);\n if (cleanAmount.lte(0)) continue;\n\n if (token.userAmountMode === \"usd\") {\n const tokenBalance =\n parseFiatNumber(token.balance) ?? new Decimal(0);\n const fiatBalance =\n parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n const price = tokenBalance.gt(0)\n ? fiatBalance.div(tokenBalance)\n : new Decimal(0);\n if (price.gt(0)) {\n cleanAmount = cleanAmount.div(price);\n } else {\n cleanAmount = new Decimal(0);\n }\n }\n\n if (cleanAmount.lte(0)) continue;\n\n const safeTokenAmountStr = toViemDecimalString(\n cleanAmount,\n token.decimals || 18\n );\n\n fromPayload.push({\n chainId: token.chainId!,\n tokenAddress: token.contractAddress as `0x${string}`,\n amountRaw: parseUnits(safeTokenAmountStr, token.decimals || 18),\n });\n }\n\n if (fromPayload.length === 0) {\n throw new Error(\"No source amount available for swap.\");\n }\n\n resetExplorerUrls();\n const exactInSwapPayload = {\n sources: fromPayload,\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n };\n let intentExplorerUrl: string | null = null;\n let intentId = currentSwapEntry?.intentId;\n let finalExplorerUrl: string | null =\n explorerUrlsRef.current.destinationExplorerUrl ||\n explorerUrlsRef.current.sourceExplorerUrl;\n\n if (hasCustomSwapRecipient && resolvedRecipientAddress) {\n const sdkWithOptionalTransfer = nexusSDK as any;\n\n if (typeof sdkWithOptionalTransfer.swapAndTransfer === \"function\") {\n const swapAndTransferExactInInput = {\n mode: \"exactIn\",\n recipient: resolvedRecipientAddress as `0x${string}`,\n ...exactInSwapPayload,\n };\n logSdkIntentInput(\"swapAndTransfer exactIn\", swapAndTransferExactInInput, {\n activeMode,\n quoteInputKey,\n runId,\n });\n const result = await sdkWithOptionalTransfer.swapAndTransfer(\n swapAndTransferExactInInput,\n { onEvent }\n );\n if (result?.success === false) {\n throw new Error(result?.error || \"Swap and transfer failed\");\n }\n\n const swapResult = getSdkSwapResult(result);\n intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result,\n swapResult\n );\n intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const resultFinalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result,\n swapResult\n );\n finalExplorerUrl = resultFinalExplorerUrl || finalExplorerUrl;\n if (resultFinalExplorerUrl) {\n setTransferExplorerUrl(resultFinalExplorerUrl);\n mergeExplorerUrls({\n destinationExplorerUrl: resultFinalExplorerUrl,\n });\n }\n } else {\n logSdkIntentInput(\"swapWithExactIn\", exactInSwapPayload, {\n activeMode,\n quoteInputKey,\n runId,\n });\n startIntentFetchTiming({\n background,\n operation: \"swapWithExactIn\",\n quoteInputKey,\n runId,\n });\n const result = await nexusSDK.swapWithExactIn(exactInSwapPayload, {\n hooks: {\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n },\n onEvent,\n });\n\n intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result\n );\n intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n\n const latestSwapIntent = (\n swapIntentRef.current as unknown as {\n intent?: SwapIntentData;\n } | null\n )?.intent;\n const transferAmount = latestSwapIntent?.destination?.amount;\n if (!transferAmount) {\n throw new Error(\n \"Unable to determine received amount to transfer.\"\n );\n }\n\n const transferAmountBigInt = parseUnits(\n toViemDecimalString(transferAmount, toToken.decimals || 18),\n toToken.decimals || 18\n );\n finalExplorerUrl =\n (await executeRecipientTransfer(transferAmountBigInt)) ||\n finalExplorerUrl;\n }\n } else {\n // Start exact-in swap — the intent hook will fire and populate preview\n logSdkIntentInput(\"swapWithExactIn\", exactInSwapPayload, {\n activeMode,\n quoteInputKey,\n runId,\n });\n startIntentFetchTiming({\n background,\n operation: \"swapWithExactIn\",\n quoteInputKey,\n runId,\n });\n const result = await nexusSDK.swapWithExactIn(exactInSwapPayload, {\n hooks: {\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n },\n onEvent,\n });\n intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result\n );\n intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const swapResult = getSdkSwapResult(result);\n const resultFinalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result,\n swapResult\n );\n finalExplorerUrl = resultFinalExplorerUrl || finalExplorerUrl;\n if (resultFinalExplorerUrl) {\n setTransferExplorerUrl(resultFinalExplorerUrl);\n mergeExplorerUrls({\n destinationExplorerUrl: resultFinalExplorerUrl,\n });\n }\n }\n\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n const resolvedFinalExplorerUrl =\n finalExplorerUrl ||\n explorerUrlsRef.current.destinationExplorerUrl ||\n explorerUrlsRef.current.sourceExplorerUrl;\n const resolvedIntentExplorerUrl =\n intentExplorerUrl || intentUrlRef.current;\n finishCurrentSwapHistoryEntry(\"fulfilled\", {\n finalExplorerUrl: resolvedFinalExplorerUrl,\n ...(resolvedIntentExplorerUrl\n ? { intentExplorerUrl: resolvedIntentExplorerUrl }\n : {}),\n ...(intentId ? { intentId } : {}),\n });\n resetInputsAfterSuccessfulExecution();\n onComplete?.();\n setSwapStep(\"success\");\n }\n } else {\n const exactOutAmountString =\n activeMode === \"deposit\"\n ? depositTokenAmountForQuote\n ?.toDecimalPlaces(toToken.decimals || 18, Decimal.ROUND_DOWN)\n .toFixed()\n : amount;\n if (!exactOutAmountString || new Decimal(exactOutAmountString).lte(0)) {\n setTxError(\n depositAmountMode === \"usd\"\n ? \"Unable to convert USD amount into the destination token amount.\"\n : \"Enter a valid amount.\"\n );\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n const amountBigInt = parseUnits(\n toViemDecimalString(exactOutAmountString, toToken.decimals || 18),\n toToken.decimals || 18\n );\n\n resetExplorerUrls();\n\n const fromSourcesPayload = buildExactOutSourcesPayload(\n getExactOutSourceTokens()\n );\n\n let executeConfig: any;\n if (activeMode === \"deposit\" && !selectedOpportunity?.executeDeposit) {\n throw new Error(\"Deposit config is missing executeDeposit.\");\n }\n\n if (activeMode === \"deposit\" && selectedOpportunity) {\n const user = (ownerAddress ?? connectedAddress) as `0x${string}`;\n\n const executeParams = selectedOpportunity.executeDeposit(\n selectedOpportunity.tokenSymbol,\n selectedOpportunity.tokenAddress,\n amountBigInt,\n selectedOpportunity.chainId,\n user\n );\n if (!isPositiveGasLimit(executeParams.gas)) {\n throw new Error(\n \"Deposit config executeDeposit must return a positive gas limit.\"\n );\n }\n executeConfig = {\n to: executeParams.to,\n value: executeParams.value,\n data: executeParams.data,\n tokenApproval: executeParams.tokenApproval,\n gas: executeParams.gas,\n };\n } else if (\n (activeMode === \"send\" || hasCustomSwapRecipient) &&\n resolvedRecipientAddress\n ) {\n executeConfig = buildRecipientTransferExecuteConfig(amountBigInt);\n }\n\n if (executeConfig?.tokenApproval) {\n executeConfig = {\n ...executeConfig,\n tokenApproval: {\n toTokenAddress:\n executeConfig.tokenApproval.toTokenAddress ||\n toToken.contractAddress,\n amount: executeConfig.tokenApproval.amount,\n spender: executeConfig.tokenApproval.spender,\n },\n };\n }\n\n if (executeConfig) {\n const sdkWithOptionalTransfer = nexusSDK as any;\n const isTransferExactOut =\n (activeMode === \"send\" || hasCustomSwapRecipient) &&\n typeof sdkWithOptionalTransfer.swapAndTransfer === \"function\";\n const exactOutOperationInput = isTransferExactOut\n ? {\n mode: \"exactOut\",\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmountRaw: amountBigInt,\n recipient: resolvedRecipientAddress as `0x${string}`,\n ...fromSourcesPayload,\n }\n : {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmountRaw: amountBigInt,\n execute: executeConfig,\n ...fromSourcesPayload,\n };\n logSdkIntentInput(\n isTransferExactOut\n ? \"swapAndTransfer exactOut\"\n : \"swapAndExecute exactOut\",\n exactOutOperationInput,\n {\n activeMode,\n quoteInputKey,\n runId,\n }\n );\n startIntentFetchTiming({\n background,\n operation: isTransferExactOut\n ? \"swapAndTransfer exactOut\"\n : \"swapAndExecute exactOut\",\n quoteInputKey,\n runId,\n });\n const result =\n isTransferExactOut\n ? await sdkWithOptionalTransfer.swapAndTransfer(\n exactOutOperationInput,\n {\n onEvent,\n onIntent: (data: any) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n }\n )\n : await nexusSDK.swapAndExecute(\n exactOutOperationInput as any,\n {\n onEvent,\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n }\n );\n\n const swapResult = result?.swapResult ?? result?.result ?? null;\n const swapSkipped = Boolean((result as any)?.swapSkipped);\n if (swapSkipped) {\n appendSkippedSwapProgress();\n }\n if (\n !swapResult &&\n !swapSkipped &&\n activeMode !== \"send\" &&\n !hasCustomSwapRecipient\n ) {\n throw new Error(\"Swap failed\");\n }\n const executeTxHash = getSdkTransactionHash(result);\n const intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result,\n swapResult\n );\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const finalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n executeTxHash,\n result,\n swapResult\n );\n if (finalExplorerUrl) {\n if (activeMode === \"send\" || hasCustomSwapRecipient) {\n setTransferExplorerUrl(finalExplorerUrl);\n }\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n patchCurrentSwapHistoryEntry({\n ...(finalExplorerUrl ? { finalExplorerUrl } : {}),\n ...(intentExplorerUrl ? { intentExplorerUrl } : {}),\n ...(intentId ? { intentId } : {}),\n });\n } else {\n const exactOutSwapInput = {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmountRaw: amountBigInt,\n ...fromSourcesPayload,\n };\n logSdkIntentInput(\"swapWithExactOut\", exactOutSwapInput, {\n activeMode,\n quoteInputKey,\n runId,\n });\n startIntentFetchTiming({\n background,\n operation: \"swapWithExactOut\",\n quoteInputKey,\n runId,\n });\n const result = await nexusSDK.swapWithExactOut(\n exactOutSwapInput,\n {\n hooks: {\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n },\n onEvent,\n }\n );\n const intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result\n );\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const swapResult = getSdkSwapResult(result);\n const finalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result,\n swapResult\n );\n if (finalExplorerUrl) {\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n patchCurrentSwapHistoryEntry({\n ...(finalExplorerUrl ? { finalExplorerUrl } : {}),\n ...(intentExplorerUrl ? { intentExplorerUrl } : {}),\n ...(intentId ? { intentId } : {}),\n });\n }\n\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n finishCurrentSwapHistoryEntry(\"fulfilled\");\n resetInputsAfterSuccessfulExecution();\n onComplete?.();\n if (activeMode === \"deposit\") {\n reachedTerminalRef.current = true;\n const now = Date.now();\n trackDeposit(\"deposit_completed\", {\n postConfirmDurationMs: previewConfirmedTsRef.current\n ? now - previewConfirmedTsRef.current\n : 0,\n totalDurationMs: now - widgetOpenedTsRef.current,\n attemptCount: attemptCountRef.current,\n amountToken: depositTokenDisplay,\n amountUsd: Number(depositUsdDisplay) || 0,\n });\n }\n setSwapStep(\"success\");\n }\n }\n } catch (err: any) {\n const caughtTimeout = isTimeoutLikeError(err);\n if (caughtTimeout) {\n console.warn(\"Timeout in handleEnterPreview:\", err);\n } else {\n console.error(\"Error in handleEnterPreview:\", err);\n }\n if (swapRunIdRef.current !== runId || !isCurrentQuoteInput()) {\n return;\n }\n finishIntentFetchTiming(runId, \"failed\");\n if (activeMode === \"deposit\" && err?.code !== \"USER_DENIED_INTENT\") {\n const hasActiveExecution =\n swapStepRef.current === \"progress\" &&\n Boolean(currentSwapIdRef.current);\n const isInsufficient = isInsufficientSourcesError(err);\n const errMessage =\n (typeof err?.message === \"string\" ? err.message : \"\") ||\n (typeof err === \"string\" ? err : \"\");\n const errName = typeof err?.name === \"string\" ? err.name : \"\";\n const isTimeout = isTimeoutLikeError(err);\n const isUserRejected =\n err?.code === 4001 ||\n err?.code === \"ACTION_REJECTED\" ||\n errName === \"UserRejectedRequestError\" ||\n /user rejected|user denied/i.test(errMessage);\n const failedAtStep:\n | \"simulation\"\n | \"nexus_operation\"\n | \"execute_leg\"\n | \"unknown\" = !hasActiveExecution ? \"simulation\" : \"nexus_operation\";\n const errorCategory: string = isUserRejected\n ? \"user_rejected\"\n : isTimeout\n ? \"timeout\"\n : isInsufficient\n ? \"no_eligible_sources\"\n : !hasActiveExecution\n ? \"quote_failed\"\n : fundsMovedRef.current\n ? \"pending_after_funds_moved\"\n : \"execution_failed\";\n reachedTerminalRef.current = true;\n if (fundsMovedRef.current) {\n trackDeposit(\"deposit_pending_after_funds_moved\", {\n errorCode: err?.code ?? \"UNKNOWN\",\n errorMessage: errMessage || \"Transaction pending.\",\n intentUrl: intentUrlRef.current,\n failedAtStep,\n });\n } else {\n trackDeposit(\"deposit_failed\", {\n errorCode: err?.code ?? \"UNKNOWN\",\n errorCategory,\n errorMessage: errMessage || \"Transaction failed.\",\n failedAtStep,\n });\n }\n }\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n const hasActiveExecution =\n swapStepRef.current === \"progress\" && Boolean(currentSwapIdRef.current);\n const isTimeout = caughtTimeout;\n const showFailedProgressThenReceipt = (\n error: string,\n patch: Partial = {}\n ) => {\n const failedProgressEvent = progressEventsRef.current.at(-1);\n const isTransferExecution =\n activeMode === \"send\" || hasCustomSwapRecipient;\n const fallbackFailedStep =\n activeMode === \"deposit\" || isTransferExecution\n ? ({ type: \"APPROVAL\", typeID: \"AP\" } as BridgeStepType)\n : ({\n type: \"DETERMINING_SWAP\",\n typeID: \"DETERMINING_SWAP\",\n } as unknown as SwapStepType);\n const failedStep = failedProgressEvent?.step ?? fallbackFailedStep;\n const autoRefundAvailable =\n isAutoRefundAvailableProgressEvent(failedProgressEvent);\n setFailedProgressStep(failedStep);\n finishCurrentSwapHistoryEntry(\"failed\", {\n error,\n autoRefundAvailable,\n failureDescription: getFailureDescriptionForProgressStep(\n failedStep,\n autoRefundAvailable\n ),\n failureMessage: getFailureMessageForProgressStep(\n failedStep,\n hasCustomSwapRecipient ? \"send\" : activeMode,\n autoRefundAvailable\n ),\n failedStepType: getProgressStepType(failedStep),\n ...patch,\n });\n window.setTimeout(() => {\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n setSwapStep(\"failed\");\n }\n }, 700);\n };\n const showTimeoutReceipt = (\n message = \"Transaction timed out\",\n patch: Partial = {}\n ) => {\n finishCurrentSwapHistoryEntry(\"timeout\", {\n error: message,\n failureDescription:\n \"This transaction is still pending. Check the intent explorer for the latest status.\",\n failureMessage: TIMEOUT_LABEL,\n ...patch,\n });\n window.setTimeout(() => {\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n setSwapStep(\"failed\");\n }\n }, 700);\n };\n if (err?.code === \"USER_DENIED_INTENT\") {\n if (hasActiveExecution) {\n showFailedProgressThenReceipt(\"Transaction cancelled by user\");\n } else if (!background && swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n return;\n }\n if (isInsufficientSourcesError(err) && !hasActiveExecution) {\n const issue = buildInsufficientSourcesIssue(err);\n if (!background || swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n setTxError(null);\n setSwapQuoteIssue(issue);\n onError?.(issue.message);\n return;\n }\n if (isExactOutFlow && isViemInvalidDecimalError(err) && !hasActiveExecution) {\n const issue = buildExactOutSourceBalanceIssue();\n if (issue) {\n if (!background || swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n setTxError(null);\n setSwapQuoteIssue(issue);\n onError?.(issue.message);\n return;\n }\n }\n const errorMessage =\n err?.message ||\n (typeof err === \"string\"\n ? err\n : \"Transaction failed. Please try again or check console.\");\n if (isTimeout && hasActiveExecution) {\n showTimeoutReceipt(errorMessage);\n setTxError(null);\n return;\n }\n if (hasActiveExecution && fundsMovedRef.current) {\n showTimeoutReceipt(errorMessage, {\n failureDescription:\n \"Funds have already moved and the transaction is still pending. Check the intent explorer for the latest status.\",\n });\n setTxError(null);\n return;\n }\n if (hasActiveExecution) {\n showFailedProgressThenReceipt(errorMessage);\n } else if (!background || swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n setTxError(errorMessage);\n onError?.(errorMessage);\n }\n };\n\n const hasInsufficientSourcesQuoteIssue =\n swapQuoteIssue?.type === \"insufficientSources\";\n const hasReceiveAmountQuoteIssue = Boolean(receiveAmountIssue);\n\n useEffect(() => {\n if (activeMode !== \"swap\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = false;\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n }\n\n if (hasReceiveAmountQuoteIssue) {\n clearPendingSwapIntent(true);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n if (hasInsufficientSourcesQuoteIssue) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n const hasEnoughForQuote = hasReadyExactInSwapInput(fromTokens, toToken);\n\n if (!hasEnoughForQuote) {\n clearPendingSwapIntent();\n setSwapQuoteIssue(null);\n setTxError(null);\n return;\n }\n\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n return;\n }\n\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n let quoteStarted = false;\n const quoteDelay = getQuoteRequestDelay();\n const timer = window.setTimeout(() => {\n quoteStarted = true;\n void handleEnterPreview({ background: true });\n }, quoteDelay);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (!quoteStarted && swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n activeQuoteInputKey,\n amount,\n defaultRecipientAddress,\n fromTokensQuoteKey,\n hasCurrentQuoteIntent,\n hasInsufficientSourcesQuoteIssue,\n hasReceiveAmountQuoteIssue,\n getQuoteRequestDelay,\n nexusSDK,\n recipientAddress,\n swapStep,\n toTokenQuoteKey,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = false;\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n }\n\n if (hasReceiveAmountQuoteIssue) {\n clearPendingSwapIntent(true);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n if (hasInsufficientSourcesQuoteIssue) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n const parsedAmount = parseFiatNumber(amount);\n const hasEnoughForQuote = Boolean(\n parsedAmount?.gt(0) &&\n toToken &&\n selectedOpportunity &&\n depositTokenAmountForQuote\n );\n\n if (!hasEnoughForQuote) {\n clearPendingSwapIntent();\n clearSelectedSources();\n return;\n }\n\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n return;\n }\n\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n let quoteStarted = false;\n const quoteDelay = getQuoteRequestDelay();\n const timer = window.setTimeout(() => {\n quoteStarted = true;\n void handleEnterPreview({ background: true });\n }, quoteDelay);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (!quoteStarted && swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n amount,\n activeQuoteInputKey,\n depositAmountMode,\n depositQuoteAmountKey,\n hasCurrentQuoteIntent,\n hasInsufficientSourcesQuoteIssue,\n hasReceiveAmountQuoteIssue,\n getQuoteRequestDelay,\n nexusSDK,\n sourceSelectionRevision,\n selectedOpportunityIdentity,\n swapStep,\n toTokenQuoteKey,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"send\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = false;\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n }\n\n if (hasReceiveAmountQuoteIssue) {\n clearPendingSwapIntent(true);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n if (hasInsufficientSourcesQuoteIssue) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n const parsedAmount = parseFiatNumber(amount);\n const hasEnoughForQuote = Boolean(parsedAmount?.gt(0) && toToken);\n\n if (!hasEnoughForQuote) {\n clearPendingSwapIntent();\n clearSelectedSources();\n return;\n }\n\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n return;\n }\n\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n let quoteStarted = false;\n const quoteDelay = getQuoteRequestDelay();\n const timer = window.setTimeout(() => {\n quoteStarted = true;\n void handleEnterPreview({ background: true });\n }, quoteDelay);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (!quoteStarted && swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n amount,\n activeQuoteInputKey,\n hasCurrentQuoteIntent,\n hasInsufficientSourcesQuoteIssue,\n hasReceiveAmountQuoteIssue,\n getQuoteRequestDelay,\n nexusSDK,\n sourceSelectionRevision,\n swapStep,\n toTokenQuoteKey,\n ]);\n\n const refreshActiveSwapIntent = useCallback(async () => {\n if (receiveAmountIssueRef.current) return;\n\n const activeIntent = swapIntentRef.current;\n if (\n !activeIntent ||\n intentLoading ||\n quoteRefreshing ||\n receiveMaxCalculating ||\n previewQuoteRefreshing\n ) {\n return;\n }\n\n const runId = activeIntent.runId;\n const quoteInputKey = activeIntent.quoteInputKey;\n if (!quoteInputKey || activeQuoteInputKeyRef.current !== quoteInputKey) {\n return;\n }\n const isPreviewRefresh = swapStepRef.current === \"preview-intent\";\n if (isPreviewRefresh) {\n setPreviewQuoteRefreshing(true);\n } else {\n setQuoteRefreshing(true);\n }\n try {\n const updatedRaw = await activeIntent.refresh();\n const updatedBridgeProvider = normalizeBridgeProvider(\n (updatedRaw as any)?.bridgeProvider ??\n (updatedRaw as any)?.normalizedIntent?.bridgeProvider ??\n (updatedRaw as any)?.swap?.bridgeProvider ??\n activeIntent.intent?.bridgeProvider\n );\n const updated = normalizeRenderableSwapIntentData(\n updatedRaw,\n updatedBridgeProvider\n );\n if (\n !updated ||\n swapRunIdRef.current !== runId ||\n activeQuoteInputKeyRef.current !== quoteInputKey\n ) {\n return;\n }\n\n if (swapIntentRef.current) {\n swapIntentRef.current.intent = updated;\n }\n applySwapIntent(updated);\n } catch (err) {\n console.error(\"Unable to refresh swap intent\", err);\n } finally {\n if (\n swapRunIdRef.current === runId &&\n activeQuoteInputKeyRef.current === quoteInputKey\n ) {\n if (isPreviewRefresh) {\n setPreviewQuoteRefreshing(false);\n } else {\n setQuoteRefreshing(false);\n }\n }\n }\n }, [\n applySwapIntent,\n intentLoading,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n ]);\n\n useEffect(() => {\n const hasRefreshableIntent =\n (activeMode === \"swap\" ||\n activeMode === \"deposit\" ||\n activeMode === \"send\") &&\n Boolean(\n intentData &&\n swapIntentRef.current &&\n swapIntentRef.current.quoteInputKey === activeQuoteInputKey\n ) &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n\n if (!hasRefreshableIntent || receiveAmountIssue) {\n setQuoteRefreshProgress(0);\n setQuoteRefreshSecondsRemaining(0);\n return;\n }\n\n let cancelled = false;\n let timeout: number | undefined;\n\n const scheduleRefresh = () => {\n const quoteAge = Date.now() - lastSwapIntentRefreshAtRef.current;\n const delay = Math.max(0, QUOTE_REFRESH_INTERVAL_MS - quoteAge);\n timeout = window.setTimeout(() => {\n if (receiveAmountIssueRef.current) {\n clearPendingSwapIntent(true);\n setQuoteRefreshProgress(0);\n setQuoteRefreshSecondsRemaining(0);\n return;\n }\n\n if (\n intentLoading ||\n quoteRefreshing ||\n receiveMaxCalculating ||\n previewQuoteRefreshing\n ) {\n if (!cancelled) {\n timeout = window.setTimeout(scheduleRefresh, 1000);\n }\n return;\n }\n\n void refreshActiveSwapIntent().finally(() => {\n if (!cancelled) {\n scheduleRefresh();\n }\n });\n }, delay);\n };\n\n scheduleRefresh();\n\n return () => {\n cancelled = true;\n if (timeout !== undefined) {\n window.clearTimeout(timeout);\n }\n };\n }, [\n activeMode,\n activeQuoteInputKey,\n intentData,\n intentLoading,\n receiveAmountIssue,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n refreshActiveSwapIntent,\n swapStep,\n ]);\n\n useEffect(() => {\n const hasRefreshableIntent =\n (activeMode === \"swap\" ||\n activeMode === \"deposit\" ||\n activeMode === \"send\") &&\n Boolean(\n intentData &&\n swapIntentRef.current &&\n swapIntentRef.current.quoteInputKey === activeQuoteInputKey\n ) &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n\n if (!hasRefreshableIntent || receiveAmountIssue) {\n setQuoteRefreshProgress(0);\n setQuoteRefreshSecondsRemaining(0);\n return;\n }\n\n const updateProgress = () => {\n const quoteAge = Date.now() - lastSwapIntentRefreshAtRef.current;\n const remaining = Math.max(0, QUOTE_REFRESH_INTERVAL_MS - quoteAge);\n setQuoteRefreshProgress(remaining / QUOTE_REFRESH_INTERVAL_MS);\n setQuoteRefreshSecondsRemaining(Math.ceil(remaining / 1000));\n };\n\n updateProgress();\n const interval = window.setInterval(updateProgress, 250);\n\n return () => window.clearInterval(interval);\n }, [\n activeMode,\n activeQuoteInputKey,\n intentData,\n receiveAmountIssue,\n swapStep,\n ]);\n\n /** User accepted swap from the preview — call allow() from the intent hook */\n const handleSwapAccept = () => {\n const activeIntent = swapIntentRef.current;\n if (activeIntent) {\n if (\n activeIntent.quoteInputKey &&\n activeQuoteInputKeyRef.current !== activeIntent.quoteInputKey\n ) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n setSwapStep(\"idle\");\n return;\n }\n if (activeMode === \"deposit\") {\n previewConfirmedTsRef.current = Date.now();\n attemptCountRef.current += 1;\n const timeInPreviewMs = previewViewedTsRef.current\n ? previewConfirmedTsRef.current - previewViewedTsRef.current\n : 0;\n trackDeposit(\"deposit_preview_confirmed\", {\n timeInPreviewMs,\n totalFeeUsd: Number(intentFeeUsd) || 0,\n sourceCount: (intentData?.sources ?? []).length,\n });\n }\n onStart?.();\n startSwapHistoryEntry();\n setSwapStep(\"progress\");\n setQuoteRefreshing(false);\n resetProgressEvents();\n if (swapStepsListRef.current.length > 0) {\n seed(swapStepsListRef.current);\n } else {\n resetSteps();\n }\n activeIntent.allow();\n }\n };\n\n // ---------------------------------------------------------------------------\n // Header title\n // ---------------------------------------------------------------------------\n const getTitle = () => {\n const configuredWidgetHeading = normalizeConfiguredString(\n appearanceConfig?.widgetHeading\n );\n if (swapStep === \"history\") return \"Transaction History\";\n // Drawer panels overlay the main page,\n // so the header should still show the main page title.\n\n if (swapStep === \"preview-intent\") {\n return activeMode === \"deposit\"\n ? \"Confirm Deposit\"\n : activeMode === \"send\"\n ? \"Confirm Send\"\n : \"Confirm Swap\";\n }\n\n if (activeMode === \"swap\") {\n if (swapStep === \"progress\") return \"Swapping…\";\n if (swapStep === \"success\") return \"Swap Complete\";\n if (swapStep === \"failed\" && currentSwapEntry?.status === \"timeout\") {\n return TIMEOUT_LABEL;\n }\n if (swapStep === \"failed\") return \"Swap Failed\";\n return configuredWidgetHeading ?? \"Swap and Bridge\";\n }\n if (activeMode === \"deposit\") {\n if (swapStep === \"progress\") return \"Depositing…\";\n if (swapStep === \"success\") return \"Deposit Complete\";\n if (swapStep === \"failed\" && currentSwapEntry?.status === \"timeout\") {\n return TIMEOUT_LABEL;\n }\n if (swapStep === \"failed\") return \"Deposit Failed\";\n return configuredWidgetHeading ?? \"Deposit\";\n }\n if (activeMode === \"send\") {\n if (swapStep === \"progress\") return \"Sending…\";\n if (swapStep === \"success\") return \"Send Complete\";\n if (swapStep === \"failed\" && currentSwapEntry?.status === \"timeout\") {\n return TIMEOUT_LABEL;\n }\n if (swapStep === \"failed\") return \"Send Failed\";\n return configuredWidgetHeading ?? \"Send\";\n }\n return configuredWidgetHeading ?? \"Nexus Widget\";\n };\n\n // Titles that should be center-aligned (main screens / confirm screens)\n // Left-aligned: choose-swap-asset, choose-receive-asset (sub-screens with subtitles)\n const isTitleCentered = () => {\n if (swapStep === \"history\") return false;\n return true; // idle, drawer panels, preview-intent, progress, etc.\n };\n\n const canGoBack = swapStep === \"preview-intent\" || swapStep === \"history\";\n const handleBack = () => {\n if (swapStep === \"history\") {\n setSwapStep(\"idle\");\n return;\n }\n if (swapStep === \"choose-swap-asset\") {\n handleSourcePickerCancel();\n return;\n }\n if (swapStep === \"choose-receive-asset\") {\n closeDrawerToIdle();\n return;\n }\n if (swapStep === \"enter-recipient\") {\n closeDrawerToIdle();\n return;\n }\n if (swapStep === \"preview-intent\") {\n const canRequoteAfterPreviewBack =\n activeMode === \"swap\"\n ? hasReadyExactInSwapInput(fromTokens, toToken)\n : canRefreshExactOutQuote();\n\n if (\n canRequoteAfterPreviewBack &&\n (activeMode === \"deposit\" || activeMode === \"send\")\n ) {\n setExactOutQuoteSourceModeValue(\"all\");\n }\n if (activeMode === \"deposit\" || activeMode === \"send\") {\n invalidateExactOutQuoteForRefresh();\n } else {\n clearPendingSwapIntent(true, {\n keepQuoteRefreshing: canRequoteAfterPreviewBack,\n });\n }\n if (canRequoteAfterPreviewBack && activeMode === \"swap\") {\n setQuoteRefreshing(true);\n setTxError(null);\n setSwapQuoteIssue(null);\n }\n setSwapStep(\"idle\");\n return;\n }\n if (swapStep === \"progress\") {\n return;\n } // can't go back during tx\n setSwapStep(\"idle\");\n };\n\n const handleSwapAmountChange = (val: string, panel: \"send\" | \"receive\") => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n setSwapQuoteIssue(null);\n setTxError(null);\n const nextAmount = parseFiatNumber(val);\n const receiveIssue = buildReceiveAmountIssue({ inputAmount: val });\n applyReceiveAmountIssue(receiveIssue);\n const hasSelectedSourceToken = fromTokens.some(\n (token) => token.chainId && token.contractAddress\n );\n const shouldLoadQuote = Boolean(\n !receiveIssue &&\n nexusSDK &&\n nextAmount?.gt(0) &&\n toToken &&\n hasSelectedSourceToken\n );\n if (!receiveIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n }\n setAmount(val);\n if (panel === \"receive\") {\n setFromTokens((prev) =>\n prev.map((token) => ({ ...token, userAmount: \"\" }))\n );\n }\n // Nexus Widget swaps are exact-in only. Exact-out is reserved for Deposit and Send.\n if (swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n };\n\n const handleSwapTokensUpdate = (tokens: SwapTokenOption[]) => {\n setSwapQuoteIssue(null);\n setTxError(null);\n applyReceiveAmountIssue(buildReceiveAmountIssue({ sourceTokens: tokens }));\n setFromTokens(tokens);\n };\n\n const handleDepositAmountChange = (val: string) => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n resetExactOutSourcesToAuto();\n maxPercentRunRef.current += 1;\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setSwapQuoteIssue(null);\n const nextAmount = parseFiatNumber(val);\n const receiveIssue = buildReceiveAmountIssue({ inputAmount: val });\n applyReceiveAmountIssue(receiveIssue);\n const shouldLoadQuote = Boolean(\n !receiveIssue &&\n nexusSDK &&\n nextAmount?.gt(0) &&\n toToken &&\n selectedOpportunity\n );\n if (!receiveIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n } else {\n clearSelectedSources();\n }\n setAmount(val);\n };\n\n const handleSendAmountChange = (val: string) => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n resetExactOutSourcesToAuto();\n maxPercentRunRef.current += 1;\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setSwapQuoteIssue(null);\n setSwapType(\"exactOut\");\n const nextAmount = parseFiatNumber(val);\n const receiveIssue = buildReceiveAmountIssue({\n inputAmount: val,\n type: \"exactOut\",\n });\n applyReceiveAmountIssue(receiveIssue);\n const shouldLoadQuote = Boolean(\n !receiveIssue && nexusSDK && nextAmount?.gt(0) && toToken\n );\n if (!receiveIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n } else {\n clearSelectedSources();\n }\n setAmount(val);\n };\n\n const handleDepositAmountModeToggle = () => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n resetExactOutSourcesToAuto();\n const rate = getDepositTokenUsdRate();\n const parsedAmount = parseFiatNumber(amount) ?? new Decimal(0);\n if (parsedAmount.gt(0) && rate.gt(0)) {\n const converted =\n depositAmountMode === \"token\"\n ? parsedAmount.mul(rate).toDecimalPlaces(2)\n : parsedAmount\n .div(rate)\n .toDecimalPlaces(\n getCappedTokenDisplayDecimals(toToken?.decimals),\n Decimal.ROUND_DOWN\n );\n setAmount(converted.toFixed());\n }\n clearPendingSwapIntent();\n setDepositAmountMode((current) => (current === \"token\" ? \"usd\" : \"token\"));\n };\n\n const handleDepositPercentSelect = async (pct: number) => {\n if (isConfiguredAmountFixed) return;\n if (!toToken) return;\n\n syncingIntentSourcesRef.current = false;\n setTxError(null);\n setSwapQuoteIssue(null);\n const runId = ++maxPercentRunRef.current;\n lastInputMethodRef.current =\n pct === 20\n ? \"percent_20\"\n : pct === 25\n ? \"percent_25\"\n : pct === 50\n ? \"percent_50\"\n : pct === 75\n ? \"percent_75\"\n : \"percent_max\";\n\n if (pct !== 100) {\n const nextAmount = getExactOutPercentAmountFromBalance(\n toToken,\n pct,\n depositAmountMode === \"usd\"\n );\n\n if (nextAmount) {\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setDepositAmountMode(nextAmount.mode);\n handleDepositAmountChange(nextAmount.amount);\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setTxError(\"Unable to calculate this percentage for the deposit asset.\");\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n const maxAmount = getExactOutPercentAmountFromBalance(\n toToken,\n 100,\n depositAmountMode === \"usd\"\n );\n if (runId !== maxPercentRunRef.current) return;\n if (!maxAmount) {\n setTxError(\"No depositable amount is available for this deposit.\");\n return;\n }\n\n setDepositAmountMode(maxAmount.mode);\n handleDepositAmountChange(maxAmount.amount);\n };\n\n const handleSendPercentSelect = async (pct: number) => {\n if (isConfiguredAmountFixed) return;\n if (!toToken) return;\n\n syncingIntentSourcesRef.current = false;\n setTxError(null);\n setSwapQuoteIssue(null);\n const runId = ++maxPercentRunRef.current;\n lastInputMethodRef.current =\n pct === 20\n ? \"percent_20\"\n : pct === 25\n ? \"percent_25\"\n : pct === 50\n ? \"percent_50\"\n : pct === 75\n ? \"percent_75\"\n : \"percent_max\";\n\n if (pct !== 100) {\n const nextAmount = getExactOutPercentAmountFromBalance(\n toToken,\n pct,\n false\n );\n\n if (nextAmount) {\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleSendAmountChange(nextAmount.amount);\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setTxError(\"Unable to calculate this percentage for the send asset.\");\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n const maxAmount = getExactOutPercentAmountFromBalance(toToken, 100, false);\n if (runId !== maxPercentRunRef.current) return;\n if (!maxAmount) {\n setTxError(\"No transferable amount is available for this asset.\");\n return;\n }\n\n handleSendAmountChange(maxAmount.amount);\n };\n\n // ---------------------------------------------------------------------------\n // Render\n // ---------------------------------------------------------------------------\n const insufficientSourceIssue =\n swapQuoteIssue?.type === \"insufficientSources\" ? swapQuoteIssue : null;\n const configuredAmountIssue = buildConfiguredAmountIssue();\n const blockingQuoteIssue =\n insufficientSourceIssue ?? receiveAmountIssue ?? configuredAmountIssue;\n const isExactOutPaymentFlow =\n activeMode === \"deposit\" || activeMode === \"send\";\n const hasCurrentRunnableIntent = hasCurrentQuoteIntent;\n const hasIntentSources = Boolean((intentData?.sources ?? []).length > 0);\n const hasCurrentExactOutPaymentIntent =\n hasCurrentRunnableIntent &&\n (hasIntentSources ||\n (isExactOutPaymentFlow && Boolean(intentData?.destination)));\n const isExactOutRouteLoading =\n isExactOutPaymentFlow &&\n swapStep === \"idle\" &&\n swapType === \"exactOut\" &&\n Boolean(\n toToken && (receiveMaxCalculating || (amount && Number(amount) > 0))\n ) &&\n !blockingQuoteIssue &&\n !hasCurrentExactOutPaymentIntent &&\n (quoteRefreshing || intentLoading || receiveMaxCalculating);\n const isQuoteUnavailableForAutoSourceFlow =\n isExactOutPaymentFlow &&\n Boolean(hasPositiveDecimalInput(amount) && toToken) &&\n !quoteRefreshing &&\n !receiveMaxCalculating &&\n !intentLoading &&\n !blockingQuoteIssue &&\n !hasCurrentExactOutPaymentIntent;\n const hasPositiveRootAmount = hasPositiveDecimalInput(amount);\n const hasReadySwapQuoteInput = hasReadyExactInSwapInput(fromTokens, toToken);\n const needsWalletConnection = !ownerAddress || !nexusSDK;\n const isExactOutPaymentQuotePending =\n isExactOutPaymentFlow && (quoteRefreshing || intentLoading);\n const walletConnectBusy =\n walletActionPending ||\n nexusLoading ||\n isWalletConnectPending ||\n walletStatus === \"connecting\";\n const hasConnectWalletHandler = Boolean(\n onConnectClick || onConnectWallet || connectors.length > 0\n );\n const walletCtaLabel = hasConnectWalletHandler\n ? walletConnectBusy\n ? \"Connecting...\"\n : \"Connect Wallet\"\n : \"Connect your wallet to proceed\";\n const isSwapCtaDisabled = needsWalletConnection\n ? !hasConnectWalletHandler || walletConnectBusy\n : !hasReadySwapQuoteInput ||\n receiveMaxCalculating ||\n quoteRefreshing ||\n Boolean(blockingQuoteIssue);\n const isDepositCtaDisabled = needsWalletConnection\n ? !hasConnectWalletHandler || walletConnectBusy\n : !hasPositiveRootAmount ||\n !toToken ||\n receiveMaxCalculating ||\n isExactOutPaymentQuotePending ||\n (!hasCurrentExactOutPaymentIntent &&\n isQuoteUnavailableForAutoSourceFlow) ||\n Boolean(blockingQuoteIssue);\n const sendNeedsRecipient = activeMode === \"send\" && !recipientAddress;\n const isSendCtaDisabled = needsWalletConnection\n ? !hasConnectWalletHandler || walletConnectBusy\n : !hasPositiveRootAmount ||\n !toToken ||\n hasSameOwnerSendRecipient ||\n receiveMaxCalculating ||\n (!sendNeedsRecipient &&\n (isExactOutPaymentQuotePending ||\n (!hasCurrentExactOutPaymentIntent &&\n isQuoteUnavailableForAutoSourceFlow))) ||\n Boolean(blockingQuoteIssue);\n const quoteCtaLabel = (fallback: string) => {\n if (needsWalletConnection) return walletCtaLabel;\n if (insufficientSourceIssue) return \"Insufficient balance\";\n if (receiveAmountIssue) return receiveAmountIssue.ctaLabel;\n if (configuredAmountIssue) return configuredAmountIssue.ctaLabel;\n if (receiveMaxCalculating) return \"Calculating...\";\n if (\n isExactOutPaymentQuotePending ||\n (!hasCurrentExactOutPaymentIntent && (quoteRefreshing || intentLoading))\n ) {\n return \"Fetching quotes...\";\n }\n if (isQuoteUnavailableForAutoSourceFlow) return \"Quote unavailable\";\n if (!hasPositiveRootAmount) return \"Enter amount\";\n return fallback;\n };\n const sendCtaLabel = (() => {\n if (needsWalletConnection) return walletCtaLabel;\n if (insufficientSourceIssue) return \"Insufficient balance\";\n if (receiveAmountIssue) return receiveAmountIssue.ctaLabel;\n if (configuredAmountIssue) return configuredAmountIssue.ctaLabel;\n if (!hasPositiveRootAmount) return \"Enter amount\";\n if (!toToken) return \"Select token\";\n if (hasSameOwnerSendRecipient) return \"Change recipient\";\n if (sendNeedsRecipient) return \"Add recipient\";\n return quoteCtaLabel(\"Review send\");\n })();\n const previewIntentSourceUsdNumber = (intentData?.sources ?? []).reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0)\n );\n const previewSourceUsdNumber = previewIntentSourceUsdNumber.gt(0)\n ? previewIntentSourceUsdNumber\n : fromTokens.length > 0\n ? fromTokens.reduce(\n (sum, token) =>\n sum.plus(\n getTokenUsdValue(\n token,\n swapType === \"exactIn\" && fromTokens.length === 1\n ? amount\n : undefined\n )\n ),\n new Decimal(0)\n )\n : undefined;\n const previewExactOutDestinationAmount =\n activeMode === \"deposit\"\n ? depositTokenAmountForQuote\n : activeMode === \"send\"\n ? parseFiatNumber(amount)\n : undefined;\n const previewExactOutDestinationUsdNumber =\n activeMode === \"deposit\"\n ? depositUsdDecimal\n : activeMode === \"send\" && amount && toToken\n ? getTokenUsdValue(\n {\n ...toToken,\n userAmount: amount,\n userAmountMode: \"token\",\n },\n amount\n )\n : undefined;\n const previewDestinationUsdNumber =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n previewExactOutDestinationUsdNumber?.gt(0)\n ? previewExactOutDestinationUsdNumber\n : parseFiatNumber((intentData?.destination as any)?.value);\n const previewDestinationAmount =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n previewExactOutDestinationAmount?.gt(0)\n ? previewExactOutDestinationAmount\n .toDecimalPlaces(toToken?.decimals ?? 18, Decimal.ROUND_DOWN)\n .toFixed()\n : intentToAmount;\n const previewFromAmountUsd =\n previewSourceUsdNumber && previewSourceUsdNumber.gt(0)\n ? previewSourceUsdNumber.toDecimalPlaces(6).toFixed()\n : undefined;\n const previewToAmountUsd =\n previewDestinationUsdNumber && previewDestinationUsdNumber.gt(0)\n ? previewDestinationUsdNumber.toDecimalPlaces(6).toFixed()\n : undefined;\n const previewDestinationGasFeeUsd = (() => {\n const value = getIntentDestinationGasUsdValue(intentData);\n return value && value.gt(0)\n ? value.toDecimalPlaces(6, Decimal.ROUND_DOWN).toFixed()\n : undefined;\n })();\n const predictiveExactInQuote =\n predictiveQuote?.mode === \"exactIn\" &&\n predictiveQuote.key === getPredictiveQuoteCacheKey(\"swap\", \"exactIn\")\n ? predictiveQuote\n : null;\n const predictiveExactOutQuote =\n predictiveQuote?.mode === \"exactOut\" &&\n predictiveQuote.key === getPredictiveQuoteCacheKey(activeMode, \"exactOut\")\n ? predictiveQuote\n : null;\n const shouldUseCurrentExactOutIntentSources =\n isExactOutPaymentFlow && hasCurrentQuoteIntent && hasIntentSources;\n const currentExactOutIntentSourceTokens =\n shouldUseCurrentExactOutIntentSources\n ? sortSwapTokensByUsdDesc(\n (intentData?.sources ?? [])\n .map(buildIntentSourceToken)\n .filter(hasPositiveSourceSpend)\n )\n : [];\n const resolvedToToken =\n toToken ??\n (activeMode === \"deposit\" && selectedOpportunity\n ? toTokenFromOpportunity(selectedOpportunity)\n : undefined);\n const enrichedResolvedToToken = enrichTokenOptionLogo(resolvedToToken);\n const toTokenWithFetchedBalance =\n enrichedResolvedToToken && destinationBalance\n ? { ...enrichedResolvedToToken, balance: destinationBalance }\n : enrichedResolvedToToken;\n const idleReceiveQuoteAmount =\n activeMode === \"swap\" && swapType === \"exactIn\"\n ? (intentToAmount ?? predictiveExactInQuote?.toAmount)\n : undefined;\n const idleReceiveQuoteUsd =\n activeMode === \"swap\" && swapType === \"exactIn\"\n ? (previewToAmountUsd ?? predictiveExactInQuote?.toUsd)\n : previewToAmountUsd;\n const exactOutDestinationCoverage = getExactOutDestinationBalanceCoverage({\n requestedAmount: previewExactOutDestinationAmount,\n requestedUsd: previewExactOutDestinationUsdNumber,\n producedAmount: hasIntentSources\n ? parseFiatNumber(intentData?.destination?.amount)\n : undefined,\n producedUsd: hasIntentSources\n ? parseFiatNumber(intentData?.destination?.value)\n : undefined,\n token: toTokenWithFetchedBalance,\n });\n const destinationBalanceDisplayToken = buildDestinationBalanceDisplayToken(\n exactOutDestinationCoverage,\n toTokenWithFetchedBalance\n );\n const shouldShowPredictiveExactOutDisplay =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (quoteRefreshing || intentLoading) &&\n !hasIntentSources &&\n Boolean(\n predictiveExactOutQuote &&\n ((predictiveExactOutQuote.sources?.length ?? 0) > 0 ||\n destinationBalanceDisplayToken)\n );\n const baseDisplayFromTokens = shouldUseCurrentExactOutIntentSources\n ? currentExactOutIntentSourceTokens\n : shouldShowPredictiveExactOutDisplay\n ? (predictiveExactOutQuote?.sources ?? fromTokens)\n : fromTokens;\n const displayFromTokens = (() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n return baseDisplayFromTokens;\n }\n if (!destinationBalanceDisplayToken) {\n return mergeDisplaySourceTokens(\n sortDisplaySourcesByBalanceUsdDesc(baseDisplayFromTokens)\n );\n }\n\n const destinationKey = getTokenSelectionKey(destinationBalanceDisplayToken);\n let replacedEmptyDestinationToken = false;\n const tokens = baseDisplayFromTokens.map((token) => {\n const isDestinationToken = getTokenSelectionKey(token) === destinationKey;\n if (\n isDestinationToken &&\n !hasPositiveDecimalInput(token.userAmount) &&\n !hasPositiveDecimalInput(token.userAmountUsd)\n ) {\n replacedEmptyDestinationToken = true;\n return destinationBalanceDisplayToken;\n }\n return token;\n });\n\n const displayTokens = replacedEmptyDestinationToken\n ? tokens\n : [...tokens, destinationBalanceDisplayToken];\n return mergeDisplaySourceTokens(\n sortDisplaySourcesByBalanceUsdDesc(displayTokens)\n );\n })();\n const displayExactOutRouteLoading =\n isExactOutRouteLoading && !shouldShowPredictiveExactOutDisplay;\n const isSourcePickerRefreshDisabled =\n quoteRefreshing || intentLoading || previewQuoteRefreshing;\n const isIntentSourceUiActionable =\n activeMode === \"deposit\"\n ? !isDepositCtaDisabled\n : activeMode === \"send\"\n ? !isSendCtaDisabled\n : !isSwapCtaDisabled;\n\n useEffect(() => {\n if (!intentFetchTiming?.endTimeMs) {\n return;\n }\n if (latestIntentFetchRunIdRef.current !== intentFetchTiming.runId) {\n return;\n }\n if (intentUiRenderLoggedRunIdRef.current === intentFetchTiming.runId) {\n return;\n }\n const hasRenderedSources =\n displayFromTokens.length > 0 ||\n hasIntentSources ||\n Boolean(intentData?.destination);\n if (!hasRenderedSources) return;\n if (\n isSwapBalancePending ||\n isSourcePickerRefreshDisabled ||\n quoteRefreshing ||\n intentLoading ||\n previewQuoteRefreshing ||\n receiveMaxCalculating ||\n displayExactOutRouteLoading ||\n !isIntentSourceUiActionable\n ) {\n return;\n }\n\n const renderTimeMs = Date.now();\n const renderDurationMs = renderTimeMs - intentFetchTiming.startTimeMs;\n const fetchDurationMs =\n intentFetchTiming.fetchDurationMs ??\n intentFetchTiming.endTimeMs - intentFetchTiming.startTimeMs;\n const differenceMs = renderDurationMs - fetchDurationMs;\n\n console.log(\"[NexusWidget] intent sources UI render complete\", {\n runId: intentFetchTiming.runId,\n walletAddress: intentFetchTiming.walletAddress,\n operation: intentFetchTiming.operation,\n quoteInputKey: intentFetchTiming.quoteInputKey,\n sourceCount: displayFromTokens.length,\n dateNow: renderTimeMs,\n date: new Date(renderTimeMs),\n totalRenderTimeSeconds: renderDurationMs / 1000,\n elapsedMs: renderDurationMs,\n });\n console.log(\"[NexusWidget] intent render/fetch difference\", {\n runId: intentFetchTiming.runId,\n walletAddress: intentFetchTiming.walletAddress,\n operation: intentFetchTiming.operation,\n quoteInputKey: intentFetchTiming.quoteInputKey,\n differenceMs,\n dateNow: renderTimeMs,\n date: new Date(renderTimeMs),\n });\n intentUiRenderLoggedRunIdRef.current = intentFetchTiming.runId;\n }, [\n displayExactOutRouteLoading,\n displayFromTokens.length,\n hasIntentSources,\n intentLoading,\n intentData?.destination,\n intentFetchTiming,\n isDepositCtaDisabled,\n isIntentSourceUiActionable,\n isSendCtaDisabled,\n isSourcePickerRefreshDisabled,\n isSwapCtaDisabled,\n isSwapBalancePending,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n ]);\n\n const totalSwapBalanceUsd = getActiveTotalBalanceUsd()\n .toDecimalPlaces(2)\n .toFixed();\n const requestedOutputUsd =\n amount && toToken\n ? getTokenUsdValue(\n {\n ...toToken,\n userAmount: amount,\n userAmountMode: \"token\",\n },\n amount\n )\n : new Decimal(0);\n const sendAmountUsd = requestedOutputUsd.toNumber();\n const exactOutRequiredUsdAmount = (() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return undefined;\n\n const issueMissingUsd = parseFiatNumber(insufficientSourceIssue?.missingUsd);\n if (issueMissingUsd && issueMissingUsd.gt(0)) {\n return getExactOutAvailableSourceUsd().plus(issueMissingUsd);\n }\n\n const requiredFundingUsd = getExactOutRequiredFundingUsd();\n if (requiredFundingUsd?.gt(0)) return requiredFundingUsd;\n\n if (requestedOutputUsd.gt(0)) return requestedOutputUsd;\n\n return undefined;\n })();\n const exactOutRequiredUsdDisplay = exactOutRequiredUsdAmount\n ?.toDecimalPlaces(2)\n .toFixed();\n const assetSelectorRequiredUsdDisplay =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (swapStep === \"choose-swap-asset\" ||\n closingDrawerStep === \"choose-swap-asset\")\n ? (sourceSelectionRequiredUsdDisplay ?? exactOutRequiredUsdDisplay)\n : exactOutRequiredUsdDisplay;\n const captureSourceSelectionRequiredUsd = () => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n setSourceSelectionRequiredUsdDisplay(exactOutRequiredUsdDisplay);\n };\n const isIdleSwapQuoteLoading =\n activeMode === \"swap\" &&\n swapStep === \"idle\" &&\n (quoteRefreshing || intentLoading);\n const isReceiveAmountLoading =\n receiveMaxCalculating ||\n (isIdleSwapQuoteLoading &&\n swapType === \"exactIn\" &&\n !idleReceiveQuoteAmount);\n const isReceiveUsdLoading =\n receiveMaxCalculating ||\n (isIdleSwapQuoteLoading && swapType === \"exactIn\" && !idleReceiveQuoteUsd);\n const hasQuoteRefreshCountdown =\n (activeMode === \"swap\" ||\n activeMode === \"deposit\" ||\n activeMode === \"send\") &&\n hasCurrentQuoteIntent &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n const isRecipientDrawerClosing = closingDrawerStep === \"enter-recipient\";\n const isSwapAssetDrawerClosing = closingDrawerStep === \"choose-swap-asset\";\n const isReceiveAssetDrawerClosing =\n closingDrawerStep === \"choose-receive-asset\";\n const isReceiveAssetPickerLocked =\n activeMode === \"deposit\"\n ? configuredDepositOptions.length <= 1\n : (config.allowedDestinationPairs?.length ?? 0) === 1;\n const hideDestinationTokenDropdownIcon =\n activeMode === \"deposit\"\n ? configuredDepositOptions.length === 1\n : (config.allowedDestinationPairs?.length ?? 0) === 1;\n const isTokenAssetDrawerActive =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n closingDrawerStep === \"choose-swap-asset\" ||\n closingDrawerStep === \"choose-receive-asset\";\n const configuredReceiveTokenCount =\n activeMode === \"deposit\" || activeMode === \"send\"\n ? Math.max(\n configuredDestinationTokenOptions.length,\n config.allowedDestinationPairs?.length ?? 0\n )\n : 0;\n const isConfiguredReceiveAssetDrawerActive =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (swapStep === \"choose-receive-asset\" ||\n closingDrawerStep === \"choose-receive-asset\") &&\n Boolean(config.allowedDestinationPairs?.length);\n const tokenAssetDrawerMinRootContentHeight =\n isConfiguredReceiveAssetDrawerActive\n ? getConfiguredReceiveSelectorRootHeight(configuredReceiveTokenCount)\n : isTokenAssetDrawerActive\n ? TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT\n : 0;\n const isDrawerOverlayActive =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n swapStep === \"enter-recipient\" ||\n closingDrawerStep !== null;\n const isPreviewIntentScreen = swapStep === \"preview-intent\";\n const displayedRootContentHeight =\n hasMeasuredRootContent && rootContentHeight\n ? Math.max(\n rootContentHeight,\n tokenAssetDrawerMinRootContentHeight\n )\n : null;\n\n const widgetContent = (\n {\n const transitions = [];\n if (hasMeasuredRootContent && shouldAnimateRootHeight) {\n transitions.push(`height ${ROOT_HEIGHT_TRANSITION_MS}ms ease-out`);\n }\n return transitions.join(\", \");\n })(),\n willChange: \"height\",\n width: \"450px\",\n maxWidth: \"100%\",\n minWidth: \"320px\",\n WebkitFontSmoothing: \"antialiased\",\n MozOsxFontSmoothing: \"grayscale\",\n }}\n >\n \n \n
\n {canGoBack && (\n \n \n \n )}\n \n {getTitle()}\n
\n\n {/* Sub-screen asset counts */}\n {!isTitleCentered() &&\n activeMode === \"swap\" &&\n swapStep === \"choose-swap-asset\" &&\n swapType === \"exactIn\" && (\n \n {fromTokens.length} asset(s) selected\n \n )}\n \n\n {/* Right side icons */}\n \n {hasQuoteRefreshCountdown && (\n \n )}\n setSwapStep(\"history\")}\n style={{\n alignItems: \"center\",\n backgroundColor: theme.primitives.iconButton.backgroundColor,\n borderColor: theme.primitives.iconButton.borderColor,\n borderRadius: theme.radius.iconButton,\n borderStyle: \"solid\",\n borderWidth: \"1px\",\n boxShadow: theme.primitives.iconButton.boxShadow,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n height: \"28px\",\n justifyContent: \"center\",\n width: \"28px\",\n cursor: \"pointer\",\n padding: 0,\n }}\n >\n \n \n \n \n \n \n {showCloseButton && (\n \n \n \n \n \n )}\n \n \n\n {/* ------------------------------------------------------------------ */}\n {/* Main content area */}\n {/* ------------------------------------------------------------------ */}\n \n {/* =============================================================== */}\n {/* SHARED SUB-SCREENS (non-drawer panels) */}\n {/* =============================================================== */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep !== \"idle\" &&\n swapStep !== \"choose-swap-asset\" &&\n swapStep !== \"choose-receive-asset\" &&\n swapStep !== \"enter-recipient\" && (\n <>\n {/* Panel: preview. */}\n {swapStep === \"preview-intent\" && (\n \n {\n clearPendingSwapIntent();\n setSwapStep(\"idle\");\n }}\n onTransitionChange={setIsPreviewTransitioning}\n opportunity={selectedOpportunity}\n recipientAddress={transferRecipientAddress}\n steps={steps}\n supportedTokenAssets={supportedChainsAndTokens}\n swapBalances={swapBalance}\n swapType={swapType}\n toAmount={previewDestinationAmount}\n toAmountTokens={\n previewDestinationAmount\n ? `${previewDestinationAmount}`\n : undefined\n }\n toAmountUsd={previewToAmountUsd}\n toToken={toTokenWithFetchedBalance}\n totalFeeUsd={intentFeeUsd}\n />\n \n )}\n\n {swapStep === \"progress\" && (\n \n )}\n\n {(swapStep === \"success\" || swapStep === \"failed\") &&\n currentSwapEntry && (\n
\n \n
\n )}\n \n )}\n\n {/* =============================================================== */}\n {/* HISTORY SCREEN */}\n {/* =============================================================== */}\n {swapStep === \"history\" && (\n \n )}\n\n {/* =============================================================== */}\n {/* SWAP IDLE SCREEN */}\n {/* =============================================================== */}\n {activeMode === \"swap\" &&\n [\n \"idle\",\n \"choose-swap-asset\",\n \"choose-receive-asset\",\n \"enter-recipient\",\n ].includes(swapStep) && (\n <>\n {\n handleSwapAmountChange(val, panel);\n }}\n onOpenDestPicker={() =>\n openDrawerStep(\"choose-receive-asset\")\n }\n onOpenRecipientPicker={handleOpenRecipientEditor}\n onOpenSourcePicker={(index) => {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n setEditingAssetIndex(index ?? null);\n beginSourcePickerEdit();\n openDrawerStep(\"choose-swap-asset\");\n }}\n onUpdateTokens={handleSwapTokensUpdate}\n receiveQuoteAmount={\n swapType === \"exactIn\" ? idleReceiveQuoteAmount : undefined\n }\n receiveQuoteUsd={idleReceiveQuoteUsd}\n recipientAddress={effectiveRecipientAddress}\n sourceRouteMessage={insufficientSourceIssue?.message}\n sourceRouteStatus={\n insufficientSourceIssue\n ? \"insufficient\"\n : isExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n swapType={swapType}\n toToken={toTokenWithFetchedBalance}\n totalBalance={totalSwapBalanceUsd}\n usdValue={amount && usdValue > 0 ? usdValue.toFixed(2) : \"\"}\n />\n\n {receiveAmountIssue && (\n \n )}\n\n {configuredAmountIssue && (\n \n )}\n\n {txError && !blockingQuoteIssue && (\n \n )}\n\n {/* CTA Button */}\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n void handleEnterPreview();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: blockingQuoteIssue\n ? \"#FCEEED\"\n : isSwapCtaDisabled\n ? theme.colors.surfaceCool\n : primaryButtonBackground,\n border: blockingQuoteIssue ? \"1px solid #F7C4C1\" : \"none\",\n borderRadius: theme.radius.primaryButton,\n boxShadow:\n blockingQuoteIssue || isSwapCtaDisabled\n ? \"none\"\n : theme.shadows.primaryButton,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"7px\",\n height: \"42px\",\n justifyContent: \"center\",\n marginTop: \"8px\",\n paddingInline: \"16px\",\n cursor: isSwapCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {blockingQuoteIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n quoteRefreshing ||\n receiveMaxCalculating ? (\n\t \n ) : null}\n \n {needsWalletConnection\n ? walletCtaLabel\n : fromTokens.length === 0\n ? \"Add Assets to Bridge\"\n : quoteCtaLabel(\"Review swap\")}\n \n \n \n \n )}\n\n {/* =============================================================== */}\n {/* DEPOSIT MODE LAYOUT */}\n {/* =============================================================== */}\n {activeMode === \"deposit\" &&\n [\n \"idle\",\n \"choose-swap-asset\",\n \"choose-receive-asset\",\n \"enter-recipient\",\n ].includes(swapStep) && (\n <>\n {selectedOpportunity && (\n <>\n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n captureSourceSelectionRequiredUsd();\n beginSourcePickerEdit();\n openDrawerStep(\"choose-swap-asset\");\n }}\n onOpenTokenPicker={() =>\n openDrawerStep(\"choose-receive-asset\")\n }\n onSetPercent={handleDepositPercentSelect}\n routeMessage={insufficientSourceIssue?.message}\n routeStatus={\n insufficientSourceIssue\n ? \"insufficient\"\n : displayExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n showAutoBadge={!sourceSelectionTouched}\n isTokenPickerDisabled={isReceiveAssetPickerLocked}\n hideDestinationTokenDropdownIcon={\n hideDestinationTokenDropdownIcon\n }\n tokenValue={depositTokenDisplay}\n toToken={toTokenWithFetchedBalance}\n totalBalance={totalSwapBalanceUsd}\n usdValue={depositUsdDisplay}\n />\n\n {receiveAmountIssue && (\n \n )}\n\n {configuredAmountIssue && (\n \n )}\n\n {txError && !blockingQuoteIssue && (\n \n )}\n\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n void handleEnterPreview();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: blockingQuoteIssue\n ? \"#FCEEED\"\n : isDepositCtaDisabled\n ? theme.colors.surfaceCool\n : primaryButtonBackground,\n border: blockingQuoteIssue\n ? \"1px solid #F7C4C1\"\n : \"none\",\n borderRadius: blockingQuoteIssue\n ? \"4px\"\n : theme.radius.primaryButton,\n boxShadow:\n blockingQuoteIssue || isDepositCtaDisabled\n ? \"none\"\n : theme.shadows.primaryButton,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"7px\",\n height: \"40px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n cursor: isDepositCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {blockingQuoteIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n isExactOutPaymentQuotePending ||\n receiveMaxCalculating ? (\n\t \n ) : null}\n \n {quoteCtaLabel(\"Review deposit\")}\n \n \n \n \n )}\n \n )}\n\n {/* =============================================================== */}\n {/* SEND MODE — recipient first, then amount, then asset */}\n {/* =============================================================== */}\n {activeMode === \"send\" &&\n [\n \"idle\",\n \"choose-swap-asset\",\n \"choose-receive-asset\",\n \"enter-recipient\",\n ].includes(swapStep) && (\n <>\n \n openDrawerStep(\"choose-receive-asset\")\n }\n isRecipientLocked={isConfiguredRecipientLocked}\n onOpenRecipientPicker={\n isConfiguredRecipientLocked\n ? () => undefined\n : handleOpenRecipientEditor\n }\n onOpenSourcePicker={() => {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n setEditingAssetIndex(null);\n captureSourceSelectionRequiredUsd();\n beginSourcePickerEdit();\n openDrawerStep(\"choose-swap-asset\");\n }}\n onSetPercent={handleSendPercentSelect}\n recipientAddress={recipientAddress || \"\"}\n routeMessage={insufficientSourceIssue?.message}\n routeStatus={\n insufficientSourceIssue\n ? \"insufficient\"\n : displayExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n showAutoBadge={!sourceSelectionTouched}\n toToken={toTokenWithFetchedBalance}\n totalBalance={totalSwapBalanceUsd}\n usdValue={\n amount && sendAmountUsd > 0 ? sendAmountUsd.toFixed(2) : \"\"\n }\n />\n\n {receiveAmountIssue && (\n \n )}\n\n {configuredAmountIssue && (\n \n )}\n\n {txError && !blockingQuoteIssue && (\n \n )}\n\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n if (sendNeedsRecipient) {\n handleOpenRecipientEditor();\n return;\n }\n void handleEnterPreview();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: blockingQuoteIssue\n ? \"#FCEEED\"\n : isSendCtaDisabled\n ? theme.colors.surfaceCool\n : primaryButtonBackground,\n border: blockingQuoteIssue ? \"1px solid #F7C4C1\" : \"none\",\n borderRadius: blockingQuoteIssue\n ? \"4px\"\n : theme.radius.primaryButton,\n boxShadow:\n blockingQuoteIssue || isSendCtaDisabled\n ? \"none\"\n : theme.shadows.primaryButton,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"7px\",\n height: \"40px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n cursor: isSendCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {blockingQuoteIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n (!sendNeedsRecipient &&\n (isExactOutPaymentQuotePending ||\n receiveMaxCalculating)) ? (\n\t \n ) : null}\n \n {sendCtaLabel}\n \n \n \n \n )}\n \n \n\n {/* ================================================================== */}\n {/* DRAWER PANELS — rendered as direct children of root widget */}\n {/* so they overlay the main page as bottom drawers */}\n {/* ================================================================== */}\n\n {/* Drawer: enter-recipient */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep === \"enter-recipient\" && (\n \n {\n setTxError(null);\n closeDrawerToIdle();\n }}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n backgroundColor: \"rgba(0,0,0,0.35)\",\n pointerEvents: \"auto\",\n opacity: isRecipientDrawerClosing ? 0 : 1,\n transition: `opacity ${DRAWER_CLOSE_MS}ms ease`,\n }}\n />\n \n \n \n \n \n {\n setTxError(null);\n closeDrawerToIdle();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: theme.colors.surface,\n border: `1px solid ${theme.colors.border}`,\n borderRadius: \"8px\",\n cursor: \"pointer\",\n display: \"flex\",\n flexShrink: 0,\n height: \"32px\",\n justifyContent: \"center\",\n padding: 0,\n width: \"32px\",\n }}\n type=\"button\"\n >\n \n \n \n Recipient\n \n \n \n \n \n Wallet Address\n \n {activeMode === \"swap\" && defaultRecipientAddress && (\n \n Reset to default\n \n )}\n \n {\n setRecipientAddress(next);\n if (txError) setTxError(null);\n }}\n onClear={() => setRecipientAddress(\"\")}\n placeholder=\"Wallet address\"\n value={recipientAddress}\n />\n {txError && (\n \n {txError}\n \n )}\n {activeMode === \"send\" && (\n \n Recipient must be different from the connected wallet.\n \n )}\n \n Save\n \n \n \n )}\n\n {/* Drawer: choose-swap-asset */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep === \"choose-swap-asset\" && (\n \n \n \n {\n if (activeMode === \"swap\") {\n const next = [...fromTokens];\n const targetIndex =\n editingAssetIndex !== null &&\n editingAssetIndex < next.length\n ? editingAssetIndex\n : null;\n const existingToken =\n targetIndex !== null ? next[targetIndex] : undefined;\n const tokenChanged = !isSameTokenSelection(\n existingToken,\n token\n );\n const preservedAmount = tokenChanged\n ? \"\"\n : existingToken?.userAmount ||\n (targetIndex === 0 ? amount : \"\");\n const newToken = {\n ...token,\n userAmount: preservedAmount,\n };\n\n if (targetIndex !== null) {\n next[targetIndex] = newToken;\n } else {\n next.push(newToken);\n }\n\n if (tokenChanged) {\n clearPendingSwapIntent();\n setAmount(getSourceAmountInput(next));\n }\n if (swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n setFromTokens(next);\n closeDrawerToIdle();\n } else if (\n activeMode === \"deposit\" ||\n activeMode === \"send\"\n ) {\n handleSourcePickerDraftSelectionChange([\n { ...token, userAmount: \"\" },\n ]);\n }\n }}\n onSelectionChange={\n activeMode === \"deposit\" || activeMode === \"send\"\n ? handleSourcePickerDraftSelectionChange\n : undefined\n }\n onToggle={(token) => {\n const isDepositOrSendSourcePicker =\n activeMode === \"deposit\" || activeMode === \"send\";\n const prevTokens = isDepositOrSendSourcePicker\n ? sourcePickerSelectedTokens\n : fromTokens;\n const isSameSelection = (\n a: SwapTokenOption,\n b: SwapTokenOption\n ) => {\n if (a.isUnified || b.isUnified) {\n return Boolean(\n a.isUnified &&\n b.isUnified &&\n a.unifiedSymbol === b.unifiedSymbol\n );\n }\n return (\n a.contractAddress.toLowerCase() ===\n b.contractAddress.toLowerCase() && a.chainId === b.chainId\n );\n };\n const sourceTokens = token.sourceTokens ?? [];\n const isSameUnifiedGroup = (item: SwapTokenOption) =>\n Boolean(\n item.isUnified &&\n token.isUnified &&\n item.unifiedSymbol === token.unifiedSymbol\n );\n const withDefaultAmount = (item: SwapTokenOption) => ({\n ...item,\n userAmount:\n activeMode === \"swap\" && prevTokens.length === 0\n ? amount\n : \"\",\n });\n const getNextTokens = () => {\n if (\n isDepositOrSendSourcePicker &&\n token.isUnified &&\n sourceTokens.length > 0\n ) {\n const hasUnifiedSelection =\n prevTokens.some(isSameUnifiedGroup);\n const areAllChildrenSelected = sourceTokens.every(\n (source) =>\n prevTokens.some((item) =>\n isSameSelection(item, source)\n )\n );\n const withoutGroup = prevTokens.filter(\n (item) =>\n !isSameUnifiedGroup(item) &&\n !sourceTokens.some((source) =>\n isSameSelection(item, source)\n )\n );\n\n if (hasUnifiedSelection || areAllChildrenSelected) {\n return withoutGroup;\n }\n\n return [\n ...withoutGroup,\n ...sourceTokens.map((source) =>\n withDefaultAmount(source)\n ),\n ];\n }\n\n if (isDepositOrSendSourcePicker && !token.isUnified) {\n const unifiedSelection = prevTokens.find(\n (item) =>\n item.isUnified &&\n item.sourceTokens?.some((source) =>\n isSameSelection(source, token)\n )\n );\n\n if (unifiedSelection?.sourceTokens?.length) {\n const withoutUnified = prevTokens.filter(\n (item) => !isSameSelection(item, unifiedSelection)\n );\n return [\n ...withoutUnified,\n ...unifiedSelection.sourceTokens\n .filter((source) => !isSameSelection(source, token))\n .map((source) => withDefaultAmount(source)),\n ];\n }\n }\n\n const exists = prevTokens.find((item) =>\n isSameSelection(item, token)\n );\n if (exists) {\n return prevTokens.filter(\n (item) => !isSameSelection(item, token)\n );\n }\n const tokenSourceKeys = new Set(\n (token.sourceTokens ?? []).map(\n (source) =>\n `${source.chainId}-${source.contractAddress.toLowerCase()}`\n )\n );\n const next = prevTokens.filter((existing) => {\n if (\n token.isUnified &&\n tokenSourceKeys.has(\n `${existing.chainId}-${existing.contractAddress.toLowerCase()}`\n )\n ) {\n return false;\n }\n if (\n existing.isUnified &&\n existing.sourceTokens?.some(\n (source) =>\n source.chainId === token.chainId &&\n source.contractAddress.toLowerCase() ===\n token.contractAddress.toLowerCase()\n )\n ) {\n return false;\n }\n return true;\n });\n return [...next, withDefaultAmount(token)];\n };\n const nextTokens = getNextTokens();\n\n if (isDepositOrSendSourcePicker) {\n handleSourcePickerDraftSelectionChange(nextTokens);\n } else {\n clearPendingSwapIntent();\n setFromTokens(nextTokens);\n }\n }}\n preserveSelectedBelowMinimum={false}\n requiredUsd={assetSelectorRequiredUsdDisplay}\n selectedTokens={sourcePickerSelectedTokens}\n showBelowMinimumInline={\n activeMode === \"swap\" && swapType === \"exactIn\"\n }\n swapBalance={swapBalance}\n swapSupportedChains={swapSupportedChainsAndTokens}\n title={\n activeMode === \"deposit\" || activeMode === \"send\"\n ? \"Choose Assets to Pay with\"\n : \"Choose assets to send\"\n }\n />\n \n \n )}\n\n {/* Drawer: choose-receive-asset */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep === \"choose-receive-asset\" && (\n \n \n \n {\n const tokenChanged = !isSameTokenSelection(toToken, token);\n if (tokenChanged) {\n onReceiveAssetChange?.({\n chainId: token.chainId,\n chainName: token.chainName,\n contractAddress: token.contractAddress,\n symbol: token.symbol,\n });\n }\n if (activeMode === \"send\" || activeMode === \"deposit\") {\n setExactOutQuoteSourceModeValue(\"all\");\n if (tokenChanged) {\n clearPendingSwapIntent();\n if (!isConfiguredAmountFixed) {\n setAmount(\"\");\n }\n }\n setSwapType(\"exactOut\");\n if (activeMode === \"deposit\") {\n const nextDeposit = getDepositForTokenSelection(\n configuredDepositOptions,\n token\n );\n if (nextDeposit) {\n setSelectedOpportunity(nextDeposit);\n }\n }\n setToToken(token);\n closeDrawerToIdle();\n return;\n }\n if (tokenChanged) {\n clearPendingSwapIntent();\n }\n if (swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n setToToken(token);\n closeDrawerToIdle();\n }}\n />\n \n \n )}\n \n );\n\n if (embed) return widgetContent;\n\n return (\n \n \n \n \n \n {widgetContent}\n \n \n );\n}\n\nexport default NexusWidget;\n", "type": "registry:component", "target": "components/nexus/nexus.tsx" }, @@ -110,7 +110,7 @@ }, { "path": "registry/avail-widgets/nexus-widget/theme.ts", - "content": "const ink = {\n 900: \"#161615\",\n 800: \"#1F1F1F\",\n 600: \"#5B5B5A\",\n 500: \"#848483\",\n 400: \"#8E8E89\",\n 300: \"#C9C9C5\",\n} as const;\n\nexport const NEXUS_WIDGET_DEFAULT_PRIMARY_COLOR = \"rgb(0, 107, 244)\" as const;\n\nconst blue = {\n 700: \"var(--foreground-brand)\",\n 500: \"var(--foreground-brand)\",\n 100: \"#EAF1FF\",\n 50: \"#E5EEFF\",\n tab: \"#F0F3F9\",\n} as const;\n\nconst surface = {\n default: \"#FFFFFE\",\n soft: \"#FAFAFC\",\n cool: \"#F6F6F6\",\n border: \"#ECECEA\",\n border2: \"#E8E8E7\",\n} as const;\n\nconst status = {\n success: \"#1BC57A\",\n successBg: \"rgba(27, 197, 122, 0.18)\",\n} as const;\n\nexport const nexusWidgetTheme = {\n colors: {\n ink,\n blue,\n surfaceScale: surface,\n status,\n\n background: surface.default,\n border: surface.border2,\n divider: surface.border,\n icon: ink[600],\n muted: ink[400],\n primary: blue[700],\n primaryText: blue[500],\n segmented: blue.tab,\n surface: surface.default,\n surfaceCool: surface.cool,\n surfaceInset: surface.soft,\n text: ink[800],\n textEmpty: ink[300],\n textStrong: ink[900],\n textSubtle: ink[500],\n },\n fonts: {\n display: '\"Delight-Medium\", \"Delight\", system-ui, sans-serif',\n mono: '\"JetBrains Mono\", ui-monospace, SFMono-Regular, Menlo, monospace',\n sans: '\"Geist\", var(--font-geist-sans), system-ui, sans-serif',\n },\n typography: {\n displayXl: {\n fontFamily: '\"Delight-Medium\", \"Delight\", system-ui, sans-serif',\n fontSize: \"31px\",\n fontWeight: 500,\n letterSpacing: \"0\",\n lineHeight: \"36px\",\n },\n displayLg: {\n fontFamily: '\"Delight-Medium\", \"Delight\", system-ui, sans-serif',\n fontSize: \"22px\",\n fontWeight: 500,\n letterSpacing: \"0\",\n lineHeight: \"26px\",\n },\n headingPanel: {\n fontFamily: '\"Delight-Medium\", \"Delight\", system-ui, sans-serif',\n fontSize: \"17px\",\n fontWeight: 500,\n letterSpacing: \"0\",\n lineHeight: \"21px\",\n },\n bodyLg: {\n fontFamily: '\"Geist\", var(--font-geist-sans), system-ui, sans-serif',\n fontSize: \"14px\",\n fontWeight: 400,\n letterSpacing: \"0\",\n lineHeight: \"17px\",\n },\n bodyMd: {\n fontFamily: '\"Geist\", var(--font-geist-sans), system-ui, sans-serif',\n fontSize: \"14px\",\n fontWeight: 400,\n letterSpacing: \"0\",\n lineHeight: \"20px\",\n },\n bodySm: {\n fontFamily: '\"Geist\", var(--font-geist-sans), system-ui, sans-serif',\n fontSize: \"13px\",\n fontWeight: 400,\n letterSpacing: \"0\",\n lineHeight: \"17px\",\n },\n labelCap: {\n fontFamily: '\"Geist\", var(--font-geist-sans), system-ui, sans-serif',\n fontSize: \"11px\",\n fontWeight: 500,\n letterSpacing: \"0.08em\",\n lineHeight: \"14px\",\n textTransform: \"uppercase\",\n },\n caption: {\n fontFamily: '\"Geist\", var(--font-geist-sans), system-ui, sans-serif',\n fontSize: \"11px\",\n fontWeight: 400,\n letterSpacing: \"0\",\n lineHeight: \"15px\",\n },\n code: {\n fontFamily:\n '\"JetBrains Mono\", ui-monospace, SFMono-Regular, Menlo, monospace',\n fontSize: \"11px\",\n fontWeight: 400,\n letterSpacing: \"0\",\n lineHeight: \"14px\",\n },\n },\n radius: {\n modal: \"16px\",\n panel: \"9px\",\n primaryButton: \"10px\",\n segmented: \"7px\",\n segmentedItem: \"5px\",\n tokenPill: \"999px\",\n iconButton: \"999px\",\n },\n shadows: {\n card: \"#3C286433 0px 0px 3px, #3C28640A 0px 1px 4px\",\n control: \"#3C28640F 0px 1px 2px, #3C28640A 0px 2px 6px\",\n iconButton: \"#3C28640F 0px 1px 2px, #3C28640A 0px 2px 4px\",\n inset: \"#3C28640F 0px 1px 2px inset\",\n primaryButton:\n \"#FFFFFF12 0px 1px 0px inset, #0000001F 0px 1px 2px, #14141E24 0px 4px 10px\",\n root: \"#FFFFFFE6 0px 1px 0px inset, #FFFFFF8C 0px 0px 0px 9px, #11346A0A 0px 2px 4px, #0078F721 0px 9px 18px, #1571FE1F 0px 22px 42px\",\n segmentedActive:\n \"#FFFFFFE6 0px 1px 0px inset, #3C286414 0px 1px 2px, #3C28640F 0px 2px 6px\",\n sheet: \"#0000001F 0px -4px 20px\",\n tokenPill: \"#3C28640F 0px 1px 2px, #3C28640A 0px 2px 8px\",\n tooltip: \"0 8px 24px rgba(22,22,21,0.12)\",\n },\n primitives: {\n badge: {\n backgroundColor: blue[100],\n color: blue[500],\n },\n iconButton: {\n backgroundColor: surface.default,\n borderColor: \"#0000000F\",\n boxShadow: \"#3C28640F 0px 1px 2px, #3C28640A 0px 2px 4px\",\n size: \"29px\",\n },\n tokenPill: {\n backgroundColor: surface.default,\n borderColor: \"#0000000A\",\n boxShadow: \"#3C28640F 0px 1px 2px, #3C28640A 0px 2px 8px\",\n },\n },\n} as const;\n", + "content": "const ink = {\n 900: \"#161615\",\n 800: \"#1F1F1F\",\n 600: \"#5B5B5A\",\n 500: \"#848483\",\n 400: \"#8E8E89\",\n 300: \"#C9C9C5\",\n} as const;\n\nexport const NEXUS_WIDGET_DEFAULT_PRIMARY_COLOR = \"rgb(0, 107, 244)\" as const;\nexport const NEXUS_WIDGET_FAST_SPINNER_STYLE = {\n animationDuration: \"700ms\",\n} as const;\nexport const NEXUS_WIDGET_FAST_SPINNER_ANIMATION =\n \"spin 700ms linear infinite\" as const;\n\nconst blue = {\n 700: \"var(--foreground-brand)\",\n 500: \"var(--foreground-brand)\",\n 100: \"#EAF1FF\",\n 50: \"#E5EEFF\",\n tab: \"#F0F3F9\",\n} as const;\n\nconst surface = {\n default: \"#FFFFFE\",\n soft: \"#FAFAFC\",\n cool: \"#F6F6F6\",\n border: \"#ECECEA\",\n border2: \"#E8E8E7\",\n} as const;\n\nconst status = {\n success: \"#1BC57A\",\n successBg: \"rgba(27, 197, 122, 0.18)\",\n} as const;\n\nexport const nexusWidgetTheme = {\n colors: {\n ink,\n blue,\n surfaceScale: surface,\n status,\n\n background: surface.default,\n border: surface.border2,\n divider: surface.border,\n icon: ink[600],\n muted: ink[400],\n primary: blue[700],\n primaryText: blue[500],\n segmented: blue.tab,\n surface: surface.default,\n surfaceCool: surface.cool,\n surfaceInset: surface.soft,\n text: ink[800],\n textEmpty: ink[300],\n textStrong: ink[900],\n textSubtle: ink[500],\n },\n fonts: {\n display: '\"Delight-Medium\", \"Delight\", system-ui, sans-serif',\n mono: '\"JetBrains Mono\", ui-monospace, SFMono-Regular, Menlo, monospace',\n sans: '\"Geist\", var(--font-geist-sans), system-ui, sans-serif',\n },\n typography: {\n displayXl: {\n fontFamily: '\"Delight-Medium\", \"Delight\", system-ui, sans-serif',\n fontSize: \"31px\",\n fontWeight: 500,\n letterSpacing: \"0\",\n lineHeight: \"36px\",\n },\n displayLg: {\n fontFamily: '\"Delight-Medium\", \"Delight\", system-ui, sans-serif',\n fontSize: \"22px\",\n fontWeight: 500,\n letterSpacing: \"0\",\n lineHeight: \"26px\",\n },\n headingPanel: {\n fontFamily: '\"Delight-Medium\", \"Delight\", system-ui, sans-serif',\n fontSize: \"17px\",\n fontWeight: 500,\n letterSpacing: \"0\",\n lineHeight: \"21px\",\n },\n bodyLg: {\n fontFamily: '\"Geist\", var(--font-geist-sans), system-ui, sans-serif',\n fontSize: \"14px\",\n fontWeight: 400,\n letterSpacing: \"0\",\n lineHeight: \"17px\",\n },\n bodyMd: {\n fontFamily: '\"Geist\", var(--font-geist-sans), system-ui, sans-serif',\n fontSize: \"14px\",\n fontWeight: 400,\n letterSpacing: \"0\",\n lineHeight: \"20px\",\n },\n bodySm: {\n fontFamily: '\"Geist\", var(--font-geist-sans), system-ui, sans-serif',\n fontSize: \"13px\",\n fontWeight: 400,\n letterSpacing: \"0\",\n lineHeight: \"17px\",\n },\n labelCap: {\n fontFamily: '\"Geist\", var(--font-geist-sans), system-ui, sans-serif',\n fontSize: \"11px\",\n fontWeight: 500,\n letterSpacing: \"0.08em\",\n lineHeight: \"14px\",\n textTransform: \"uppercase\",\n },\n caption: {\n fontFamily: '\"Geist\", var(--font-geist-sans), system-ui, sans-serif',\n fontSize: \"11px\",\n fontWeight: 400,\n letterSpacing: \"0\",\n lineHeight: \"15px\",\n },\n code: {\n fontFamily:\n '\"JetBrains Mono\", ui-monospace, SFMono-Regular, Menlo, monospace',\n fontSize: \"11px\",\n fontWeight: 400,\n letterSpacing: \"0\",\n lineHeight: \"14px\",\n },\n },\n radius: {\n modal: \"16px\",\n panel: \"9px\",\n primaryButton: \"10px\",\n segmented: \"7px\",\n segmentedItem: \"5px\",\n tokenPill: \"999px\",\n iconButton: \"999px\",\n },\n shadows: {\n card: \"#3C286433 0px 0px 3px, #3C28640A 0px 1px 4px\",\n control: \"#3C28640F 0px 1px 2px, #3C28640A 0px 2px 6px\",\n iconButton: \"#3C28640F 0px 1px 2px, #3C28640A 0px 2px 4px\",\n inset: \"#3C28640F 0px 1px 2px inset\",\n primaryButton:\n \"#FFFFFF12 0px 1px 0px inset, #0000001F 0px 1px 2px, #14141E24 0px 4px 10px\",\n root: \"#FFFFFFE6 0px 1px 0px inset, #FFFFFF8C 0px 0px 0px 9px, #11346A0A 0px 2px 4px, #0078F721 0px 9px 18px, #1571FE1F 0px 22px 42px\",\n segmentedActive:\n \"#FFFFFFE6 0px 1px 0px inset, #3C286414 0px 1px 2px, #3C28640F 0px 2px 6px\",\n sheet: \"#0000001F 0px -4px 20px\",\n tokenPill: \"#3C28640F 0px 1px 2px, #3C28640A 0px 2px 8px\",\n tooltip: \"0 8px 24px rgba(22,22,21,0.12)\",\n },\n primitives: {\n badge: {\n backgroundColor: blue[100],\n color: blue[500],\n },\n iconButton: {\n backgroundColor: surface.default,\n borderColor: \"#0000000F\",\n boxShadow: \"#3C28640F 0px 1px 2px, #3C28640A 0px 2px 4px\",\n size: \"29px\",\n },\n tokenPill: {\n backgroundColor: surface.default,\n borderColor: \"#0000000A\",\n boxShadow: \"#3C28640F 0px 1px 2px, #3C28640A 0px 2px 8px\",\n },\n },\n} as const;\n", "type": "registry:component", "target": "components/nexus/theme.ts" }, diff --git a/registry/avail-widgets/nexus-widget/components/deposit-idle-form.tsx b/registry/avail-widgets/nexus-widget/components/deposit-idle-form.tsx index 2ea8eac5..27dc84e5 100644 --- a/registry/avail-widgets/nexus-widget/components/deposit-idle-form.tsx +++ b/registry/avail-widgets/nexus-widget/components/deposit-idle-form.tsx @@ -4,6 +4,7 @@ import Decimal from "decimal.js"; import { AlertCircle, ChevronDown, Loader2 } from "lucide-react"; import React, { useRef, useState } from "react"; import { PayWithSources as SharedPayWithSources } from "./pay-with-sources"; +import { NEXUS_WIDGET_FAST_SPINNER_STYLE } from "../theme"; import { formatSelectedTokenBalanceLabel, formatUsdBalanceLabel, @@ -390,7 +391,11 @@ function PayWithSources({ > Calculating best route... @@ -802,6 +807,7 @@ export function DepositIdleForm({ ) : ( - + )} Calculating best route... diff --git a/registry/avail-widgets/nexus-widget/components/send-idle-form.tsx b/registry/avail-widgets/nexus-widget/components/send-idle-form.tsx index 5938ff00..54f73ef8 100644 --- a/registry/avail-widgets/nexus-widget/components/send-idle-form.tsx +++ b/registry/avail-widgets/nexus-widget/components/send-idle-form.tsx @@ -4,6 +4,7 @@ import Decimal from "decimal.js"; import { AlertCircle, ChevronDown, Loader2 } from "lucide-react"; import React, { useRef, useState } from "react"; import { PayWithSources as SharedPayWithSources } from "./pay-with-sources"; +import { NEXUS_WIDGET_FAST_SPINNER_STYLE } from "../theme"; import { formatSelectedTokenBalanceLabel, type SwapTokenOption, @@ -390,7 +391,11 @@ function PayWithSources({ > Calculating best route... @@ -867,6 +872,7 @@ export function SendIdleForm({

+ ) : isRefreshing ? ( "Refreshing quotes..." ) : quoteUnavailable ? ( diff --git a/registry/avail-widgets/nexus-widget/nexus-widget.tsx b/registry/avail-widgets/nexus-widget/nexus-widget.tsx index f8e179c6..39f62c4a 100644 --- a/registry/avail-widgets/nexus-widget/nexus-widget.tsx +++ b/registry/avail-widgets/nexus-widget/nexus-widget.tsx @@ -79,6 +79,7 @@ import { } from "./components/swap-intent-preview"; import { NEXUS_WIDGET_DEFAULT_PRIMARY_COLOR, + NEXUS_WIDGET_FAST_SPINNER_STYLE, nexusWidgetTheme, } from "./theme"; import { @@ -11265,12 +11266,13 @@ function NexusWidgetInner({ ) : (needsWalletConnection && walletConnectBusy) || quoteRefreshing || receiveMaxCalculating ? ( - Date: Wed, 22 Jul 2026 18:52:49 +0530 Subject: [PATCH 02/28] Add onramp --- public/r/nexus.json | 16 +- registry.json | 10 + .../components/deposit-funding-method.tsx | 303 ++ .../components/deposit-onramp-flow.tsx | 3462 +++++++++++++++++ .../components/swap-idle-form.tsx | 70 +- .../nexus-widget/nexus-widget.tsx | 270 +- 6 files changed, 4042 insertions(+), 89 deletions(-) create mode 100644 registry/avail-widgets/nexus-widget/components/deposit-funding-method.tsx create mode 100644 registry/avail-widgets/nexus-widget/components/deposit-onramp-flow.tsx diff --git a/public/r/nexus.json b/public/r/nexus.json index 56a8ac7d..0174259a 100644 --- a/public/r/nexus.json +++ b/public/r/nexus.json @@ -30,12 +30,24 @@ "type": "registry:component", "target": "components/nexus/components/amount-input-unified.tsx" }, + { + "path": "registry/avail-widgets/nexus-widget/components/deposit-funding-method.tsx", + "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\nimport { CreditCard, Wallet } from \"lucide-react\";\nimport React from \"react\";\nimport { nexusWidgetTheme } from \"../theme\";\n\ntype FundingMethod = \"wallet\" | \"local-currency\";\n\ninterface DepositFundingMethodProps {\n onSelectLocalCurrency: () => void;\n onSelectWallet: () => void;\n primaryButtonForeground: string;\n totalBalance: string;\n}\n\nconst theme = nexusWidgetTheme;\nconst brand = \"var(--foreground-brand)\";\n\nconst optionBaseStyle: React.CSSProperties = {\n alignItems: \"center\",\n backgroundColor: theme.colors.surface,\n borderRadius: \"12px\",\n borderStyle: \"solid\",\n borderWidth: \"1px\",\n boxSizing: \"border-box\",\n cursor: \"pointer\",\n display: \"flex\",\n gap: \"10px\",\n minHeight: \"66px\",\n padding: \"12px\",\n textAlign: \"left\",\n width: \"100%\",\n};\n\nfunction MethodIcon({\n children,\n}: {\n children: React.ReactNode;\n}) {\n return (\n \n {children}\n \n );\n}\n\nfunction RadioMark({ selected }: { selected: boolean }) {\n return (\n \n {selected && (\n \n )}\n \n );\n}\n\nfunction FundingOption({\n active,\n amount,\n description,\n icon,\n label,\n onClick,\n recommended,\n}: {\n active: boolean;\n amount?: string;\n description: string;\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n recommended?: boolean;\n}) {\n return (\n \n {icon}\n \n \n \n {label}\n \n {recommended && (\n \n Recommended\n \n )}\n \n \n {description}\n \n \n {amount && (\n \n ${amount}\n \n )}\n \n \n );\n}\n\nexport function DepositFundingMethod({\n onSelectLocalCurrency,\n onSelectWallet,\n primaryButtonForeground,\n totalBalance,\n}: DepositFundingMethodProps) {\n const [selectedMethod, setSelectedMethod] =\n React.useState(null);\n\n const handleContinue = () => {\n if (!selectedMethod) return;\n if (selectedMethod === \"wallet\") {\n onSelectWallet();\n return;\n }\n onSelectLocalCurrency();\n };\n\n return (\n \n \n Select a funding method\n \n \n \n }\n label=\"Pay with Wallet\"\n onClick={() => {\n setSelectedMethod(\"wallet\");\n }}\n />\n \n }\n label=\"Pay with Local Currency\"\n onClick={() => {\n setSelectedMethod(\"local-currency\");\n }}\n recommended\n />\n \n \n Continue\n \n \n );\n}\n", + "type": "registry:component", + "target": "components/nexus/components/deposit-funding-method.tsx" + }, { "path": "registry/avail-widgets/nexus-widget/components/deposit-idle-form.tsx", "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\nimport Decimal from \"decimal.js\";\nimport { AlertCircle, ChevronDown, Loader2 } from \"lucide-react\";\nimport React, { useRef, useState } from \"react\";\nimport { PayWithSources as SharedPayWithSources } from \"./pay-with-sources\";\nimport { NEXUS_WIDGET_FAST_SPINNER_STYLE } from \"../theme\";\nimport {\n formatSelectedTokenBalanceLabel,\n formatUsdBalanceLabel,\n type SwapTokenOption,\n} from \"./swap-asset-selector\";\n\ninterface DepositIdleFormProps {\n amount: string;\n amountMode: \"token\" | \"usd\";\n calculatingPercent?: number | null;\n fromTokens: SwapTokenOption[];\n isAmountReadOnly?: boolean;\n isBalanceLoading?: boolean;\n isCalculatingMax?: boolean;\n isQuoteRefreshing?: boolean;\n isSourcePickerDisabled?: boolean;\n isTokenPickerDisabled?: boolean;\n hideDestinationTokenDropdownIcon?: boolean;\n onAmountChange: (val: string) => void;\n onAmountModeToggle: () => void;\n onOpenSourcePicker: () => void;\n onOpenTokenPicker?: () => void;\n onSetPercent: (pct: number) => void;\n reserveSourceRows?: boolean;\n routeMessage?: string;\n routeStatus?: \"loading\" | \"insufficient\";\n showAutoBadge?: boolean;\n tokenValue: string;\n toToken?: SwapTokenOption;\n totalBalance: string;\n usdValue: string;\n}\n\nconst uiFont = '\"Geist\", system-ui, sans-serif';\nconst primary = \"#161615\";\nconst muted = \"#848483\";\nconst border = \"#E8E8E7\";\nconst brand = \"var(--foreground-brand)\";\n\nconst parseDecimal = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = String(value).replace(/[^0-9.-]/g, \"\");\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst formatToken = (value: unknown) => {\n const amount = parseDecimal(value) ?? new Decimal(0);\n return amount.toDecimalPlaces(8).toFixed();\n};\n\nconst formatUsd = (value: unknown) => {\n const amount = parseDecimal(value) ?? new Decimal(0);\n if (amount.gt(0) && amount.lt(0.01)) return \"<$0.01\";\n return `$${amount.toDecimalPlaces(2).toFixed()}`;\n};\n\nconst MAX_AMOUNT_DISPLAY_DECIMALS = 8;\nconst getTokenInputDecimals = (token?: Pick) => {\n const decimals = Number(token?.decimals);\n return Number.isFinite(decimals) && decimals >= 0 ? Math.floor(decimals) : 18;\n};\n\nconst formatAmountInputDisplay = (value: string) => {\n if (!value) return \"\";\n try {\n return new Decimal(value)\n .toDecimalPlaces(MAX_AMOUNT_DISPLAY_DECIMALS, Decimal.ROUND_DOWN)\n .toFixed();\n } catch {\n return value;\n }\n};\n\nconst sanitizeAmountInput = (raw: string, maxDecimals: number) => {\n let next = raw.replaceAll(/[^0-9.]/g, \"\");\n const parts = next.split(\".\");\n if (parts.length > 2) next = parts[0] + \".\" + parts.slice(1).join(\"\");\n const [integerPart, decimalPart] = next.split(\".\");\n if (decimalPart !== undefined) {\n next = `${integerPart}.${decimalPart.slice(0, Math.max(0, maxDecimals))}`;\n }\n if (next === \".\") next = \"0.\";\n return next;\n};\n\nfunction TokenLogo({\n src,\n label,\n size = 30,\n fontSize = 12,\n style,\n}: {\n src?: string;\n label?: string;\n size?: number;\n fontSize?: number;\n style?: React.CSSProperties;\n}) {\n const [failed, setFailed] = useState(!src);\n\n React.useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n backgroundColor: \"#FFFFFE\",\n borderRadius: \"999px\",\n height: size,\n objectFit: \"cover\",\n width: size,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {(label || \"?\").slice(0, 1).toUpperCase()}\n \n );\n}\n\nfunction SourceLogoPair({ token }: { token: SwapTokenOption }) {\n return (\n

\n \n {token.chainLogo && (\n \n )}\n
\n );\n}\n\nfunction ExactOutPercentButtons({\n visible,\n onSelect,\n}: {\n visible: boolean;\n onSelect: (pct: number) => void;\n}) {\n const [focusedPercent, setFocusedPercent] = useState(null);\n\n React.useEffect(() => {\n if (!visible) setFocusedPercent(null);\n }, [visible]);\n\n return (\n \n {[25, 50, 75, 100].map((pct) => {\n const isMax = pct === 100;\n const isFocused = focusedPercent === pct;\n return (\n {\n e.stopPropagation();\n onSelect(pct);\n }}\n onBlur={() => setFocusedPercent(null)}\n onFocus={() => setFocusedPercent(pct)}\n style={{\n alignItems: \"center\",\n backgroundColor: isFocused ? \"#E8F0FF\" : \"#F4F4F3\",\n border: \"none\",\n borderRadius: \"8px\",\n boxSizing: \"border-box\",\n color: isFocused ? brand : \"#363635\",\n cursor: \"pointer\",\n display: \"flex\",\n flex: \"1 1 0%\",\n fontFamily: '\"Geist\", system-ui, sans-serif',\n fontSize: \"12px\",\n fontWeight: 500,\n justifyContent: \"center\",\n lineHeight: \"20px\",\n minWidth: 0,\n paddingBlock: \"5px\",\n paddingInline: \"10px\",\n }}\n type=\"button\"\n >\n {isMax ? \"MAX\" : `${pct}%`}\n \n );\n })}\n \n );\n}\n\nfunction SkeletonRow() {\n return (\n \n \n \n \n );\n}\n\nfunction PayWithSources({\n fromTokens,\n onOpenSourcePicker,\n routeStatus,\n routeMessage,\n}: {\n fromTokens: SwapTokenOption[];\n onOpenSourcePicker: () => void;\n routeStatus?: \"loading\" | \"insufficient\";\n routeMessage?: string;\n}) {\n const scrollRef = useRef(null);\n const isRouteLoading = routeStatus === \"loading\";\n const shouldShowSourceSummary = !isRouteLoading && fromTokens.length > 0;\n const shouldScroll = shouldShowSourceSummary && fromTokens.length > 3;\n const autoBadge = (\n \n AUTO\n
\n );\n\n return (\n \n \n \n \n Pay With\n {shouldShowSourceSummary ? ` · ${fromTokens.length} assets` : \"\"}\n \n {autoBadge}\n \n {shouldShowSourceSummary && (\n \n Edit tokens\n \n )}\n \n\n {isRouteLoading ? (\n <>\n \n \n \n Calculating best route...\n \n \n ) : shouldShowSourceSummary ? (\n
\n \n {fromTokens.map((token, index) => (\n \n \n \n \n \n {token.symbol}\n \n \n {token.isUnified\n ? \"Unified balance\"\n : `on ${token.chainName || \"Unknown chain\"}`}\n \n
\n \n \n \n {formatToken(token.userAmount || token.balance)}{\" \"}\n {token.symbol}\n \n \n {formatUsd(token.userAmountUsd || token.balanceInFiat)}\n \n \n \n ))}\n \n {shouldScroll && (\n \n scrollRef.current?.scrollBy({ behavior: \"smooth\", top: 64 })\n }\n style={{\n alignItems: \"center\",\n background: \"#FFFFFE\",\n border: `1px solid ${border}`,\n borderRadius: \"999px\",\n bottom: \"4px\",\n boxShadow: \"0 2px 8px rgba(22,22,21,0.08)\",\n cursor: \"pointer\",\n display: \"flex\",\n height: \"22px\",\n justifyContent: \"center\",\n left: \"50%\",\n padding: 0,\n position: \"absolute\",\n transform: \"translateX(-50%)\",\n width: \"22px\",\n }}\n type=\"button\"\n >\n \n \n )}\n \n ) : (\n \n Sources will be auto selected\n \n )}\n\n {routeStatus === \"insufficient\" && routeMessage && (\n \n \n {routeMessage}\n \n )}\n \n );\n}\n\nfunction BalanceSkeleton({\n height = \"16px\",\n width = \"72px\",\n}: {\n height?: string;\n width?: string;\n}) {\n return (\n \n );\n}\n\nexport function DepositIdleForm({\n amount,\n amountMode,\n onAmountChange,\n onAmountModeToggle,\n toToken,\n totalBalance,\n usdValue,\n tokenValue,\n fromTokens,\n onOpenSourcePicker,\n onSetPercent,\n routeStatus,\n routeMessage,\n isAmountReadOnly = false,\n isBalanceLoading = false,\n isCalculatingMax,\n calculatingPercent,\n isQuoteRefreshing,\n hideDestinationTokenDropdownIcon = false,\n showAutoBadge = true,\n isSourcePickerDisabled = false,\n isTokenPickerDisabled = false,\n onOpenTokenPicker,\n reserveSourceRows = false,\n}: DepositIdleFormProps) {\n const [pendingPercent, setPendingPercent] = useState(null);\n const [isAmountFocused, setIsAmountFocused] = useState(false);\n\n React.useEffect(() => {\n if (!isCalculatingMax) setPendingPercent(null);\n }, [isCalculatingMax]);\n\n const handlePercentSelect = (pct: number) => {\n setPendingPercent(pct);\n onSetPercent(pct);\n };\n\n const handleInput = (e: React.ChangeEvent) => {\n if (isAmountReadOnly) return;\n onAmountChange(\n sanitizeAmountInput(\n e.target.value,\n isUsdMode\n ? MAX_AMOUNT_DISPLAY_DECIMALS\n : getTokenInputDecimals(toToken),\n ),\n );\n };\n const isUsdMode = amountMode === \"usd\";\n const amountDisplayValue = isAmountFocused\n ? amount\n : formatAmountInputDisplay(amount);\n const showTokenPickerDropdownIcon =\n Boolean(onOpenTokenPicker) &&\n !isTokenPickerDisabled &&\n !hideDestinationTokenDropdownIcon;\n const activePendingPercent =\n calculatingPercent ?? (isCalculatingMax ? pendingPercent : null);\n const isMaxCalculating = Boolean(\n isCalculatingMax && activePendingPercent === 100,\n );\n const destinationBalanceLabel = isUsdMode\n ? formatUsdBalanceLabel(toToken?.balanceInFiat)\n : formatSelectedTokenBalanceLabel(toToken) ||\n `0 ${toToken?.symbol || \"\"}`.trim();\n\n return (\n \n \n \n \n Deposit\n \n
\n \n Total Balance:\n \n \n {isBalanceLoading ? (\n \n ) : (\n `$${totalBalance}`\n )}\n \n
\n \n\n
\n \n \n {isMaxCalculating ? (\n \n ) : (\n <>\n {isUsdMode && amount && (\n \n $\n \n )}\n setIsAmountFocused(false)}\n onChange={handleInput}\n onFocus={() => setIsAmountFocused(true)}\n placeholder=\"0\"\n style={{\n background: \"transparent\",\n border: \"none\",\n boxSizing: \"border-box\",\n color: amount ? primary : \"#C8C8C6\",\n cursor: isAmountReadOnly ? \"default\" : \"text\",\n fontFamily:\n '\"Delight-Medium\", \"Delight\", system-ui, sans-serif',\n fontSize: \"36px\",\n fontWeight: 500,\n lineHeight: \"44px\",\n minWidth: 0,\n outline: \"none\",\n padding: 0,\n width: \"100%\",\n }}\n readOnly={isAmountReadOnly}\n type=\"text\"\n value={amountDisplayValue}\n />\n \n )}\n {isCalculatingMax && !isMaxCalculating && (\n \n )}\n
\n\n \n \n \n {toToken?.chainLogo && (\n \n )}\n \n \n {toToken?.symbol || \"Token\"}\n \n {showTokenPickerDropdownIcon && (\n \n )}\n \n \n\n \n \n \n {isUsdMode\n ? `≈ ${tokenValue || \"0\"} ${toToken?.symbol || \"\"} ↕`\n : `≈ $${usdValue || \"0\"} ↕`}\n \n \n\n \n \n Balance:\n \n \n {isBalanceLoading ? (\n \n ) : (\n destinationBalanceLabel\n )}\n \n \n \n\n \n \n \n\n \n \n );\n}\n", "type": "registry:component", "target": "components/nexus/components/deposit-idle-form.tsx" }, + { + "path": "registry/avail-widgets/nexus-widget/components/deposit-onramp-flow.tsx", + "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\nimport Decimal from \"decimal.js\";\nimport {\n Check,\n ChevronDown,\n CreditCard,\n ExternalLink,\n Info,\n Landmark,\n Loader2,\n Search,\n Smartphone,\n X,\n} from \"lucide-react\";\nimport React from \"react\";\nimport {\n NEXUS_WIDGET_FAST_SPINNER_STYLE,\n nexusWidgetTheme,\n} from \"../theme\";\nimport type { NexusWidgetDepositOpportunityConfig } from \"../types\";\nimport type { SwapTokenOption } from \"./swap-asset-selector\";\n\ntype OnrampCryptoCurrency = {\n chainId: number;\n contract?: string;\n currencyCode: string;\n decimals?: number;\n name?: string;\n};\n\ntype OnrampOptionsResponse = {\n countries?: {\n countryCode: string;\n flagUrl?: string;\n name: string;\n }[];\n selection?: {\n countryCode: string;\n cryptoCurrencies?: OnrampCryptoCurrency[];\n defaultFiat?: string;\n defaultPaymentMethods?: string[];\n fiatCurrencies?: string[];\n } | null;\n};\n\ntype OnrampPaymentMethod = {\n limits?: {\n currencyCode?: string;\n max?: string;\n min?: string;\n };\n method: string;\n type?: string;\n};\n\ntype OnrampRoute = {\n paymentMethods?: OnrampPaymentMethod[];\n provider: string;\n};\n\ntype OnrampRoutesResponse = {\n routes?: OnrampRoute[];\n};\n\ntype OnrampQuote = {\n destinationAmount: string;\n destinationCurrencyCode: string;\n fees?: {\n network?: string;\n partner?: string;\n provider?: string;\n total?: string;\n };\n lowKyc?: boolean;\n paymentMethodType: string;\n provider: string;\n rampScore?: number;\n sourceAmount: string;\n sourceCurrencyCode: string;\n};\n\ntype OnrampQuoteResponse = {\n quotes?: OnrampQuote[];\n};\n\ntype OnrampSessionResponse = {\n createdAt?: string;\n fallbackWidgetUrl?: string;\n paymentMethodType?: string;\n provider?: string;\n rawMeldStatus?: string;\n deposit?: {\n explorerUrl?: string;\n state?: string;\n txHash?: string;\n };\n sessionId?: string;\n state?: string;\n transaction?: {\n destinationAmount?: string;\n destinationCurrencyCode?: string;\n sourceAmount?: string;\n sourceCurrencyCode?: string;\n txHash?: string;\n walletAddress?: string;\n };\n updatedAt?: string;\n widgetUrl?: string;\n};\n\ntype OnrampSheet =\n | \"currency\"\n | \"destination\"\n | \"fees\"\n | \"method\"\n | \"partner\"\n | null;\n\ninterface DepositOnrampFlowProps {\n baseUrl?: string;\n destinationTokens?: SwapTokenOption[];\n onConnectWallet: () => void | Promise;\n onError?: (message: string) => void;\n onSelectDestinationToken?: (token: SwapTokenOption) => void;\n onSessionStateChange?: (state: string | null) => void;\n ownerAddress?: string;\n opportunity?: NexusWidgetDepositOpportunityConfig;\n primaryButtonForeground: string;\n toToken?: SwapTokenOption;\n}\n\nconst ONRAMP_CLIENT_HEADER = \"nexus-widget\";\nconst ONRAMP_DEFAULT_COUNTRY = \"IN\";\nconst ONRAMP_DEFAULT_BASE_URL = \"http://localhost:8000\";\nconst QUOTE_REFRESH_SECONDS = 15;\nconst ONRAMP_SESSION_POLL_MS = 3000;\nconst TEST_FIAT_CURRENCIES = [\"INR\", \"USD\", \"EUR\", \"JPY\", \"GBP\"] as const;\nconst theme = nexusWidgetTheme;\nconst brand = \"var(--foreground-brand)\";\n\nconst panelStyle: React.CSSProperties = {\n backgroundColor: theme.colors.surface,\n border: `1px solid ${theme.colors.border}`,\n borderRadius: \"12px\",\n boxShadow: theme.shadows.card,\n boxSizing: \"border-box\",\n overflow: \"hidden\",\n width: \"100%\",\n};\n\nconst sectionLabelStyle: React.CSSProperties = {\n color: theme.colors.textSubtle,\n fontFamily: theme.fonts.sans,\n fontSize: \"12px\",\n fontWeight: 600,\n letterSpacing: \"0.08em\",\n lineHeight: \"15px\",\n textTransform: \"uppercase\",\n};\n\nconst compactTitleStyle: React.CSSProperties = {\n color: theme.colors.textStrong,\n fontFamily: theme.fonts.display,\n fontSize: \"18px\",\n fontWeight: 500,\n letterSpacing: \"0\",\n lineHeight: \"23px\",\n};\n\nconst compactBodyStyle: React.CSSProperties = {\n color: theme.colors.textSubtle,\n fontFamily: theme.fonts.sans,\n fontSize: \"13px\",\n lineHeight: \"18px\",\n};\n\nconst parseDecimal = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = String(value).replace(/[^0-9.-]/g, \"\");\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst normalizeAmountInput = (raw: string) => {\n let next = raw.replaceAll(/[^0-9.]/g, \"\");\n const parts = next.split(\".\");\n if (parts.length > 2) next = parts[0] + \".\" + parts.slice(1).join(\"\");\n const [integerPart, decimalPart] = next.split(\".\");\n if (decimalPart !== undefined) {\n next = `${integerPart}.${decimalPart.slice(0, 2)}`;\n }\n if (next === \".\") next = \"0.\";\n return next;\n};\n\nconst formatNumberDisplay = (value: unknown, maxDecimals = 2) => {\n const parsed = parseDecimal(value) ?? new Decimal(0);\n return parsed\n .toDecimalPlaces(maxDecimals, Decimal.ROUND_DOWN)\n .toFixed()\n .replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n};\n\nconst formatCurrencyAmount = (value: unknown, currencyCode?: string) => {\n const suffix = currencyCode ? ` ${currencyCode}` : \"\";\n return `${formatNumberDisplay(value)}${suffix}`;\n};\n\nconst formatUsdDisplay = (value: unknown) => {\n const parsed = parseDecimal(value) ?? new Decimal(0);\n if (parsed.gt(0) && parsed.lt(0.01)) return \"<$0.01\";\n return `$${formatNumberDisplay(parsed, 2)}`;\n};\n\nconst getFiatCurrencyName = (currencyCode?: string) => {\n switch ((currencyCode ?? \"\").toUpperCase()) {\n case \"EUR\":\n return \"Euro\";\n case \"GBP\":\n return \"British Pound\";\n case \"INR\":\n return \"Indian Rupee\";\n case \"JPY\":\n return \"Japanese Yen\";\n case \"USD\":\n return \"US Dollar\";\n default:\n return currencyCode ?? \"\";\n }\n};\n\nconst getMethodLabel = (method?: string) => {\n switch ((method ?? \"\").toUpperCase()) {\n case \"APPLE_PAY\":\n return \"Apple Pay\";\n case \"GOOGLE_PAY\":\n return \"Google Pay\";\n case \"UPI\":\n return \"UPI\";\n case \"BANK_TRANSFER\":\n case \"IMPS\":\n case \"NEFT\":\n case \"RTGS\":\n return \"Bank Transfer\";\n case \"CREDIT_DEBIT_CARD\":\n case \"CARD\":\n return \"Credit / Debit Cards\";\n default:\n return method\n ? method\n .split(\"_\")\n .filter(Boolean)\n .map(\n (part) =>\n part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()\n )\n .join(\" \")\n : \"Payment method\";\n }\n};\n\nconst getMethodSubtitle = (method?: string) => {\n switch ((method ?? \"\").toUpperCase()) {\n case \"UPI\":\n return \"~1 min\";\n case \"APPLE_PAY\":\n case \"GOOGLE_PAY\":\n return \"~2 min\";\n case \"BANK_TRANSFER\":\n case \"IMPS\":\n case \"NEFT\":\n case \"RTGS\":\n return \"~3 min\";\n case \"CREDIT_DEBIT_CARD\":\n case \"CARD\":\n return \"~3 min\";\n default:\n return \"Available route\";\n }\n};\n\nconst getProviderLabel = (provider?: string) =>\n provider\n ? provider\n .split(/[_\\s-]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())\n .join(\" \")\n : \"Payment partner\";\n\nconst getProviderInitials = (provider?: string) => {\n const label = getProviderLabel(provider);\n const parts = label.split(/\\s+/).filter(Boolean);\n if (parts.length <= 1) return label.slice(0, 2).toUpperCase();\n return `${parts[0]?.[0] ?? \"\"}${parts[1]?.[0] ?? \"\"}`.toUpperCase();\n};\n\nconst getErrorMessage = (error: unknown) => {\n if (error instanceof Error) return error.message;\n if (typeof error === \"string\") return error;\n return \"Unable to continue with local currency.\";\n};\n\nconst ONRAMP_TERMINAL_STATES = new Set([\n \"CANCELLED\",\n \"EXPIRED\",\n \"FAILED\",\n \"REFUNDED\",\n \"SETTLED\",\n]);\n\nconst getNormalizedOnrampState = (state?: string | null) =>\n (state ?? \"\").trim().toUpperCase();\n\nconst isOnrampTerminalState = (state?: string | null) =>\n ONRAMP_TERMINAL_STATES.has(getNormalizedOnrampState(state)) ||\n isOnrampDepositSuccessState(state) ||\n isOnrampDepositFailedState(state);\n\nconst isOnrampProcessingState = (state?: string | null) => {\n const normalized = getNormalizedOnrampState(state);\n return normalized === \"PROCESSING\" || normalized === \"SETTLING\";\n};\n\nconst ONRAMP_DEPOSIT_PROCESSING_STATES = new Set([\n \"COMPLETING_DEPOSIT\",\n \"DEPOSIT_PROCESSING\",\n \"DEPOSITING\",\n]);\n\nconst ONRAMP_DEPOSIT_SUCCESS_STATES = new Set([\n \"COMPLETED\",\n \"DEPOSIT_COMPLETE\",\n \"DEPOSIT_SUCCESS\",\n \"DEPOSITED\",\n]);\n\nconst ONRAMP_DEPOSIT_FAILED_STATES = new Set([\n \"DEPOSIT_ATTENTION\",\n \"DEPOSIT_FAILED\",\n \"DEPOSIT_REQUIRES_ATTENTION\",\n]);\n\nconst isOnrampDepositProcessingState = (state?: string | null) =>\n ONRAMP_DEPOSIT_PROCESSING_STATES.has(getNormalizedOnrampState(state));\n\nconst isOnrampDepositSuccessState = (state?: string | null) =>\n ONRAMP_DEPOSIT_SUCCESS_STATES.has(getNormalizedOnrampState(state));\n\nconst isOnrampDepositFailedState = (state?: string | null) =>\n ONRAMP_DEPOSIT_FAILED_STATES.has(getNormalizedOnrampState(state));\n\nconst isNativeAddress = (address?: string) => {\n const lower = address?.toLowerCase();\n return (\n lower === \"0x0000000000000000000000000000000000000000\" ||\n lower === \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\"\n );\n};\n\nconst getChainCurrencySuffix = (chainId?: number, chainName?: string) => {\n switch (chainId) {\n case 1:\n return \"ETHEREUM\";\n case 10:\n return \"OPTIMISM\";\n case 56:\n return \"BNB\";\n case 137:\n return \"POLYGON\";\n case 8453:\n return \"BASE\";\n case 42161:\n return \"ARBITRUM\";\n case 43114:\n return \"AVALANCHE\";\n default:\n return (chainName ?? \"CHAIN\").replaceAll(/[^a-zA-Z0-9]+/g, \"_\").toUpperCase();\n }\n};\n\nconst getFallbackDestinationCurrencyCode = (token?: SwapTokenOption) => {\n if (!token?.symbol) return \"\";\n return `${token.symbol.toUpperCase()}_${getChainCurrencySuffix(\n token.chainId,\n token.chainName\n )}`;\n};\n\nconst getDestinationCurrencyCode = (\n options: OnrampOptionsResponse | null,\n token?: SwapTokenOption\n) => {\n if (!token?.chainId || !token.contractAddress) {\n return getFallbackDestinationCurrencyCode(token);\n }\n\n const targetAddress = token.contractAddress.toLowerCase();\n const currencies = options?.selection?.cryptoCurrencies ?? [];\n const matched = currencies.find((currency) => {\n if (currency.chainId !== token.chainId) return false;\n const contract = currency.contract?.toLowerCase();\n if (!contract) return false;\n return (\n contract === targetAddress ||\n (isNativeAddress(contract) && isNativeAddress(targetAddress))\n );\n });\n\n return matched?.currencyCode ?? getFallbackDestinationCurrencyCode(token);\n};\n\nconst getOnrampTokenKey = (token?: SwapTokenOption) => {\n if (!token?.chainId || !token.contractAddress) return \"\";\n return `${token.chainId}:${token.contractAddress.toLowerCase()}`;\n};\n\nconst isSameOnrampToken = (\n left?: SwapTokenOption,\n right?: SwapTokenOption\n) => Boolean(left && right && getOnrampTokenKey(left) === getOnrampTokenKey(right));\n\nconst sortQuotes = (quotes: OnrampQuote[]) =>\n [...quotes].sort((a, b) => {\n const scoreDelta = (b.rampScore ?? 0) - (a.rampScore ?? 0);\n if (scoreDelta !== 0) return scoreDelta;\n const destinationDelta =\n parseDecimal(b.destinationAmount)?.cmp(\n parseDecimal(a.destinationAmount) ?? new Decimal(0)\n ) ?? 0;\n return destinationDelta;\n });\n\nconst matchesSearch = (query: string, values: Array) => {\n const normalizedQuery = query.trim().toLowerCase();\n if (!normalizedQuery) return true;\n return values.some((value) =>\n (value ?? \"\").toLowerCase().includes(normalizedQuery)\n );\n};\n\nconst createIdempotencyKey = () => {\n if (typeof crypto !== \"undefined\" && \"randomUUID\" in crypto) {\n return crypto.randomUUID();\n }\n return `${Date.now()}-${Math.random().toString(16).slice(2)}`;\n};\n\nconst fetchOnrampJson = async (\n baseUrl: string,\n path: string,\n init?: RequestInit\n) => {\n const response = await fetch(`${baseUrl}${path}`, {\n ...init,\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-nexus-client\": ONRAMP_CLIENT_HEADER,\n ...(init?.headers ?? {}),\n },\n });\n const text = await response.text();\n const data = text ? JSON.parse(text) : {};\n\n if (!response.ok) {\n const message =\n typeof data?.message === \"string\"\n ? data.message\n : `Onramp request failed (${response.status})`;\n throw new Error(message);\n }\n\n return data as T;\n};\n\nfunction TokenLogo({\n label,\n size = 30,\n src,\n}: {\n label?: string;\n size?: number;\n src?: string;\n}) {\n const [failed, setFailed] = React.useState(!src);\n\n React.useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n backgroundColor: theme.colors.surface,\n borderRadius: \"999px\",\n height: size,\n objectFit: \"cover\",\n width: size,\n }}\n />\n );\n }\n\n return (\n \n {(label || \"?\").slice(0, 2).toUpperCase()}\n \n );\n}\n\nfunction TokenLogoPair({ token }: { token?: SwapTokenOption }) {\n return (\n \n \n {token?.chainLogo && (\n \n \n \n )}\n \n );\n}\n\nfunction CurrencyMark({ code }: { code?: string }) {\n return (\n \n {(code ?? \"?\").slice(0, 3).toUpperCase()}\n \n );\n}\n\nfunction ProviderMark({ provider }: { provider?: string }) {\n return (\n \n {getProviderInitials(provider)}\n \n );\n}\n\nfunction MethodMark({ method }: { method?: string }) {\n const normalized = (method ?? \"\").toUpperCase();\n const icon =\n normalized === \"UPI\" || normalized === \"APPLE_PAY\" || normalized === \"GOOGLE_PAY\" ? (\n \n ) : normalized.includes(\"BANK\") ||\n normalized === \"IMPS\" ||\n normalized === \"NEFT\" ||\n normalized === \"RTGS\" ? (\n \n ) : (\n \n );\n\n return (\n \n {icon}\n \n );\n}\n\nfunction SelectPill({\n children,\n disabled,\n onClick,\n}: {\n children: React.ReactNode;\n disabled?: boolean;\n onClick?: () => void;\n}) {\n return (\n \n {children}\n {!disabled && (\n \n )}\n \n );\n}\n\nfunction EditButton({\n disabled,\n onClick,\n}: {\n disabled?: boolean;\n onClick: () => void;\n}) {\n return (\n \n Edit\n \n );\n}\n\nfunction DetailRow({\n action,\n children,\n divider,\n label,\n}: {\n action?: React.ReactNode;\n children: React.ReactNode;\n divider?: boolean;\n label: string;\n}) {\n return (\n \n \n
{label}
\n {action}\n \n {children}\n \n );\n}\n\nfunction Sheet({\n children,\n onClose,\n title,\n}: {\n children: React.ReactNode;\n onClose: () => void;\n title: string;\n}) {\n return (\n \n \n \n \n \n {title}\n \n \n \n \n \n \n {children}\n \n \n \n );\n}\n\nfunction SheetSearchInput({\n onChange,\n placeholder,\n value,\n}: {\n onChange: (value: string) => void;\n placeholder: string;\n value: string;\n}) {\n const [focused, setFocused] = React.useState(false);\n return (\n \n \n setFocused(false)}\n onChange={(event) => onChange(event.target.value)}\n onFocus={() => setFocused(true)}\n placeholder={placeholder}\n style={{\n backgroundColor: \"transparent\",\n border: \"none\",\n color: theme.colors.textStrong,\n flex: \"1 1 0%\",\n fontFamily: theme.fonts.sans,\n fontSize: \"14px\",\n lineHeight: \"18px\",\n minWidth: 0,\n outline: \"none\",\n padding: 0,\n }}\n value={value}\n />\n {value && (\n onChange(\"\")}\n style={{\n alignItems: \"center\",\n backgroundColor: \"transparent\",\n border: \"none\",\n cursor: \"pointer\",\n display: \"flex\",\n padding: \"2px\",\n }}\n type=\"button\"\n >\n \n \n )}\n \n );\n}\n\nfunction SelectRow({\n icon,\n onClick,\n primary,\n selected,\n subtitle,\n title,\n value,\n}: {\n icon: React.ReactNode;\n onClick: () => void;\n primary?: boolean;\n selected: boolean;\n subtitle?: string;\n title: string;\n value?: string;\n}) {\n return (\n \n {icon}\n \n \n \n {title}\n \n {primary && (\n \n Best rate\n \n )}\n \n {subtitle && (\n \n {subtitle}\n \n )}\n \n {value && (\n \n {value}\n \n )}\n \n {selected && (\n \n )}\n \n \n );\n}\n\nfunction EmptySheetMessage({ children }: { children: React.ReactNode }) {\n return (\n \n {children}\n \n );\n}\n\nfunction SkeletonBlock({\n borderRadius = \"8px\",\n height,\n width,\n}: {\n borderRadius?: string;\n height: string;\n width: string;\n}) {\n return (\n \n );\n}\n\nfunction QuoteDetailsSkeleton() {\n return (\n
\n }\n label=\"Payment Method\"\n >\n
\n \n \n
\n \n\n }\n divider\n label=\"Payment Partner\"\n >\n
\n \n
\n \n \n
\n
\n \n\n \n
\n \n \n
\n
\n \n \n
\n
\n \n );\n}\n\nconst formatTokenAmountDisplay = (amount: unknown, symbol?: string) => {\n const suffix = symbol ? ` ${symbol}` : \"\";\n if (amount === null || amount === undefined || amount === \"\") {\n return suffix ? `--${suffix}` : \"--\";\n }\n return `${formatNumberDisplay(amount, 6)}${suffix}`;\n};\n\nconst getOnrampSessionSubtitle = (\n state: string | null | undefined,\n opportunity?: NexusWidgetDepositOpportunityConfig\n) => {\n const normalized = getNormalizedOnrampState(state);\n if (normalized === \"SETTLED\" || isOnrampDepositSuccessState(normalized)) {\n return `The amount was deposited on ${getDepositTargetLabel(opportunity)}`;\n }\n if (isOnrampDepositFailedState(normalized)) {\n return \"The funds are in your wallet, but the deposit transaction could not be performed automatically.\";\n }\n if (isOnrampDepositProcessingState(normalized)) {\n return \"Usually takes 20 seconds\";\n }\n if (normalized === \"FAILED\") return \"The payment could not be completed\";\n if (normalized === \"CANCELLED\") return \"No payment was completed\";\n if (normalized === \"REFUNDED\") return \"The payment was refunded\";\n if (normalized === \"EXPIRED\") return \"Start a new payment to continue\";\n if (isOnrampProcessingState(normalized)) return \"Usually takes 2 - 5 min\";\n return \"The payment processor requires some information\";\n};\n\nconst getOnrampSummaryLabel = (\n state: string | null | undefined,\n provider?: string\n) => {\n const partner = getProviderLabel(provider);\n const normalized = getNormalizedOnrampState(state);\n if (normalized === \"SETTLED\") return `Payment received from ${partner}`;\n if (normalized === \"FAILED\") return `Payment failed with ${partner}`;\n if (normalized === \"CANCELLED\") return `Payment cancelled with ${partner}`;\n if (normalized === \"REFUNDED\") return `Payment refunded by ${partner}`;\n if (normalized === \"EXPIRED\") return `Payment expired with ${partner}`;\n if (isOnrampProcessingState(normalized)) {\n return `Payment to be received by ${partner}`;\n }\n return `Payment to receive from ${partner}`;\n};\n\nconst getDepositTargetLabel = (\n opportunity?: NexusWidgetDepositOpportunityConfig\n) =>\n opportunity?.title ||\n opportunity?.label ||\n opportunity?.protocol ||\n \"the selected market\";\n\nconst getDepositChainName = (\n opportunity?: NexusWidgetDepositOpportunityConfig,\n token?: SwapTokenOption\n) => token?.chainName ?? opportunity?.subtitle?.replace(/^on\\s+/i, \"\") ?? \"chain\";\n\nconst getExplorerBaseUrl = (chainId?: number) => {\n switch (chainId) {\n case 1:\n return \"https://etherscan.io/tx/\";\n case 10:\n return \"https://optimistic.etherscan.io/tx/\";\n case 56:\n return \"https://bscscan.com/tx/\";\n case 137:\n return \"https://polygonscan.com/tx/\";\n case 8453:\n return \"https://basescan.org/tx/\";\n case 42161:\n return \"https://arbiscan.io/tx/\";\n case 43114:\n return \"https://snowtrace.io/tx/\";\n default:\n return undefined;\n }\n};\n\nconst getOnrampExplorerUrl = (\n session: OnrampSessionResponse,\n opportunity?: NexusWidgetDepositOpportunityConfig,\n token?: SwapTokenOption\n) => {\n if (session.deposit?.explorerUrl) return session.deposit.explorerUrl;\n if (opportunity?.explorerUrl) return opportunity.explorerUrl;\n const txHash = session.deposit?.txHash ?? session.transaction?.txHash;\n const baseUrl = getExplorerBaseUrl(token?.chainId ?? opportunity?.chainId);\n return txHash && baseUrl ? `${baseUrl}${txHash}` : undefined;\n};\n\nfunction OnrampStatusArtwork({ tone = \"neutral\" }: { tone?: \"green\" | \"neutral\" }) {\n const pulseColor =\n tone === \"green\"\n ? \"rgba(67, 190, 162, 0.72)\"\n : \"rgba(115, 129, 148, 0.58)\";\n return (\n \n \n {Array.from({ length: 18 }).map((_, index) => (\n \n ))}\n \n \n );\n}\n\nfunction SafeCloseNotice() {\n return (\n \n \n Safe to close - we'll notify you when complete\n \n );\n}\n\nfunction OnrampStatusButton({\n children,\n onClick,\n primaryButtonForeground,\n variant = \"primary\",\n}: {\n children: React.ReactNode;\n onClick: () => void;\n primaryButtonForeground: string;\n variant?: \"primary\" | \"secondary\";\n}) {\n const isPrimary = variant === \"primary\";\n return (\n \n {children}\n \n );\n}\n\nfunction WarningStatusIcon({ tone = \"red\" }: { tone?: \"orange\" | \"red\" }) {\n const color = tone === \"orange\" ? \"#F59E0B\" : \"#E8453C\";\n const backgroundColor = tone === \"orange\" ? \"#FFF7ED\" : \"#FEF2F2\";\n return (\n \n \n \n \n \n \n );\n}\n\nfunction OnrampActionStatusPanel({\n description,\n onPrimary,\n onSecondary,\n primaryButtonForeground,\n primaryLabel,\n secondaryLabel,\n title,\n}: {\n description: string;\n onPrimary: () => void;\n onSecondary: () => void;\n primaryButtonForeground: string;\n primaryLabel: string;\n secondaryLabel: string;\n title: string;\n}) {\n return (\n \n \n
\n
\n {title}\n
\n
\n {description}\n
\n
\n \n \n {primaryLabel}\n \n \n {secondaryLabel}\n \n \n \n );\n}\n\nfunction TimelineMarker({\n isComplete,\n isLast,\n isPending,\n}: {\n isComplete?: boolean;\n isLast?: boolean;\n isPending?: boolean;\n}) {\n return (\n \n \n {isComplete ? (\n \n ) : (\n \n )}\n \n {!isLast && (\n \n )}\n \n );\n}\n\nconst primaryButtonContrastColor = \"#FFFFFE\";\n\nfunction TimelineStep({\n isComplete,\n isLast,\n isPending,\n subtitle,\n title,\n}: {\n isComplete?: boolean;\n isLast?: boolean;\n isPending?: boolean;\n subtitle: string;\n title: string;\n}) {\n return (\n \n \n \n \n {title}\n \n \n {subtitle}\n \n \n \n );\n}\n\nfunction OnrampProcessingTimelinePanel({\n destinationAmount,\n destinationChainName,\n destinationSymbol,\n isSettling,\n provider,\n sourceAmount,\n sourceCurrencyCode,\n targetLabel,\n}: {\n destinationAmount: string;\n destinationChainName: string;\n destinationSymbol?: string;\n isSettling: boolean;\n provider?: string;\n sourceAmount: string;\n sourceCurrencyCode: string;\n targetLabel: string;\n}) {\n const destinationDisplay = formatTokenAmountDisplay(\n destinationAmount,\n destinationSymbol\n );\n const sourceDisplay = formatCurrencyAmount(sourceAmount, sourceCurrencyCode);\n return (\n <>\n \n Usually takes 2 - 5 min\n \n \n \n \n \n \n {isSettling\n ? \"Sending amount to your wallet\"\n : \"Processing your payment\"}\n \n \n {isSettling ? destinationDisplay : sourceDisplay}\n \n \n \n \n \n \n \n \n \n \n \n \n );\n}\n\nfunction OnrampCompletingDepositPanel({\n destinationAmount,\n destinationSymbol,\n targetLabel,\n}: {\n destinationAmount: string;\n destinationSymbol?: string;\n targetLabel: string;\n}) {\n return (\n <>\n \n Usually takes 20 seconds\n \n \n \n \n \n Depositing to {targetLabel}\n \n \n {formatTokenAmountDisplay(destinationAmount, destinationSymbol)}\n \n \n \n \n \n \n );\n}\n\nfunction OnrampSuccessPanel({\n destinationAmount,\n destinationSymbol,\n explorerUrl,\n onDone,\n paymentMethod,\n primaryButtonForeground,\n provider,\n sourceAmount,\n sourceCurrencyCode,\n subtitle,\n}: {\n destinationAmount: string;\n destinationSymbol?: string;\n explorerUrl?: string;\n onDone: () => void;\n paymentMethod?: string;\n primaryButtonForeground: string;\n provider?: string;\n sourceAmount: string;\n sourceCurrencyCode: string;\n subtitle: string;\n}) {\n return (\n <>\n \n {subtitle}\n \n \n \n
Transaction Summary
\n \n \n \n \n \n \n \n {explorerUrl && (\n \n \n View Explorer\n \n \n )}\n \n Done\n \n \n );\n}\n\nfunction SummaryRow({ label, value }: { label: string; value: string }) {\n return (\n \n \n {label}\n \n \n {value}\n \n \n );\n}\n\nfunction OnrampHandoffPanel({\n provider,\n summaryAmount,\n}: {\n provider?: string;\n summaryAmount: string;\n}) {\n return (\n <>\n \n The payment processor requires some information\n \n \n \n \n \n {getOnrampSummaryLabel(\"AWAITING_USER\", provider)}\n \n \n {summaryAmount}\n \n \n \n \n \n );\n}\n\nfunction OnrampSessionStatusPanel({\n onCancel,\n onDone,\n onRetryPayment,\n opportunity,\n primaryButtonForeground,\n quote,\n session,\n sourceAmount,\n sourceCurrencyCode,\n toToken,\n}: {\n onCancel: () => void;\n onDone: () => void;\n onRetryPayment: () => void;\n opportunity?: NexusWidgetDepositOpportunityConfig;\n primaryButtonForeground: string;\n quote?: OnrampQuote;\n session: OnrampSessionResponse;\n sourceAmount: string;\n sourceCurrencyCode: string;\n toToken?: SwapTokenOption;\n}) {\n const normalizedState =\n getNormalizedOnrampState(session.state) || \"AWAITING_USER\";\n const provider = session.provider ?? quote?.provider;\n const transaction = session.transaction;\n const destinationSymbol = toToken?.symbol ?? quote?.destinationCurrencyCode;\n const destinationAmount =\n transaction?.destinationAmount ?? quote?.destinationAmount ?? \"\";\n const sourceDisplayAmount =\n transaction?.sourceAmount ?? quote?.sourceAmount ?? sourceAmount;\n const sourceDisplayCurrency =\n transaction?.sourceCurrencyCode ??\n quote?.sourceCurrencyCode ??\n sourceCurrencyCode;\n const destinationChainName = getDepositChainName(opportunity, toToken);\n const targetLabel = getDepositTargetLabel(opportunity);\n const explorerUrl = getOnrampExplorerUrl(session, opportunity, toToken);\n const containerStyle: React.CSSProperties = {\n boxSizing: \"border-box\",\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"12px\",\n width: \"100%\",\n };\n\n if (normalizedState === \"FAILED\") {\n return (\n
\n \n
\n );\n }\n\n if ([\"CANCELLED\", \"EXPIRED\", \"REFUNDED\"].includes(normalizedState)) {\n const title =\n normalizedState === \"CANCELLED\"\n ? \"Payment cancelled\"\n : normalizedState === \"REFUNDED\"\n ? \"Payment refunded\"\n : \"Payment expired\";\n const description =\n normalizedState === \"CANCELLED\"\n ? \"No payment was completed.\"\n : normalizedState === \"REFUNDED\"\n ? `${getProviderLabel(provider)} refunded the payment.`\n : \"Start a new local currency payment to continue.\";\n return (\n
\n \n
\n );\n }\n\n if (isOnrampDepositFailedState(normalizedState)) {\n return (\n
\n \n
\n );\n }\n\n if (normalizedState === \"SETTLED\" || isOnrampDepositSuccessState(normalizedState)) {\n return (\n
\n \n
\n );\n }\n\n if (isOnrampDepositProcessingState(normalizedState)) {\n return (\n
\n \n
\n );\n }\n\n if (isOnrampProcessingState(normalizedState)) {\n return (\n
\n \n
\n );\n }\n\n return (\n
\n \n
\n );\n}\n\nexport function DepositOnrampFlow({\n baseUrl = ONRAMP_DEFAULT_BASE_URL,\n destinationTokens,\n onConnectWallet,\n onError,\n onSelectDestinationToken,\n onSessionStateChange,\n ownerAddress,\n opportunity,\n primaryButtonForeground,\n toToken,\n}: DepositOnrampFlowProps) {\n const [countryCode] = React.useState(ONRAMP_DEFAULT_COUNTRY);\n const [sourceCurrencyCode, setSourceCurrencyCode] = React.useState(\"INR\");\n const [sourceAmount, setSourceAmount] = React.useState(\"\");\n const [options, setOptions] = React.useState(\n null\n );\n const [routes, setRoutes] = React.useState([]);\n const [selectedPaymentMethod, setSelectedPaymentMethod] =\n React.useState(\"\");\n const [quotes, setQuotes] = React.useState([]);\n const [selectedProvider, setSelectedProvider] = React.useState(\"\");\n const [activeSheet, setActiveSheet] = React.useState(null);\n const [currencySearch, setCurrencySearch] = React.useState(\"\");\n const [destinationSearch, setDestinationSearch] = React.useState(\"\");\n const [methodSearch, setMethodSearch] = React.useState(\"\");\n const [partnerSearch, setPartnerSearch] = React.useState(\"\");\n const [optionsLoading, setOptionsLoading] = React.useState(false);\n const [routesLoading, setRoutesLoading] = React.useState(false);\n const [quotesLoading, setQuotesLoading] = React.useState(false);\n const [sessionLoading, setSessionLoading] = React.useState(false);\n const [error, setError] = React.useState(null);\n const [session, setSession] = React.useState(\n null\n );\n const [quoteRefreshSeconds, setQuoteRefreshSeconds] = React.useState(\n QUOTE_REFRESH_SECONDS\n );\n const quoteRunIdRef = React.useRef(0);\n const normalizedSessionState = getNormalizedOnrampState(session?.state);\n\n const fiatCurrencies = TEST_FIAT_CURRENCIES;\n const availableDestinationTokens = React.useMemo(() => {\n const tokens = destinationTokens?.length ? destinationTokens : toToken ? [toToken] : [];\n const byKey = new Map();\n for (const token of tokens) {\n const key = getOnrampTokenKey(token);\n if (!key) continue;\n byKey.set(key, token);\n }\n return Array.from(byKey.values());\n }, [destinationTokens, toToken]);\n const destinationCurrencyCode = React.useMemo(\n () => getDestinationCurrencyCode(options, toToken),\n [options, toToken]\n );\n const availablePaymentMethods = React.useMemo(() => {\n const map = new Map();\n for (const route of routes) {\n for (const method of route.paymentMethods ?? []) {\n if (!method.method || map.has(method.method)) continue;\n map.set(method.method, method);\n }\n }\n return Array.from(map.values());\n }, [routes]);\n const selectedPaymentMethodDetails = availablePaymentMethods.find(\n (method) => method.method === selectedPaymentMethod\n );\n const selectedQuote =\n quotes.find((quote) => quote.provider === selectedProvider) ?? quotes[0];\n const filteredFiatCurrencies = React.useMemo(\n () =>\n fiatCurrencies.filter((currencyCode) =>\n matchesSearch(currencySearch, [\n currencyCode,\n getFiatCurrencyName(currencyCode),\n ])\n ),\n [currencySearch, fiatCurrencies]\n );\n const filteredDestinationTokens = React.useMemo(\n () =>\n availableDestinationTokens.filter((token) =>\n matchesSearch(destinationSearch, [\n token.symbol,\n token.name,\n token.chainName,\n token.contractAddress,\n ])\n ),\n [availableDestinationTokens, destinationSearch]\n );\n const filteredPaymentMethods = React.useMemo(\n () =>\n availablePaymentMethods.filter((method) =>\n matchesSearch(methodSearch, [\n method.method,\n getMethodLabel(method.method),\n getMethodSubtitle(method.method),\n ])\n ),\n [availablePaymentMethods, methodSearch]\n );\n const filteredQuotes = React.useMemo(\n () =>\n quotes.filter((quote) =>\n matchesSearch(partnerSearch, [\n quote.provider,\n getProviderLabel(quote.provider),\n quote.paymentMethodType,\n getMethodLabel(quote.paymentMethodType),\n ])\n ),\n [partnerSearch, quotes]\n );\n const hasMultipleCurrencies = fiatCurrencies.length > 1;\n const hasMultipleDestinationTokens = availableDestinationTokens.length > 1;\n const hasMultipleMethods = availablePaymentMethods.length > 1;\n const hasMultipleProviders = quotes.length > 1;\n const isLoading = optionsLoading || routesLoading || quotesLoading;\n const parsedSourceAmount = React.useMemo(\n () => parseDecimal(sourceAmount),\n [sourceAmount]\n );\n const hasPositiveSourceAmount = Boolean(parsedSourceAmount?.gt(0));\n const amountLimitMessage = (() => {\n const limits = selectedPaymentMethodDetails?.limits;\n if (!limits || !parsedSourceAmount?.gt(0)) return null;\n const min = parseDecimal(limits.min);\n const max = parseDecimal(limits.max);\n if (min && parsedSourceAmount.lt(min)) {\n return `Minimum ${formatCurrencyAmount(min, sourceCurrencyCode)}`;\n }\n if (max && parsedSourceAmount.gt(max)) {\n return `Maximum ${formatCurrencyAmount(max, sourceCurrencyCode)}`;\n }\n return null;\n })();\n\n const loadOptions = React.useCallback(async () => {\n setOptionsLoading(true);\n setError(null);\n try {\n const data = await fetchOnrampJson(\n baseUrl,\n `/api/v1/onramp/options?countryCode=${encodeURIComponent(countryCode)}`,\n { method: \"GET\" }\n );\n setOptions(data);\n setSourceCurrencyCode((current) => current || \"INR\");\n } catch (requestError) {\n const message = getErrorMessage(requestError);\n setError(message);\n onError?.(message);\n } finally {\n setOptionsLoading(false);\n }\n }, [baseUrl, countryCode, onError]);\n\n React.useEffect(() => {\n void loadOptions();\n }, [loadOptions]);\n\n React.useEffect(() => {\n if (activeSheet !== \"currency\") setCurrencySearch(\"\");\n if (activeSheet !== \"destination\") setDestinationSearch(\"\");\n if (activeSheet !== \"method\") setMethodSearch(\"\");\n if (activeSheet !== \"partner\") setPartnerSearch(\"\");\n }, [activeSheet]);\n\n React.useEffect(() => {\n if (!sourceCurrencyCode || !destinationCurrencyCode) return;\n let cancelled = false;\n\n const loadRoutes = async () => {\n setRoutesLoading(true);\n setError(null);\n try {\n const params = new URLSearchParams({\n countryCode,\n destinationCurrencyCode,\n sourceCurrencyCode,\n });\n const data = await fetchOnrampJson(\n baseUrl,\n `/api/v1/onramp/routes?${params.toString()}`,\n { method: \"GET\" }\n );\n if (cancelled) return;\n const nextRoutes = data.routes ?? [];\n setRoutes(nextRoutes);\n const methods = nextRoutes.flatMap((route) => route.paymentMethods ?? []);\n const preferred =\n options?.selection?.defaultPaymentMethods?.find((method) =>\n methods.some((candidate) => candidate.method === method)\n ) ?? methods[0]?.method;\n setSelectedPaymentMethod((current) =>\n current && methods.some((method) => method.method === current)\n ? current\n : preferred ?? \"\"\n );\n } catch (requestError) {\n if (cancelled) return;\n const message = getErrorMessage(requestError);\n setError(message);\n onError?.(message);\n } finally {\n if (!cancelled) setRoutesLoading(false);\n }\n };\n\n void loadRoutes();\n return () => {\n cancelled = true;\n };\n }, [\n baseUrl,\n countryCode,\n destinationCurrencyCode,\n onError,\n options?.selection?.defaultPaymentMethods,\n sourceCurrencyCode,\n ]);\n\n const fetchQuotes = React.useCallback(async () => {\n if (\n !ownerAddress ||\n !sourceCurrencyCode ||\n !destinationCurrencyCode ||\n !selectedPaymentMethod ||\n !parsedSourceAmount?.gt(0) ||\n amountLimitMessage\n ) {\n return;\n }\n\n const runId = quoteRunIdRef.current + 1;\n quoteRunIdRef.current = runId;\n setQuotesLoading(true);\n setError(null);\n try {\n const data = await fetchOnrampJson(\n baseUrl,\n \"/api/v1/onramp/quote\",\n {\n body: JSON.stringify({\n countryCode,\n destinationCurrencyCode,\n paymentMethodType: selectedPaymentMethod,\n sourceAmount,\n sourceCurrencyCode,\n walletAddress: ownerAddress,\n }),\n method: \"POST\",\n }\n );\n if (quoteRunIdRef.current !== runId) return;\n const nextQuotes = sortQuotes(data.quotes ?? []);\n setQuotes(nextQuotes);\n setSelectedProvider((current) =>\n current && nextQuotes.some((quote) => quote.provider === current)\n ? current\n : nextQuotes[0]?.provider ?? \"\"\n );\n if (nextQuotes.length === 0) {\n setError(\"No onramp quotes are available for this amount.\");\n }\n } catch (requestError) {\n if (quoteRunIdRef.current !== runId) return;\n const message = getErrorMessage(requestError);\n setQuotes([]);\n setError(message);\n onError?.(message);\n } finally {\n if (quoteRunIdRef.current === runId) {\n setQuotesLoading(false);\n setQuoteRefreshSeconds(QUOTE_REFRESH_SECONDS);\n }\n }\n }, [\n amountLimitMessage,\n baseUrl,\n countryCode,\n destinationCurrencyCode,\n onError,\n ownerAddress,\n parsedSourceAmount,\n selectedPaymentMethod,\n sourceAmount,\n sourceCurrencyCode,\n ]);\n\n React.useEffect(() => {\n setRoutes([]);\n setSelectedPaymentMethod(\"\");\n setQuotes([]);\n setSelectedProvider(\"\");\n setSession(null);\n }, [destinationCurrencyCode]);\n\n React.useEffect(() => {\n if (session?.sessionId) return;\n setQuotes([]);\n setSelectedProvider(\"\");\n if (\n !ownerAddress ||\n !sourceCurrencyCode ||\n !destinationCurrencyCode ||\n !selectedPaymentMethod ||\n !parsedSourceAmount?.gt(0) ||\n amountLimitMessage\n ) {\n return;\n }\n const timer = window.setTimeout(() => {\n void fetchQuotes();\n }, 350);\n return () => {\n window.clearTimeout(timer);\n };\n }, [\n amountLimitMessage,\n destinationCurrencyCode,\n fetchQuotes,\n ownerAddress,\n parsedSourceAmount,\n selectedPaymentMethod,\n session?.sessionId,\n sourceCurrencyCode,\n ]);\n\n React.useEffect(() => {\n if (session?.sessionId || !selectedQuote || quotesLoading || sessionLoading) {\n return;\n }\n setQuoteRefreshSeconds(QUOTE_REFRESH_SECONDS);\n const interval = window.setInterval(() => {\n setQuoteRefreshSeconds((current) => {\n if (current <= 1) {\n void fetchQuotes();\n return QUOTE_REFRESH_SECONDS;\n }\n return current - 1;\n });\n }, 1000);\n return () => {\n window.clearInterval(interval);\n };\n }, [\n fetchQuotes,\n quotesLoading,\n selectedQuote,\n session?.sessionId,\n sessionLoading,\n ]);\n\n React.useEffect(() => {\n onSessionStateChange?.(\n session?.sessionId\n ? normalizedSessionState || \"AWAITING_USER\"\n : null\n );\n }, [\n normalizedSessionState,\n onSessionStateChange,\n session?.sessionId,\n ]);\n\n React.useEffect(\n () => () => {\n onSessionStateChange?.(null);\n },\n [onSessionStateChange]\n );\n\n React.useEffect(() => {\n const sessionId = session?.sessionId;\n if (!sessionId || isOnrampTerminalState(normalizedSessionState)) return;\n\n let cancelled = false;\n let inFlight = false;\n\n const pollSession = async () => {\n if (inFlight) return;\n inFlight = true;\n try {\n const data = await fetchOnrampJson(\n baseUrl,\n `/api/v1/onramp/sessions/${encodeURIComponent(sessionId)}`,\n { method: \"GET\" }\n );\n if (cancelled) return;\n setSession((current) =>\n current?.sessionId === sessionId ? { ...current, ...data } : current\n );\n } catch (requestError) {\n if (cancelled) return;\n const message = getErrorMessage(requestError);\n setError(message);\n onError?.(message);\n } finally {\n inFlight = false;\n }\n };\n\n void pollSession();\n const interval = window.setInterval(pollSession, ONRAMP_SESSION_POLL_MS);\n return () => {\n cancelled = true;\n window.clearInterval(interval);\n };\n }, [\n baseUrl,\n normalizedSessionState,\n onError,\n session?.sessionId,\n ]);\n\n const createSession = async () => {\n if (!selectedQuote || !ownerAddress || !selectedPaymentMethod) return;\n setSessionLoading(true);\n setError(null);\n try {\n const returnUrl =\n typeof window !== \"undefined\"\n ? window.location.href\n : \"http://localhost:8000/onramp/complete\";\n const data = await fetchOnrampJson(\n baseUrl,\n \"/api/v1/onramp/sessions\",\n {\n body: JSON.stringify({\n countryCode,\n destinationCurrencyCode: selectedQuote.destinationCurrencyCode,\n paymentMethodType: selectedPaymentMethod,\n provider: selectedQuote.provider,\n returnUrl,\n sourceAmount: selectedQuote.sourceAmount,\n sourceCurrencyCode: selectedQuote.sourceCurrencyCode,\n walletAddress: ownerAddress,\n }),\n headers: {\n \"Idempotency-Key\": createIdempotencyKey(),\n },\n method: \"POST\",\n }\n );\n setSession(data);\n const widgetUrl = data.widgetUrl ?? data.fallbackWidgetUrl;\n if (widgetUrl && typeof window !== \"undefined\") {\n const opened = window.open(widgetUrl, \"_blank\", \"noopener,noreferrer\");\n if (!opened) {\n window.location.assign(widgetUrl);\n }\n }\n } catch (requestError) {\n const message = getErrorMessage(requestError);\n setError(message);\n onError?.(message);\n } finally {\n setSessionLoading(false);\n }\n };\n\n const resetSession = () => {\n setError(null);\n setSession(null);\n };\n\n const completeSession = () => {\n setError(null);\n setQuotes([]);\n setSelectedProvider(\"\");\n setSession(null);\n setSourceAmount(\"\");\n };\n\n const handleCurrencySelect = (currencyCode: string) => {\n setSourceCurrencyCode(currencyCode);\n setSelectedPaymentMethod(\"\");\n setSelectedProvider(\"\");\n setQuotes([]);\n setRoutes([]);\n setActiveSheet(null);\n };\n\n const handleDestinationTokenSelect = (token: SwapTokenOption) => {\n if (!isSameOnrampToken(token, toToken)) {\n onSelectDestinationToken?.(token);\n setSelectedPaymentMethod(\"\");\n setSelectedProvider(\"\");\n setQuotes([]);\n setRoutes([]);\n setSession(null);\n }\n setActiveSheet(null);\n };\n\n const handleMethodSelect = (method: string) => {\n setSelectedPaymentMethod(method);\n setSelectedProvider(\"\");\n setQuotes([]);\n setActiveSheet(null);\n };\n\n const receiveAmount = selectedQuote?.destinationAmount;\n const receiveUsd = selectedQuote?.destinationAmount;\n const feeTotal = selectedQuote?.fees?.total;\n const fetchingBestRates =\n hasPositiveSourceAmount &&\n !selectedQuote &&\n !amountLimitMessage &&\n !error &&\n Boolean(destinationCurrencyCode) &&\n (isLoading || Boolean(selectedPaymentMethod));\n const ctaRateLoading = fetchingBestRates || (hasPositiveSourceAmount && isLoading);\n const quoteDetailsLoading = fetchingBestRates;\n const shouldShowQuoteDetails =\n hasPositiveSourceAmount && (quoteDetailsLoading || Boolean(selectedQuote));\n const shouldShowQuoteTimer = Boolean(selectedQuote);\n const ctaDisabled =\n !ownerAddress ||\n !hasPositiveSourceAmount ||\n !selectedQuote ||\n Boolean(amountLimitMessage) ||\n ctaRateLoading ||\n sessionLoading;\n\n if (!ownerAddress) {\n return (\n
\n \n \n Connect wallet to continue\n
\n \n Your wallet address is required for the hosted onramp session.\n \n void onConnectWallet()}\n style={{\n backgroundColor: brand,\n border: \"none\",\n borderRadius: theme.radius.primaryButton,\n color: primaryButtonForeground,\n cursor: \"pointer\",\n fontFamily: theme.fonts.sans,\n fontSize: \"14px\",\n fontWeight: 500,\n height: \"40px\",\n paddingInline: \"16px\",\n }}\n type=\"button\"\n >\n Connect Wallet\n \n \n \n );\n }\n\n if (session?.sessionId) {\n return (\n void createSession()}\n opportunity={opportunity}\n primaryButtonForeground={primaryButtonForeground}\n quote={selectedQuote}\n session={session}\n sourceAmount={sourceAmount}\n sourceCurrencyCode={sourceCurrencyCode}\n toToken={toToken}\n />\n );\n }\n\n return (\n \n {shouldShowQuoteTimer && (\n \n \n {quotesLoading || routesLoading ? \"...\" : `${quoteRefreshSeconds}s`}\n \n )}\n\n
\n \n \n \n
You Pay
\n \n setSourceAmount(normalizeAmountInput(event.target.value))\n }\n placeholder=\"0\"\n style={{\n background: \"transparent\",\n border: \"none\",\n color: theme.colors.textStrong,\n fontFamily: theme.fonts.display,\n fontSize: \"31px\",\n fontWeight: 500,\n letterSpacing: \"0\",\n lineHeight: \"36px\",\n outline: \"none\",\n padding: 0,\n width: \"100%\",\n }}\n value={sourceAmount}\n />\n \n {selectedQuote\n ? `${formatUsdDisplay(sourceAmount)} USD`\n : hasPositiveSourceAmount\n ? `${formatUsdDisplay(sourceAmount)} USD`\n : \"Select currency\"}\n
\n \n setActiveSheet(\"currency\")}\n >\n \n \n {sourceCurrencyCode || \"---\"}\n \n \n \n\n {selectedPaymentMethodDetails?.limits && (\n \n \n \n Limits\n {selectedPaymentMethodDetails.limits.min\n ? ` · Min ${formatCurrencyAmount(\n selectedPaymentMethodDetails.limits.min,\n sourceCurrencyCode\n )}`\n : \"\"}\n {selectedPaymentMethodDetails.limits.max\n ? ` · Max ${formatCurrencyAmount(\n selectedPaymentMethodDetails.limits.max,\n sourceCurrencyCode\n )}`\n : \"\"}\n \n \n )}\n\n \n\n \n \n
Receive
\n \n {receiveAmount ? formatNumberDisplay(receiveAmount, 6) : \"0\"}\n \n \n {receiveAmount ? formatUsdDisplay(receiveUsd) : \"$0.00 USD\"}\n \n \n setActiveSheet(\"destination\")}\n >\n \n \n {toToken?.symbol ?? \"Token\"}\n \n \n \n \n \n\n {shouldShowQuoteDetails &&\n (quoteDetailsLoading ? (\n \n ) : selectedQuote ? (\n
\n setActiveSheet(\"method\")}\n />\n }\n label=\"Payment Method\"\n >\n
\n \n \n {getMethodLabel(selectedPaymentMethod)}\n
\n
\n \n\n setActiveSheet(\"partner\")}\n />\n }\n divider\n label=\"Payment Partner\"\n >\n
\n \n \n \n {getProviderLabel(selectedQuote.provider)}\n
\n \n Best available quote\n \n \n \n \n\n setActiveSheet(\"fees\")}\n style={{\n alignItems: \"center\",\n backgroundColor: \"transparent\",\n border: \"none\",\n borderTop: `1px solid ${theme.colors.divider}`,\n boxSizing: \"border-box\",\n cursor: \"pointer\",\n display: \"flex\",\n justifyContent: \"space-between\",\n padding: \"14px\",\n textAlign: \"left\",\n width: \"100%\",\n }}\n type=\"button\"\n >\n
\n \n Total Fees\n
\n \n Inclusive of fees\n \n \n \n \n {feeTotal\n ? formatCurrencyAmount(feeTotal, sourceCurrencyCode)\n : \"--\"}\n \n \n View breakdown\n \n \n \n \n ) : null)}\n\n {(error || amountLimitMessage || session?.state) && (\n \n {error ??\n amountLimitMessage ??\n `Onramp session ${session?.state?.toLowerCase() ?? \"created\"}.`}\n \n )}\n\n void createSession()}\n style={{\n alignItems: \"center\",\n backgroundColor: ctaDisabled ? theme.colors.surfaceCool : brand,\n border: \"none\",\n borderRadius: theme.radius.primaryButton,\n boxShadow: ctaDisabled ? \"none\" : theme.shadows.primaryButton,\n color: ctaDisabled ? theme.colors.muted : primaryButtonForeground,\n cursor: ctaDisabled ? \"default\" : \"pointer\",\n display: \"flex\",\n fontFamily: theme.fonts.sans,\n fontSize: \"14px\",\n fontWeight: 500,\n gap: \"8px\",\n height: \"44px\",\n justifyContent: \"center\",\n lineHeight: \"18px\",\n width: \"100%\",\n }}\n type=\"button\"\n >\n {sessionLoading || ctaRateLoading ? (\n \n ) : selectedQuote ? (\n \n ) : null}\n {sessionLoading\n ? \"Opening provider...\"\n : ctaRateLoading\n ? \"fetching best rates...\"\n : selectedQuote\n ? `Pay ${formatCurrencyAmount(sourceAmount, sourceCurrencyCode)}`\n : \"Enter amount\"}\n \n\n {activeSheet === \"fees\" && (\n setActiveSheet(null)} title={`Buying ${formatNumberDisplay(receiveAmount, 6)} ${toToken?.symbol ?? \"\"}`}>\n \n 1 {toToken?.symbol ?? \"token\"} ≈{\" \"}\n {selectedQuote && parseDecimal(selectedQuote.destinationAmount)?.gt(0)\n ? formatCurrencyAmount(\n parseDecimal(selectedQuote.sourceAmount)?.div(\n parseDecimal(selectedQuote.destinationAmount) ?? new Decimal(1)\n ),\n sourceCurrencyCode\n )\n : \"--\"}\n \n
\n {[\n [\"Provider fee\", selectedQuote?.fees?.provider],\n [\"Network fee\", selectedQuote?.fees?.network],\n [\"Partner fee\", selectedQuote?.fees?.partner],\n ].map(([label, value], index) => (\n 0 ? `1px solid ${theme.colors.divider}` : undefined,\n display: \"flex\",\n justifyContent: \"space-between\",\n padding: \"12px 14px\",\n }}\n >\n \n {label}\n \n \n {value ? formatCurrencyAmount(value, sourceCurrencyCode) : \"--\"}\n \n
\n ))}\n \n \n Total you pay\n \n \n {formatCurrencyAmount(sourceAmount, sourceCurrencyCode)}\n \n \n \n
\n )}\n\n {activeSheet === \"partner\" && (\n setActiveSheet(null)} title=\"Payment partner\">\n \n {filteredQuotes.length > 0 ? (\n
\n {filteredQuotes.map((quote) => {\n const quoteIndex = quotes.findIndex(\n (candidate) =>\n candidate.provider === quote.provider &&\n candidate.paymentMethodType === quote.paymentMethodType\n );\n return (\n }\n key={`${quote.provider}-${quote.paymentMethodType}`}\n onClick={() => {\n setSelectedProvider(quote.provider);\n setActiveSheet(null);\n }}\n primary={quoteIndex === 0}\n selected={quote.provider === selectedQuote?.provider}\n subtitle={`${getMethodLabel(quote.paymentMethodType)} · ${getMethodSubtitle(quote.paymentMethodType)}`}\n title={getProviderLabel(quote.provider)}\n value={`${formatNumberDisplay(quote.destinationAmount, 6)} ${toToken?.symbol ?? \"\"}`}\n />\n );\n })}\n
\n ) : (\n No payment partners found.\n )}\n
\n )}\n\n {activeSheet === \"currency\" && (\n setActiveSheet(null)} title=\"Select currency\">\n \n
Fiat currencies
\n {filteredFiatCurrencies.length > 0 ? (\n
\n {filteredFiatCurrencies.map((currencyCode) => (\n }\n key={currencyCode}\n onClick={() => handleCurrencySelect(currencyCode)}\n selected={currencyCode === sourceCurrencyCode}\n subtitle={getFiatCurrencyName(currencyCode)}\n title={currencyCode}\n />\n ))}\n
\n ) : (\n No currencies found.\n )}\n
\n )}\n\n {activeSheet === \"method\" && (\n setActiveSheet(null)} title=\"Payment method\">\n \n {filteredPaymentMethods.length > 0 ? (\n
\n {filteredPaymentMethods.map((method) => (\n }\n key={method.method}\n onClick={() => handleMethodSelect(method.method)}\n selected={method.method === selectedPaymentMethod}\n subtitle={getMethodSubtitle(method.method)}\n title={getMethodLabel(method.method)}\n />\n ))}\n
\n ) : (\n No payment methods found.\n )}\n
\n )}\n\n {activeSheet === \"destination\" && (\n setActiveSheet(null)} title=\"Select token\">\n \n {filteredDestinationTokens.length > 0 ? (\n
\n {filteredDestinationTokens.map((token) => (\n }\n key={getOnrampTokenKey(token)}\n onClick={() => handleDestinationTokenSelect(token)}\n selected={isSameOnrampToken(token, toToken)}\n subtitle={token.chainName}\n title={token.symbol}\n value={token.balanceInFiat}\n />\n ))}\n
\n ) : (\n No tokens found.\n )}\n
\n )}\n \n );\n}\n", + "type": "registry:component", + "target": "components/nexus/components/deposit-onramp-flow.tsx" + }, { "path": "registry/avail-widgets/nexus-widget/components/nexus-widget-progress-screen.tsx", "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\n\"use client\";\n\nimport Decimal from \"decimal.js\";\nimport { Check, ChevronDown, Loader2, X } from \"lucide-react\";\nimport React, { useEffect, useState } from \"react\";\nimport type {\n BridgeStepType,\n SwapStepType,\n} from \"../../common/types/transaction-flow\";\nimport { getShortChainName } from \"../../common/utils/constant\";\nimport {\n type NexusWidgetDepositOpportunityMetadata,\n type NexusWidgetMode,\n} from \"../types\";\nimport { NEXUS_WIDGET_FAST_SPINNER_STYLE } from \"../theme\";\nimport { type SwapTokenOption } from \"./swap-asset-selector\";\nimport { type SwapIntentData } from \"./swap-intent-preview\";\n\ntype ProgressSdkStep = SwapStepType | BridgeStepType;\n\ntype ProgressStep = {\n id: number;\n completed: boolean;\n step: ProgressSdkStep;\n};\n\nexport type NexusWidgetProgressEvent = {\n id: string;\n name: string;\n completed: boolean;\n event?: unknown;\n step?: ProgressSdkStep;\n steps?: ProgressSdkStep[];\n};\n\ninterface NexusWidgetProgressScreenProps {\n failedStep?: ProgressSdkStep | null;\n fromAmountUsd?: string;\n fromTokens?: SwapTokenOption[];\n intentData?: SwapIntentData | null;\n mode: NexusWidgetMode;\n opportunity?: NexusWidgetDepositOpportunityMetadata;\n progressEvents?: NexusWidgetProgressEvent[];\n recipientAddress?: string;\n steps?: ProgressStep[];\n toAmount?: string;\n toAmountUsd?: string;\n toToken?: SwapTokenOption;\n totalFeeUsd?: string;\n}\n\nconst fontFamily = '\"Geist\", var(--font-geist-sans), system-ui, sans-serif';\nconst primary = \"var(--foreground-primary, #161615)\";\nconst muted = \"var(--foreground-muted, #848483)\";\nconst border = \"var(--border-default, #E8E8E7)\";\nconst brand = \"var(--foreground-brand)\";\nconst danger = \"var(--foreground-negative, #E92C2C)\";\n\nconst parseDecimal = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = String(value).replace(/[^0-9.-]/g, \"\");\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst formatDecimal = (value: unknown, decimals = 2) =>\n (parseDecimal(value) ?? new Decimal(0)).toDecimalPlaces(decimals).toFixed();\n\nconst formatUsd = (value: unknown) => `$${formatDecimal(value, 2)}`;\n\nconst unique = (values: Array) =>\n Array.from(new Set(values.filter(Boolean) as string[]));\n\nconst formatSymbolSummary = (symbols: string[]) => {\n if (symbols.length <= 2) return symbols.join(\", \");\n return `${symbols.slice(0, 2).join(\", \")} and ${symbols.length - 2} others`;\n};\n\nconst getStepType = (step?: ProgressSdkStep) =>\n String((step as any)?.type ?? (step as any)?.typeID ?? \"\").toUpperCase();\n\ntype ProgressStatusId =\n | \"approveTokens\"\n | \"swapTokens\"\n | \"receiveToken\"\n | \"action\";\n\ntype ProgressStatusState =\n | \"default\"\n | \"preapproval\"\n | \"inProgress\"\n | \"completed\"\n | \"error\";\n\ntype ProgressStatusRow = {\n id: ProgressStatusId;\n label: string;\n description?: string;\n state: ProgressStatusState;\n};\n\nconst PROGRESS_EVENT_NAMES = {\n BRIDGE_PLAN_LIST: \"bridge_plan_list\",\n BRIDGE_PLAN_PROGRESS: \"bridge_plan_progress\",\n SWAP_PLAN_LIST: \"swap_plan_list\",\n SWAP_PLAN_PROGRESS: \"swap_plan_progress\",\n} as const;\n\ntype ProgressListEventName =\n | typeof PROGRESS_EVENT_NAMES.BRIDGE_PLAN_LIST\n | typeof PROGRESS_EVENT_NAMES.SWAP_PLAN_LIST;\n\nconst STATUS_ORDER: ProgressStatusId[] = [\n \"approveTokens\",\n \"swapTokens\",\n \"receiveToken\",\n \"action\",\n];\n\nconst SWAP_APPROVAL_TYPES = [\n \"SOURCE_SWAP\",\n \"CREATE_PERMIT_EOA_TO_EPHEMERAL\",\n \"CREATE_PERMIT_FOR_SOURCE_SWAP\",\n \"EOA_TO_EPHEMERAL_TRANSFER\",\n \"EOA_EXECUTE_CALL\",\n // \"BRIDGE_DEPOSIT\",\n];\n\nconst REFUND_ELIGIBLE_SWAP_TYPES = [\n \"BRIDGE_INTENT_SUBMISSION\",\n \"BRIDGE_DEPOSIT\",\n];\n\nconst DESTINATION_SWAP_TYPES = [\n \"DESTINATION_SWAP\",\n \"DESTINATION_SWAP_BATCH_TX\",\n \"DESTINATION_SWAP_HASH\",\n];\nconst BRIDGE_FILL_RECEIVE_TYPES = [\"BRIDGE_FILL\"];\n\nconst getStatusForStep = (\n step: ProgressSdkStep | undefined,\n mode: NexusWidgetMode,\n hasTransferAction = false,\n): ProgressStatusId | null => {\n const type = getStepType(step);\n\n if (\n type === \"APPROVAL\" ||\n type === \"TRANSACTION_SENT\" ||\n type === \"TRANSACTION_CONFIRMED\"\n ) {\n return mode === \"swap\" && !hasTransferAction ? null : \"action\";\n }\n\n if (type.includes(\"SWAP_START\")) {\n return \"swapTokens\";\n }\n\n if (SWAP_APPROVAL_TYPES.some((token) => type.includes(token))) {\n return \"approveTokens\";\n }\n\n if (\n type.includes(\"SOURCE_SWAP\") ||\n type.includes(\"SOURCE_BATCH\") ||\n type.includes(\"SWAP_SOURCE\") ||\n type.includes(\"BRIDGE_DEPOSIT\") ||\n type.includes(\"BRIDGE_FILL\") ||\n type.includes(\"BRIDGE_INTENT_SUBMISSION\") ||\n type.includes(\"SWAP_COMPLETE\") ||\n type.includes(\"SWAP_SKIPPED\")\n ) {\n return \"swapTokens\";\n }\n\n if (type.includes(\"DESTINATION_SWAP\") || type.includes(\"DESTINATION_BATCH\")) {\n return \"receiveToken\";\n }\n\n return null;\n};\n\nconst stepMatches = (step: ProgressSdkStep | undefined, tokens: string[]) => {\n const type = getStepType(step);\n return tokens.some((token) => type.includes(token));\n};\n\nconst hasCompletedType = (\n events: NexusWidgetProgressEvent[],\n steps: ProgressStep[],\n tokens: string[],\n) => {\n const completedEvent = events.some(\n (event) => event.completed && stepMatches(event.step, tokens),\n );\n if (completedEvent) return true;\n\n return steps.some((item) => item.completed && stepMatches(item.step, tokens));\n};\n\nconst hasStepType = (\n events: NexusWidgetProgressEvent[],\n steps: ProgressStep[],\n tokens: string[],\n) =>\n events.some(\n (event) =>\n stepMatches(event.step, tokens) ||\n (event.steps ?? []).some((step) => stepMatches(step, tokens)),\n ) || steps.some((item) => stepMatches(item.step, tokens));\n\nconst hasEventType = (events: NexusWidgetProgressEvent[], tokens: string[]) =>\n events.some(\n (event) =>\n stepMatches(event.step, tokens) ||\n (event.steps ?? []).some((step) => stepMatches(step, tokens)),\n );\n\nconst hasProgressEventType = (\n events: NexusWidgetProgressEvent[],\n tokens: string[],\n) =>\n events.some(\n (event) =>\n (event.name === PROGRESS_EVENT_NAMES.BRIDGE_PLAN_PROGRESS ||\n event.name === PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS) &&\n stepMatches(event.step, tokens),\n );\n\nconst getListedSteps = (\n events: NexusWidgetProgressEvent[],\n eventName: ProgressListEventName,\n) => {\n const listEvent = [...events]\n .reverse()\n .find(\n (event) => event.name === eventName && (event.steps?.length ?? 0) > 0,\n );\n return listEvent?.steps ?? [];\n};\n\nconst countListedSteps = (steps: ProgressSdkStep[], tokens: string[]) =>\n steps.filter((step) => stepMatches(step, tokens)).length;\n\ntype ApprovalUnit = {\n symbol?: string;\n};\n\nconst getStepSwaps = (step?: ProgressSdkStep) => {\n const swaps = (step as any)?.swaps;\n return Array.isArray(swaps) ? swaps : [];\n};\n\nconst getApprovalUnitsForStep = (step?: ProgressSdkStep): ApprovalUnit[] => {\n if (!stepMatches(step, SWAP_APPROVAL_TYPES)) return [];\n\n const swaps = getStepSwaps(step);\n if (swaps.length > 0) {\n return swaps.map((swap) => ({\n symbol:\n typeof swap?.input?.symbol === \"string\" ? swap.input.symbol : undefined,\n }));\n }\n\n const assetSymbol = (step as any)?.asset?.symbol;\n const tokenSymbol = (step as any)?.token?.symbol;\n return [\n {\n symbol:\n typeof assetSymbol === \"string\"\n ? assetSymbol\n : typeof tokenSymbol === \"string\"\n ? tokenSymbol\n : undefined,\n },\n ];\n};\n\nconst countApprovalUnits = (steps: ProgressSdkStep[]) =>\n steps.reduce((sum, step) => sum + getApprovalUnitsForStep(step).length, 0);\n\nconst countCompletedApprovalUnitsFromEvents = (\n events: NexusWidgetProgressEvent[],\n) =>\n events.reduce((sum, event) => {\n if (\n event.name !== PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS ||\n !event.completed\n ) {\n return sum;\n }\n return sum + getApprovalUnitsForStep(event.step).length;\n }, 0);\n\nconst countCompletedApprovalUnitsFromSteps = (steps: ProgressStep[]) =>\n steps.reduce(\n (sum, item) =>\n item.completed ? sum + getApprovalUnitsForStep(item.step).length : sum,\n 0,\n );\n\nconst getApprovalUnitSymbols = (steps: ProgressSdkStep[]) =>\n steps\n .flatMap((step) => getApprovalUnitsForStep(step))\n .map((unit) => unit.symbol)\n .filter(Boolean) as string[];\n\nconst getNumericEventIndex = (...values: unknown[]) => {\n for (const value of values) {\n if (typeof value === \"number\" && Number.isInteger(value) && value >= 0) {\n return value;\n }\n if (typeof value === \"string\" && /^\\d+$/.test(value)) {\n return Number(value);\n }\n }\n return undefined;\n};\n\nconst getApprovalIndexFromEvent = (event?: NexusWidgetProgressEvent) => {\n const rawEvent = event?.event as any;\n const rawStep = event?.step as any;\n return getNumericEventIndex(\n rawEvent?.approvalIndex,\n rawEvent?.swapIndex,\n rawEvent?.currentIndex,\n rawEvent?.index,\n rawEvent?.data?.approvalIndex,\n rawEvent?.data?.swapIndex,\n rawEvent?.data?.currentIndex,\n rawEvent?.data?.index,\n rawStep?.approvalIndex,\n rawStep?.swapIndex,\n rawStep?.currentIndex,\n rawStep?.index,\n rawStep?.data?.approvalIndex,\n rawStep?.data?.swapIndex,\n rawStep?.data?.currentIndex,\n rawStep?.data?.index,\n );\n};\n\nconst getActiveApprovalProgressEvent = (\n events: NexusWidgetProgressEvent[],\n completedApprovalCount = 0,\n approvalSymbols: string[] = [],\n) =>\n [...events]\n .reverse()\n .find((event) => {\n if (\n event.name !== PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS ||\n event.completed\n ) {\n return false;\n }\n\n const units = getApprovalUnitsForStep(event.step);\n if (units.length === 0) return false;\n\n const eventIndex = getApprovalIndexFromEvent(event);\n if (eventIndex !== undefined) {\n return eventIndex >= completedApprovalCount;\n }\n\n if (units.length === 1 && units[0]?.symbol) {\n const symbolIndex = approvalSymbols.findIndex(\n (symbol) => symbol === units[0]?.symbol,\n );\n if (symbolIndex >= 0) return symbolIndex >= completedApprovalCount;\n }\n\n return completedApprovalCount === 0;\n });\n\nconst getApprovalSymbolFromProgressEvent = (\n event?: NexusWidgetProgressEvent,\n) => {\n const units = getApprovalUnitsForStep(event?.step);\n if (units.length === 0) return undefined;\n if (units.length === 1) return units[0]?.symbol;\n\n const index = getApprovalIndexFromEvent(event);\n if (index === undefined || index >= units.length) return undefined;\n return units[index]?.symbol;\n};\n\nconst getApprovalTotalFromSwapStepsList = (\n events: NexusWidgetProgressEvent[],\n) =>\n countApprovalUnits(\n getListedSteps(events, PROGRESS_EVENT_NAMES.SWAP_PLAN_LIST),\n );\n\nconst hasStartedStatus = (\n events: NexusWidgetProgressEvent[],\n id: ProgressStatusId,\n mode: NexusWidgetMode,\n hasTransferAction = false,\n) =>\n events.some(\n (event) => getStatusForStep(event.step, mode, hasTransferAction) === id,\n );\n\nconst buildStatusRows = ({\n events,\n failedStep,\n mode,\n steps,\n approvalTotalCount,\n context,\n}: {\n events: NexusWidgetProgressEvent[];\n failedStep?: ProgressSdkStep | null;\n mode: NexusWidgetMode;\n steps: ProgressStep[];\n approvalTotalCount?: number | null;\n context: {\n destinationChain?: string;\n destinationSymbol?: string;\n opportunityName?: string;\n recipientAddress?: string;\n };\n}): ProgressStatusRow[] => {\n const hasTransferAction =\n mode === \"send\" || (mode === \"swap\" && Boolean(context.recipientAddress));\n const failedStatus = failedStep\n ? getStatusForStep(failedStep, mode, hasTransferAction)\n : null;\n const swapListSteps = getListedSteps(\n events,\n PROGRESS_EVENT_NAMES.SWAP_PLAN_LIST,\n );\n const fallbackSteps = steps.map((item) => item.step);\n const destinationSymbol = context.destinationSymbol || \"token\";\n const destinationChain = context.destinationChain || \"destination\";\n const opportunityName = context.opportunityName || \"app\";\n const immutableApprovalTotal =\n approvalTotalCount ??\n Math.max(\n countApprovalUnits(swapListSteps),\n countApprovalUnits(fallbackSteps),\n countCompletedApprovalUnitsFromEvents(events),\n );\n const refundEligibleFailure =\n failedStep !== null &&\n failedStep !== undefined &&\n stepMatches(failedStep, REFUND_ELIGIBLE_SWAP_TYPES);\n const approvalCompletedCount = Math.min(\n immutableApprovalTotal || Number.MAX_SAFE_INTEGER,\n Math.max(\n countCompletedApprovalUnitsFromEvents(events),\n countCompletedApprovalUnitsFromSteps(steps),\n ),\n );\n const approvalSymbols = getApprovalUnitSymbols(\n swapListSteps.length > 0 ? swapListSteps : fallbackSteps,\n );\n const activeApprovalSymbol = getApprovalSymbolFromProgressEvent(\n getActiveApprovalProgressEvent(\n events,\n approvalCompletedCount,\n approvalSymbols,\n ),\n );\n const hasSwapList =\n swapListSteps.length > 0 ||\n hasStepType(events, steps, [\n \"SWAP_START\",\n \"DETERMINING_SWAP\",\n \"SOURCE_SWAP\",\n \"DESTINATION_SWAP\",\n \"BRIDGE_DEPOSIT\",\n \"BRIDGE_FILL\",\n \"BRIDGE_INTENT_SUBMISSION\",\n \"SWAP_COMPLETE\",\n \"SWAP_SKIPPED\",\n ]);\n const hasDestinationReceiveStep =\n countListedSteps(swapListSteps, DESTINATION_SWAP_TYPES) > 0 ||\n countListedSteps(fallbackSteps, DESTINATION_SWAP_TYPES) > 0 ||\n hasProgressEventType(events, DESTINATION_SWAP_TYPES);\n const receiveTokenTypes = hasDestinationReceiveStep\n ? DESTINATION_SWAP_TYPES\n : BRIDGE_FILL_RECEIVE_TYPES;\n const hasReceiveTokenStep =\n hasDestinationReceiveStep ||\n countListedSteps(swapListSteps, receiveTokenTypes) > 0 ||\n countListedSteps(fallbackSteps, receiveTokenTypes) > 0 ||\n hasProgressEventType(events, receiveTokenTypes);\n const receiveTokenStarted = hasProgressEventType(events, receiveTokenTypes);\n const receiveTokenComplete =\n hasReceiveTokenStep && hasCompletedType(events, steps, receiveTokenTypes);\n const swapComplete = hasCompletedType(events, steps, [\n \"SWAP_COMPLETE\",\n \"SWAP_SKIPPED\",\n ]);\n const swapSkipped = hasCompletedType(events, steps, [\"SWAP_SKIPPED\"]);\n const shouldShowSwapRows =\n hasSwapList && !(swapSkipped && (mode === \"deposit\" || mode === \"send\"));\n const swapTokensComplete = hasReceiveTokenStep\n ? receiveTokenStarted\n : swapComplete;\n const transactionSent = hasCompletedType(events, steps, [\"TRANSACTION_SENT\"]);\n const transactionConfirmed = hasCompletedType(events, steps, [\n \"TRANSACTION_CONFIRMED\",\n ]);\n const rows: ProgressStatusRow[] = [];\n\n const pushRow = (row: ProgressStatusRow) => {\n if (failedStatus) {\n const failedIndex = STATUS_ORDER.indexOf(failedStatus);\n const currentIndex = STATUS_ORDER.indexOf(row.id);\n if (failedIndex >= 0 && currentIndex > failedIndex) return;\n }\n rows.push(row);\n };\n\n if (immutableApprovalTotal > 0) {\n const approvalCurrent = Math.min(\n immutableApprovalTotal,\n Math.max(1, approvalCompletedCount + 1),\n );\n const approvalSymbol =\n activeApprovalSymbol ??\n approvalSymbols[\n Math.min(approvalCompletedCount, approvalSymbols.length - 1)\n ];\n const approvalDescription = approvalSymbol\n ? `Approve ${approvalSymbol} in wallet`\n : \"Approve in wallet\";\n let state: ProgressStatusState = \"default\";\n if (failedStatus === \"approveTokens\") {\n state = \"error\";\n } else if (approvalCompletedCount >= immutableApprovalTotal) {\n state = \"completed\";\n } else if (\n hasStartedStatus(events, \"approveTokens\", mode, hasTransferAction) ||\n events.length === 0\n ) {\n state = \"preapproval\";\n }\n\n const approvalSteps = (\n swapListSteps.length > 0 ? swapListSteps : fallbackSteps\n ).filter((step) => stepMatches(step, SWAP_APPROVAL_TYPES));\n const currentApprovalStep = approvalSteps[approvalCompletedCount];\n const activeSymbol = currentApprovalStep\n ? (currentApprovalStep as any).symbol\n : undefined;\n\n pushRow({\n id: \"approveTokens\",\n state,\n description: state === \"preapproval\" ? approvalDescription : undefined,\n label:\n state === \"completed\"\n ? `Approved Swaps (${immutableApprovalTotal} of ${immutableApprovalTotal})`\n : state === \"error\"\n ? \"Collection failed\"\n : `Approve Swaps (${approvalCurrent} of ${immutableApprovalTotal})`,\n });\n }\n\n if (shouldShowSwapRows) {\n const approvalsSatisfied =\n immutableApprovalTotal === 0 ||\n approvalCompletedCount >= immutableApprovalTotal;\n let state: ProgressStatusState = \"default\";\n if (failedStatus === \"swapTokens\") {\n state = \"error\";\n } else if (swapTokensComplete) {\n state = \"completed\";\n } else if (\n approvalsSatisfied &&\n (hasStartedStatus(events, \"swapTokens\", mode, hasTransferAction) ||\n hasEventType(events, [\n \"DESTINATION_SWAP_BATCH_TX\",\n \"BRIDGE_DEPOSIT\",\n \"SOURCE_SWAP_BATCH_TX\",\n \"SOURCE_SWAP_HASH\",\n \"SWAP_START\",\n ]))\n ) {\n state = \"inProgress\";\n }\n\n pushRow({\n id: \"swapTokens\",\n state,\n label:\n state === \"completed\"\n ? \"Swaps completed\"\n : state === \"error\"\n ? refundEligibleFailure\n ? \"Swap failed. Refund initiated\"\n : \"Swap failed\"\n : state === \"inProgress\"\n ? \"Swaps in progress\"\n : \"Swap tokens\",\n });\n\n if (hasReceiveTokenStep) {\n pushRow({\n id: \"receiveToken\",\n state:\n failedStatus === \"receiveToken\"\n ? \"error\"\n : receiveTokenComplete\n ? \"completed\"\n : receiveTokenStarted\n ? \"inProgress\"\n : \"default\",\n label:\n failedStatus === \"receiveToken\"\n ? refundEligibleFailure\n ? \"Destination swap failed. Refund initiated.\"\n : \"Destination swap failed.\"\n : receiveTokenComplete\n ? `Received ${destinationSymbol} on ${destinationChain}`\n : receiveTokenStarted\n ? `Receiving ${destinationSymbol} on ${destinationChain}`\n : `Receive ${destinationSymbol} on ${destinationChain}`,\n });\n }\n }\n\n if (mode === \"deposit\" || hasTransferAction) {\n const receiveComplete =\n swapSkipped ||\n !shouldShowSwapRows ||\n (hasReceiveTokenStep ? receiveTokenComplete : swapTokensComplete);\n const isDeposit = mode === \"deposit\";\n let state: ProgressStatusState = \"default\";\n if (failedStatus === \"action\") {\n state = \"error\";\n } else if (transactionConfirmed) {\n state = \"completed\";\n } else if (transactionSent) {\n state = \"inProgress\";\n } else if (receiveComplete) {\n state = \"preapproval\";\n }\n\n pushRow({\n id: \"action\",\n state,\n description: state === \"preapproval\" ? \"Approve in wallet\" : undefined,\n label: isDeposit\n ? state === \"completed\"\n ? `${destinationSymbol} deposited to ${opportunityName}`\n : state === \"inProgress\"\n ? `Depositing ${destinationSymbol} to ${opportunityName}`\n : state === \"error\"\n ? \"Deposit failed. Funds are in your wallet.\"\n : state === \"preapproval\"\n ? `Approve Deposit of ${destinationSymbol} to ${opportunityName}`\n : `Deposit ${destinationSymbol} to ${opportunityName}`\n : state === \"completed\"\n ? `${destinationSymbol} sent`\n : state === \"inProgress\"\n ? `Sending ${destinationSymbol}`\n : state === \"error\"\n ? \"Send failed. Funds are in your wallet.\"\n : state === \"preapproval\"\n ? `Approve ${destinationSymbol} transfer`\n : `Send ${destinationSymbol}`,\n });\n }\n\n const orderedRows = rows.sort(\n (a, b) => STATUS_ORDER.indexOf(a.id) - STATUS_ORDER.indexOf(b.id),\n );\n\n if (\n orderedRows.some(\n (row) => row.state === \"preapproval\" || row.state === \"inProgress\",\n ) ||\n orderedRows.some((row) => row.state === \"error\")\n ) {\n return orderedRows;\n }\n\n const nextActiveIndex = orderedRows.findIndex(\n (row) => row.state === \"default\",\n );\n if (nextActiveIndex === -1) return orderedRows;\n\n return orderedRows.map((row, index) => {\n if (index !== nextActiveIndex) return row;\n const nextState =\n row.id === \"approveTokens\" || row.id === \"action\"\n ? \"preapproval\"\n : \"inProgress\";\n return {\n ...row,\n description:\n nextState === \"preapproval\" ? \"Approve in wallet\" : undefined,\n label:\n row.id === \"swapTokens\"\n ? \"Swaps in progress\"\n : row.id === \"receiveToken\"\n ? `Receiving ${destinationSymbol} on ${destinationChain}`\n : row.label,\n state: nextState,\n };\n });\n};\n\nfunction MiniLogo({\n src,\n label,\n size,\n style,\n}: {\n src?: string;\n label?: string;\n size: number;\n style?: React.CSSProperties;\n}) {\n const [failed, setFailed] = useState(!src);\n\n useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n background: \"#FFFFFE\",\n borderRadius: \"999px\",\n height: size,\n objectFit: \"cover\",\n width: size,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {(label || \"?\").trim().slice(0, 1).toUpperCase()}\n \n );\n}\n\nfunction TokenLogoPair({\n tokenLogo,\n chainLogo,\n tokenSymbol,\n chainName,\n}: {\n tokenLogo?: string;\n chainLogo?: string;\n tokenSymbol?: string;\n chainName?: string;\n}) {\n return (\n \n \n {chainLogo && (\n \n )}\n \n );\n}\n\nexport function NexusWidgetProgressScreen({\n fromTokens = [],\n toToken,\n fromAmountUsd,\n toAmount,\n toAmountUsd,\n totalFeeUsd,\n intentData,\n mode,\n opportunity,\n steps,\n progressEvents = [],\n failedStep,\n recipientAddress,\n}: NexusWidgetProgressScreenProps) {\n const intentSources = intentData?.sources ?? [];\n const intentDestination = intentData?.destination;\n const destinationSourceToken = fromTokens.find((token) => {\n const destinationChainId = intentDestination?.chain.id ?? toToken?.chainId;\n const destinationTokenAddress = (\n intentDestination?.token.contractAddress ??\n toToken?.contractAddress ??\n \"\"\n ).toLowerCase();\n const tokenAmount =\n parseDecimal(token.userAmount) ?? parseDecimal(token.balance);\n\n return (\n destinationChainId !== undefined &&\n destinationTokenAddress !== \"\" &&\n token.chainId === destinationChainId &&\n token.contractAddress.toLowerCase() === destinationTokenAddress &&\n Boolean(tokenAmount && tokenAmount.gt(0))\n );\n });\n const sourceSymbols = unique([\n ...(destinationSourceToken ? [destinationSourceToken.symbol] : []),\n ...(intentSources.length > 0\n ? intentSources.map((source) => source.token.symbol)\n : fromTokens.map((token) => token.symbol)),\n ]);\n const intentSourceUsd =\n intentSources.length > 0\n ? intentSources.reduce(\n (sum, source) => sum.plus(parseDecimal(source.value) ?? 0),\n new Decimal(0),\n )\n : parseDecimal(fromAmountUsd);\n const requestedDestinationAmount = parseDecimal(toAmount);\n const quotedDestinationAmount = parseDecimal(intentDestination?.amount);\n const destinationBalanceAmount = parseDecimal(toToken?.balance);\n const requestedDestinationUsd = parseDecimal(toAmountUsd);\n const destinationUsdRate =\n requestedDestinationAmount &&\n requestedDestinationAmount.gt(0) &&\n requestedDestinationUsd &&\n requestedDestinationUsd.gt(0)\n ? requestedDestinationUsd.div(requestedDestinationAmount)\n : quotedDestinationAmount &&\n quotedDestinationAmount.gt(0) &&\n intentDestination?.value\n ? (parseDecimal(intentDestination.value) ?? new Decimal(0)).div(\n quotedDestinationAmount,\n )\n : undefined;\n const destinationCoverageUsd =\n (mode === \"deposit\" || mode === \"send\") &&\n requestedDestinationAmount &&\n requestedDestinationAmount.gt(0) &&\n quotedDestinationAmount &&\n requestedDestinationAmount.gt(quotedDestinationAmount) &&\n destinationBalanceAmount &&\n destinationBalanceAmount.gt(0) &&\n destinationUsdRate &&\n destinationUsdRate.gt(0)\n ? Decimal.min(\n requestedDestinationAmount.minus(quotedDestinationAmount),\n destinationBalanceAmount,\n ).mul(destinationUsdRate)\n : undefined;\n const quotedDestinationUsd = parseDecimal(intentDestination?.value);\n const feeUsd = parseDecimal(totalFeeUsd);\n const sourceUsd =\n mode === \"deposit\" || mode === \"send\"\n ? [\n destinationCoverageUsd !== undefined\n ? (intentSourceUsd ?? new Decimal(0)).plus(destinationCoverageUsd)\n : intentSourceUsd,\n requestedDestinationUsd,\n requestedDestinationUsd &&\n requestedDestinationUsd.gt(0) &&\n intentSourceUsd &&\n intentSourceUsd.gt(0) &&\n quotedDestinationUsd &&\n quotedDestinationUsd.gt(0)\n ? requestedDestinationUsd.plus(\n Decimal.max(intentSourceUsd.minus(quotedDestinationUsd), 0),\n )\n : undefined,\n requestedDestinationUsd &&\n requestedDestinationUsd.gt(0) &&\n feeUsd &&\n feeUsd.gt(0)\n ? requestedDestinationUsd.plus(feeUsd)\n : undefined,\n ]\n .filter((value): value is Decimal => Boolean(value && value.gt(0)))\n .reduce(\n (max, value) => (!max || value.gt(max) ? value : max),\n undefined,\n )\n : intentSourceUsd;\n const destinationAmount =\n (mode === \"deposit\" || mode === \"send\") && toAmount\n ? toAmount\n : (intentDestination?.amount ?? toAmount ?? \"0\");\n const destinationSymbol =\n intentDestination?.token.symbol ||\n toToken?.symbol ||\n opportunity?.tokenSymbol ||\n \"\";\n const destinationChainName = getShortChainName(\n intentDestination?.chain.id ?? toToken?.chainId,\n intentDestination?.chain.name || toToken?.chainName || \"\",\n );\n const destinationChain =\n mode === \"deposit\"\n ? opportunity?.title || opportunity?.protocol || destinationChainName\n : destinationChainName;\n const seededApprovalTotal = countApprovalUnits(\n (steps ?? []).map((item) => item.step),\n );\n const completedApprovalEventTotal =\n countCompletedApprovalUnitsFromEvents(progressEvents);\n const computedApprovalTotal = Math.max(\n getApprovalTotalFromSwapStepsList(progressEvents),\n seededApprovalTotal,\n completedApprovalEventTotal,\n );\n const [lockedApprovalTotal, setLockedApprovalTotal] = useState(\n null,\n );\n const approvalTotalCount =\n lockedApprovalTotal ??\n (computedApprovalTotal > 0 ? computedApprovalTotal : null);\n\n useEffect(() => {\n if (progressEvents.length === 0) {\n setLockedApprovalTotal(null);\n return;\n }\n if (lockedApprovalTotal !== null || computedApprovalTotal <= 0) return;\n setLockedApprovalTotal(computedApprovalTotal);\n }, [computedApprovalTotal, lockedApprovalTotal, progressEvents.length]);\n\n const statusRows = buildStatusRows({\n events: progressEvents,\n failedStep,\n mode,\n steps: steps ?? [],\n approvalTotalCount,\n context: {\n destinationChain: destinationChainName || destinationChain,\n destinationSymbol,\n opportunityName: opportunity?.title || opportunity?.protocol,\n recipientAddress,\n },\n });\n const [stepsExpanded, setStepsExpanded] = useState(true);\n const activeRow =\n statusRows.find(\n (row) => row.state === \"preapproval\" || row.state === \"inProgress\",\n ) ??\n statusRows.find((row) => row.state === \"error\") ??\n statusRows.find((row) => row.state === \"default\") ??\n statusRows[statusRows.length - 1];\n const visibleRows = stepsExpanded ? statusRows : activeRow ? [activeRow] : [];\n const canExpand = statusRows.length > 1;\n const getRowHeight = (row: ProgressStatusRow) => (row.description ? 47 : 40);\n const collapsedStatusHeight = activeRow ? getRowHeight(activeRow) : 40;\n const expandedStatusHeight = statusRows.reduce(\n (sum, row) => sum + getRowHeight(row),\n 0,\n );\n\n return (\n \n \n \n {formatSymbolSummary(sourceSymbols)}\n \n \n {formatUsd(sourceUsd)}\n \n\n \n\n \n \n \n {formatDecimal(destinationAmount, 8)}\n \n {destinationSymbol}\n \n \n {destinationChain && (\n \n on {destinationChain}\n \n )}\n \n \n\n \n \n \n {visibleRows.map((row, index) => {\n const isCompleted = row.state === \"completed\";\n const isError = row.state === \"error\";\n const isDefault = row.state === \"default\";\n const isLoading =\n row.state === \"preapproval\" || row.state === \"inProgress\";\n const hasDescription = Boolean(row.description);\n const rowColor = isDefault ? muted : isError ? danger : primary;\n\n return (\n {\n if (canExpand) setStepsExpanded((current) => !current);\n }}\n style={{\n alignItems: hasDescription ? \"flex-start\" : \"center\",\n appearance: \"none\",\n background: \"transparent\",\n border: \"0\",\n borderTop:\n index > 0 && stepsExpanded ? `1px solid ${border}` : \"0\",\n boxSizing: \"border-box\",\n color: rowColor,\n cursor: canExpand ? \"pointer\" : \"default\",\n display: \"flex\",\n fontFamily,\n fontSize: \"11px\",\n fontWeight: 400,\n gap: \"7px\",\n minHeight: `${getRowHeight(row)}px`,\n padding: \"8px 11px\",\n textAlign: \"left\",\n transition:\n \"color 220ms ease, min-height 220ms ease, opacity 220ms ease\",\n width: \"100%\",\n }}\n type=\"button\"\n >\n {isCompleted || isError ? (\n \n {isError ? (\n \n ) : (\n \n )}\n \n ) : isDefault ? (\n \n ) : (\n\t \n )}\n \n \n {row.label}\n \n {row.description && (\n \n {row.description}\n \n )}\n \n {canExpand && index === 0 && (\n \n )}\n \n );\n })}\n \n \n \n \n );\n}\n", @@ -86,7 +98,7 @@ }, { "path": "registry/avail-widgets/nexus-widget/components/swap-idle-form.tsx", - "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\nimport Decimal from \"decimal.js\";\nimport React, { useEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n formatSelectedTokenBalanceLabel,\n formatUsdBalanceLabel,\n type SwapTokenOption,\n} from \"./swap-asset-selector\";\n\nconst tabularNums: React.CSSProperties = {\n fontFeatureSettings: '\"tnum\"',\n fontVariantNumeric: \"tabular-nums\",\n};\n\nconst brand = \"var(--foreground-brand)\";\n\ninterface SwapIdleFormProps {\n amount: string;\n defaultRecipientAddress?: string;\n fromTokens: SwapTokenOption[];\n isAmountReadOnly?: boolean;\n isBalanceLoading?: boolean;\n isDestinationPickerDisabled?: boolean;\n hideDestinationTokenDropdownIcon?: boolean;\n isReceiveAmountLoading?: boolean;\n isReceiveUsdLoading?: boolean;\n isSourcePickerDisabled?: boolean;\n onAmountChange: (val: string, panel: \"send\" | \"receive\") => void;\n onOpenDestPicker: () => void;\n onOpenRecipientPicker?: () => void;\n onOpenSourcePicker: (index?: number) => void;\n onUpdateTokens?: (tokens: SwapTokenOption[]) => void;\n receiveQuoteAmount?: string;\n receiveQuoteUsd?: string;\n recipientAddress?: string;\n sourceRouteMessage?: string;\n sourceRouteStatus?: \"loading\" | \"insufficient\";\n swapType: \"exactIn\" | \"exactOut\";\n toToken?: SwapTokenOption;\n totalBalance: string;\n usdValue: string;\n}\n\n/** Chevron down icon used in asset selector pills */\nconst ChevronDownIcon = () => (\n \n \n \n);\n\nconst ArrowUpDownIcon = () => (\n \n \n \n);\n\n/** Reusable percentage quick-select buttons row with transition wrapper */\nfunction PercentButtons({\n visible,\n onSelect,\n maxLabel = \"Max\",\n}: {\n visible: boolean;\n onSelect: (pct: number) => void;\n maxLabel?: string;\n}) {\n const [hoveredPct, setHoveredPct] = useState(null);\n\n return (\n \n {[20, 50, 100].map((pct) => {\n const label = pct === 100 ? maxLabel : `${pct}%`;\n const isHovered = hoveredPct === pct;\n\n return (\n {\n e.stopPropagation();\n onSelect(pct);\n }}\n onMouseDown={(e) => {\n e.preventDefault();\n }}\n onMouseEnter={() => setHoveredPct(pct)}\n onMouseLeave={() => setHoveredPct(null)}\n style={{\n alignItems: \"center\",\n backgroundColor: isHovered ? \"#FFFFFF\" : \"transparent\",\n borderRadius: \"4px\",\n boxShadow: isHovered ? \"#3C286414 0px 1px 2px\" : \"none\",\n boxSizing: \"border-box\",\n color: isHovered ? \"#1F1F1F\" : \"#8E8E89\",\n cursor: \"pointer\",\n display: \"flex\",\n fontFamily: '\"Geist\", system-ui, sans-serif',\n fontSize: \"10px\",\n fontWeight: 500,\n height: \"18px\",\n justifyContent: \"center\",\n flex: \"1 1 0%\",\n minWidth: 0,\n paddingInline: \"3px\",\n border: \"none\",\n transition: \"all 0.15s ease-out\",\n }}\n tabIndex={-1}\n type=\"button\"\n >\n {label}\n \n );\n })}\n \n );\n}\n\nfunction UnifiedTokenLogoBadge({\n token,\n size = 24,\n}: {\n token: SwapTokenOption;\n size?: number;\n}) {\n const [popover, setPopover] = useState<{\n left: number;\n top: number;\n width: number;\n maxHeight: number;\n } | null>(null);\n const triggerRef = useRef(null);\n const sources = token.sourceTokens ?? [];\n const chainCount =\n new Set(\n sources\n .map((source) => source.chainId ?? source.chainName)\n .filter(Boolean),\n ).size || sources.length;\n\n const showPopover = () => {\n const rect = triggerRef.current?.getBoundingClientRect();\n if (!rect || typeof window === \"undefined\") return;\n const width = 250;\n const maxHeight = 260;\n const viewportPadding = 8;\n const left = Math.min(\n Math.max(viewportPadding, rect.right - width),\n window.innerWidth - width - viewportPadding,\n );\n const belowTop = rect.bottom + 8;\n const top =\n belowTop + maxHeight > window.innerHeight\n ? Math.max(viewportPadding, rect.top - maxHeight - 8)\n : belowTop;\n setPopover({ left, top, width, maxHeight });\n };\n\n return (\n setPopover(null)}\n ref={triggerRef}\n style={{\n boxSizing: \"border-box\",\n flexShrink: 0,\n height: `${size}px`,\n position: \"relative\",\n width: `${size}px`,\n }}\n >\n \n {chainCount > 0 && (\n 9 ? \"3px\" : 0,\n position: \"absolute\",\n right: -3,\n }}\n >\n {chainCount}\n \n )}\n {popover &&\n sources.length > 0 &&\n typeof document !== \"undefined\" &&\n createPortal(\n \n \n \n Unified · {chainCount} {chainCount === 1 ? \"Chain\" : \"Chains\"}\n \n \n ≈ {formatUsdBalanceLabel(token.balanceInFiat)}\n \n \n \n {sources.map((source) => (\n \n \n \n \n {source.chainName || \"Unknown chain\"}\n \n \n \n {formatAmountInputDisplay(source.balance || \"0\")}\n \n \n ))}\n \n ,\n document.body,\n )}\n \n );\n}\n\nfunction PercentHoverButton({\n label,\n onClick,\n tabIndex,\n}: {\n label: string;\n onClick: () => void;\n tabIndex?: number;\n}) {\n const [hover, setHover] = useState(false);\n const [active, setActive] = useState(false);\n const handledPointerDownRef = useRef(false);\n const pointerResetTimerRef = useRef | null>(\n null,\n );\n\n useEffect(() => {\n return () => {\n if (pointerResetTimerRef.current) {\n clearTimeout(pointerResetTimerRef.current);\n }\n };\n }, []);\n\n const isHighlighted = hover || active;\n\n return (\n {\n if (handledPointerDownRef.current) {\n if (pointerResetTimerRef.current) {\n clearTimeout(pointerResetTimerRef.current);\n pointerResetTimerRef.current = null;\n }\n handledPointerDownRef.current = false;\n return;\n }\n onClick();\n }}\n onMouseDown={(event) => {\n event.preventDefault();\n setActive(true);\n }}\n onMouseEnter={() => setHover(true)}\n onMouseLeave={() => {\n setHover(false);\n setActive(false);\n }}\n onMouseUp={() => setActive(false)}\n onPointerDown={(event) => {\n if (event.pointerType === \"mouse\") return;\n event.preventDefault();\n if (pointerResetTimerRef.current) {\n clearTimeout(pointerResetTimerRef.current);\n }\n handledPointerDownRef.current = true;\n setActive(true);\n onClick();\n }}\n onPointerUp={() => {\n setActive(false);\n if (handledPointerDownRef.current) {\n pointerResetTimerRef.current = setTimeout(() => {\n handledPointerDownRef.current = false;\n pointerResetTimerRef.current = null;\n }, 350);\n }\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: isHighlighted ? \"#E8F0FF\" : \"#F4F4F3\",\n borderRadius: \"6px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flex: \"1 1 0%\",\n justifyContent: \"center\",\n paddingBlock: \"3px\",\n paddingInline: \"6px\",\n border: \"none\",\n cursor: \"pointer\",\n transition: \"background-color 0.2s ease-out\",\n }}\n tabIndex={tabIndex}\n >\n \n {label}\n \n \n );\n}\n\nfunction SkeletonBar({\n width,\n height,\n borderRadius = \"8px\",\n}: {\n width: string;\n height: string;\n borderRadius?: string;\n}) {\n return (\n \n );\n}\n\nfunction LogoCircle({\n src,\n alt,\n label,\n size,\n fontSize,\n outline,\n style,\n}: {\n src?: string;\n alt?: string;\n label?: string;\n size: number;\n fontSize: number;\n outline?: string;\n style?: React.CSSProperties;\n}) {\n const [failed, setFailed] = useState(!src);\n\n useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n const fallbackLabel = (label || alt || \"?\").trim().slice(0, 1).toUpperCase();\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n backgroundColor: \"#FFFFFE\",\n borderRadius: \"999px\",\n height: `${size}px`,\n objectFit: \"cover\",\n outline,\n width: `${size}px`,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {fallbackLabel || \"?\"}\n \n );\n}\n\nconst sameAddress = (a?: string, b?: string) =>\n Boolean(a && b && a.toLowerCase() === b.toLowerCase());\n\nconst formatShortAddress = (address?: string) => {\n if (!address) return \"\";\n return address.length > 12\n ? `${address.slice(0, 6)}…${address.slice(-4)}`\n : address;\n};\n\nconst formatTokenBalanceLabel = formatSelectedTokenBalanceLabel;\n\nconst parseDecimal = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = String(value).replace(/[^0-9.-]/g, \"\");\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst formatUsdValue = (value: Decimal) =>\n value.gt(0) && value.lt(0.01) ? \"<0.01\" : value.toDecimalPlaces(2).toFixed(2);\n\nconst MAX_AMOUNT_DISPLAY_DECIMALS = 8;\nconst getTokenInputDecimals = (token?: Pick) => {\n const decimals = Number(token?.decimals);\n return Number.isFinite(decimals) && decimals >= 0 ? Math.floor(decimals) : 18;\n};\n\nconst formatAmountInputDisplay = (value: string) => {\n if (!value) return \"\";\n try {\n return new Decimal(value)\n .toDecimalPlaces(MAX_AMOUNT_DISPLAY_DECIMALS, Decimal.ROUND_DOWN)\n .toFixed();\n } catch {\n return value;\n }\n};\n\nexport function SwapIdleForm({\n amount,\n receiveQuoteAmount,\n receiveQuoteUsd,\n isReceiveAmountLoading = false,\n isReceiveUsdLoading = false,\n sourceRouteStatus,\n sourceRouteMessage,\n onAmountChange,\n fromTokens,\n toToken,\n totalBalance,\n usdValue,\n onOpenSourcePicker,\n onOpenDestPicker,\n onOpenRecipientPicker,\n recipientAddress,\n defaultRecipientAddress,\n swapType,\n onUpdateTokens,\n isAmountReadOnly = false,\n isBalanceLoading = false,\n isDestinationPickerDisabled = false,\n hideDestinationTokenDropdownIcon = false,\n isSourcePickerDisabled = false,\n}: SwapIdleFormProps) {\n const [focusedPanel, setFocusedPanel] = useState<\"send\" | \"receive\" | null>(\n null,\n );\n const [focusedRow, setFocusedRow] = useState(null);\n const [tooltip, setTooltip] = useState(null);\n const [tooltipTriggerRect, setTooltipTriggerRect] = useState(\n null,\n );\n const sourceListRef = useRef(null);\n const sourceRowRefs = useRef>({});\n const sourceInputRefs = useRef>({});\n const previousSourceCountRef = useRef(fromTokens.length);\n const prevTokensRef = useRef(fromTokens);\n const [autofocusIndex, setAutofocusIndex] = useState(null);\n\n useEffect(() => {\n const prev = prevTokensRef.current;\n if (fromTokens.length > prev.length) {\n setAutofocusIndex(fromTokens.length - 1);\n } else if (fromTokens.length === prev.length) {\n for (let i = 0; i < fromTokens.length; i++) {\n const p = prev[i];\n const c = fromTokens[i];\n if (\n p &&\n c &&\n (p.contractAddress !== c.contractAddress ||\n p.chainId !== c.chainId) &&\n !c.userAmount\n ) {\n setAutofocusIndex(i);\n break;\n }\n }\n }\n prevTokensRef.current = fromTokens;\n }, [fromTokens]);\n\n useEffect(() => {\n const previousSourceCount = previousSourceCountRef.current;\n if (fromTokens.length > previousSourceCount && previousSourceCount > 0) {\n const newIndex = fromTokens.length - 1;\n requestAnimationFrame(() => {\n const input = sourceInputRefs.current[newIndex];\n if (input) {\n input.focus();\n input.select();\n }\n\n const container = sourceListRef.current;\n const row = sourceRowRefs.current[newIndex];\n if (\n !container ||\n !row ||\n container.scrollHeight <= container.clientHeight\n ) {\n return;\n }\n\n const containerRect = container.getBoundingClientRect();\n const rowRect = row.getBoundingClientRect();\n const nextTop =\n rowRect.top - containerRect.top + container.scrollTop - 8;\n\n container.scrollTo({\n behavior: \"smooth\",\n top: Math.max(0, nextTop),\n });\n });\n }\n previousSourceCountRef.current = fromTokens.length;\n }, [fromTokens.length]);\n\n const sanitizeInput = (raw: string, maxDecimals = 18): string => {\n let next = raw.replaceAll(/[^0-9.]/g, \"\");\n const parts = next.split(\".\");\n if (parts.length > 2) next = parts[0] + \".\" + parts.slice(1).join(\"\");\n const [integerPart, decimalPart] = next.split(\".\");\n if (decimalPart !== undefined) {\n next = `${integerPart}.${decimalPart.slice(0, Math.max(0, maxDecimals))}`;\n }\n if (next === \".\") next = \"0.\";\n // Strip leading zeros\n if (next.length > 1 && next.startsWith(\"0\") && next[1] !== \".\") {\n next = next.replace(/^0+/, \"\");\n if (next === \"\") next = \"0\";\n if (next.startsWith(\".\")) next = \"0\" + next;\n }\n return next;\n };\n\n const handleBlurAmount = (index: number) => {\n if (!onUpdateTokens) return;\n const token = fromTokens[index];\n if (!token || !token.userAmount) return;\n if (token.userAmount.includes(\".\")) {\n const stripped = token.userAmount.replace(/0+$/, \"\").replace(/\\.$/, \"\");\n if (stripped !== token.userAmount) {\n const next = [...fromTokens];\n next[index] = { ...token, userAmount: stripped };\n onUpdateTokens(next);\n }\n }\n };\n\n const handleSendInput = (e: React.ChangeEvent) => {\n if (isAmountReadOnly) return;\n const token = fromTokens.length === 1 ? fromTokens[0] : undefined;\n onAmountChange(\n sanitizeInput(e.target.value, getTokenInputDecimals(token)),\n \"send\",\n );\n };\n\n const handleTokenAmountChange = (index: number, val: string) => {\n if (isAmountReadOnly) return;\n if (!onUpdateTokens) return;\n const token = fromTokens[index];\n if (!token) return;\n\n let sanitized = sanitizeInput(\n val,\n token.userAmountMode === \"usd\"\n ? MAX_AMOUNT_DISPLAY_DECIMALS\n : getTokenInputDecimals(token),\n );\n\n const next = [...fromTokens];\n next[index] = { ...token, userAmount: sanitized };\n onUpdateTokens(next);\n\n // Also update total amount for backwards compatibility if needed\n const total = next.reduce((sum, t) => sum + Number(t.userAmount || 0), 0);\n onAmountChange(total > 0 ? String(total) : \"\", \"send\");\n };\n\n const handleToggleMode = (index: number) => {\n if (isAmountReadOnly) return;\n if (!onUpdateTokens) return;\n const token = fromTokens[index];\n if (!token) return;\n\n const tokenBalance =\n Number(String(token.balance).replace(/[^0-9.]/g, \"\")) || 0;\n const fiatBalance =\n Number(String(token.balanceInFiat).replace(/[^0-9.]/g, \"\")) || 0;\n const price = tokenBalance > 0 ? fiatBalance / tokenBalance : 0;\n if (price === 0) return;\n\n const currentVal = Number(token.userAmount || 0);\n const next = [...fromTokens];\n if (token.userAmountMode === \"usd\") {\n const newTokenVal = currentVal > 0 ? (currentVal / price).toString() : \"\";\n next[index] = {\n ...token,\n userAmountMode: \"token\",\n userAmount: newTokenVal ? newTokenVal.substring(0, 10) : \"\",\n };\n } else {\n const newUsdVal = currentVal > 0 ? (currentVal * price).toFixed(2) : \"\";\n next[index] = { ...token, userAmountMode: \"usd\", userAmount: newUsdVal };\n }\n onUpdateTokens(next);\n const total = getTokenAmountTotal(next);\n onAmountChange(total > 0 ? String(total) : \"\", \"send\");\n };\n\n const getSourceUsdValue = React.useCallback((token: SwapTokenOption) => {\n if (!token || !token.userAmount) return 0;\n const quotedUsd = parseDecimal(token.userAmountUsd);\n if (quotedUsd && quotedUsd.gte(0)) return quotedUsd.toNumber();\n const tokenBalance =\n Number(String(token.balance).replace(/[^0-9.]/g, \"\")) || 0;\n const fiatBalance =\n Number(String(token.balanceInFiat).replace(/[^0-9.]/g, \"\")) || 0;\n const price = tokenBalance > 0 ? fiatBalance / tokenBalance : 0;\n const amountNumber = Number(token.userAmount || 0);\n if (!Number.isFinite(amountNumber)) return 0;\n if (token.userAmountMode === \"usd\") return amountNumber;\n return amountNumber * price;\n }, []);\n\n const totalUsd = React.useMemo(() => {\n return fromTokens.reduce((sum, token) => sum + getSourceUsdValue(token), 0);\n }, [fromTokens, getSourceUsdValue]);\n\n const hasSourceOverflow = fromTokens.length > 3;\n const [isSourceListAtBottom, setIsSourceListAtBottom] = useState(false);\n const updateSourceListScrollState = React.useCallback(() => {\n const element = sourceListRef.current;\n if (!element || !hasSourceOverflow) {\n setIsSourceListAtBottom(false);\n return;\n }\n\n const distanceFromBottom =\n element.scrollHeight - element.scrollTop - element.clientHeight;\n setIsSourceListAtBottom(distanceFromBottom <= 2);\n }, [hasSourceOverflow]);\n\n useEffect(() => {\n requestAnimationFrame(updateSourceListScrollState);\n }, [fromTokens.length, updateSourceListScrollState]);\n\n const sourceRowsToRender: Array<{\n token: SwapTokenOption | null;\n index: number;\n position: number;\n }> =\n fromTokens.length > 0\n ? fromTokens.map((token, index) => ({ token, index, position: index }))\n : [{ token: null, index: 0, position: 0 }];\n\n const isExactIn = swapType === \"exactIn\";\n const showSourceRouteSkeleton = !isExactIn && sourceRouteStatus === \"loading\";\n const sourceRouteHelper =\n sourceRouteStatus === \"insufficient\" ? sourceRouteMessage : undefined;\n const receiveBalanceLabel = formatTokenBalanceLabel(toToken);\n const getReceiveUsdRate = () => {\n const quoteTokenAmount = parseDecimal(receiveQuoteAmount);\n const quoteUsdAmount = parseDecimal(receiveQuoteUsd);\n if (quoteTokenAmount?.gt(0) && quoteUsdAmount?.gt(0)) {\n return quoteUsdAmount.div(quoteTokenAmount);\n }\n\n const tokenBalance = parseDecimal(toToken?.balance);\n const fiatBalance = parseDecimal(toToken?.balanceInFiat);\n if (tokenBalance?.gt(0) && fiatBalance?.gt(0)) {\n return fiatBalance.div(tokenBalance);\n }\n\n return undefined;\n };\n const receiveInputValue = isExactIn ? (receiveQuoteAmount ?? \"\") : amount;\n const receiveDisplayValue =\n focusedPanel === \"receive\"\n ? receiveInputValue\n : formatAmountInputDisplay(receiveInputValue);\n const receiveAmountTextColor =\n (!isExactIn && amount) || (isExactIn && receiveQuoteAmount)\n ? \"#161615\"\n : \"#9E9E9C\";\n const receiveUsdRate = getReceiveUsdRate();\n const receiveTokenAmount = parseDecimal(receiveInputValue);\n const receiveUsdAmount = receiveQuoteUsd\n ? parseDecimal(receiveQuoteUsd)\n : receiveTokenAmount && receiveUsdRate\n ? receiveTokenAmount.mul(receiveUsdRate)\n : undefined;\n const receiveAltValue = `≈ $${\n receiveUsdAmount ? formatUsdValue(receiveUsdAmount) : \"0.00\"\n }`;\n const isDefaultRecipient = sameAddress(\n recipientAddress,\n defaultRecipientAddress,\n );\n const recipientColor = recipientAddress\n ? isDefaultRecipient\n ? brand\n : \"#B7791F\"\n : \"#848483\";\n const getTokenAmountTotal = (tokens: SwapTokenOption[]) =>\n tokens.reduce((sum, item) => sum + Number(item.userAmount || 0), 0);\n\n const handleSendPercentForToken = (\n index: number,\n pct: number,\n token: SwapTokenOption,\n ) => {\n if (isAmountReadOnly) return;\n if (!token.balance || !onUpdateTokens) return;\n let finalVal = \"\";\n const isUsdMode = token.userAmountMode === \"usd\";\n\n if (isUsdMode) {\n const fiatBalStr = String(token.balanceInFiat || \"0\");\n const fiatBalance = parseDecimal(fiatBalStr);\n if (!fiatBalance) return;\n if (pct === 100) {\n finalVal = fiatBalance\n .toDecimalPlaces(MAX_AMOUNT_DISPLAY_DECIMALS, Decimal.ROUND_DOWN)\n .toFixed();\n } else {\n finalVal = fiatBalance\n .mul(pct)\n .div(100)\n .toDecimalPlaces(MAX_AMOUNT_DISPLAY_DECIMALS, Decimal.ROUND_DOWN)\n .toFixed();\n }\n } else {\n const balanceStr = String(token.balance || \"0\");\n const tokenBalance = parseDecimal(balanceStr);\n if (!tokenBalance) return;\n const tokenDecimals = getTokenInputDecimals(token);\n if (pct === 100) {\n finalVal = tokenBalance\n .toDecimalPlaces(tokenDecimals, Decimal.ROUND_DOWN)\n .toFixed();\n } else {\n finalVal = tokenBalance\n .mul(pct)\n .div(100)\n .toDecimalPlaces(tokenDecimals, Decimal.ROUND_DOWN)\n .toFixed();\n }\n }\n\n const next = [...fromTokens];\n next[index] = {\n ...next[index],\n userAmount: finalVal,\n userAmountMode: isUsdMode ? \"usd\" : \"token\",\n };\n onUpdateTokens(next);\n const total = getTokenAmountTotal(next);\n onAmountChange(total > 0 ? String(total) : \"\", \"send\");\n };\n\n const handleSendPercent = (pct: number) => {\n if (isAmountReadOnly) return;\n if (!totalBalance) return;\n const bal = parseFloat(totalBalance.replace(/[^0-9.]/g, \"\"));\n if (isNaN(bal)) return;\n const val = bal * (pct / 100);\n // If there's only one token, or no tokens, update the main amount\n if (fromTokens.length <= 1) {\n if (fromTokens.length === 1 && onUpdateTokens) {\n handleSendPercentForToken(0, pct, fromTokens[0]);\n return;\n }\n onAmountChange(val.toFixed(6).replace(/\\.?0+$/, \"\"), \"send\");\n }\n };\n\n return (\n \n {(isReceiveAmountLoading || isReceiveUsdLoading || sourceRouteStatus) && (\n \n )}\n {/* ─── SEND PANEL ─── */}\n \n {/* Header row: SEND + add asset */}\n \n \n Send\n \n onOpenSourcePicker()}\n style={{\n alignItems: \"center\",\n background: \"transparent\",\n border: \"none\",\n borderRadius: \"6px\",\n display: \"flex\",\n gap: \"4px\",\n padding: \"2px 0\",\n color:\n fromTokens.length > 0 && !isSourcePickerDisabled\n ? brand\n : \"#A8A8A6\",\n cursor:\n fromTokens.length > 0 && !isSourcePickerDisabled\n ? \"pointer\"\n : \"not-allowed\",\n fontFamily: '\"Geist\", system-ui, sans-serif',\n fontSize: \"12px\",\n fontWeight: 500,\n lineHeight: \"18px\",\n opacity:\n fromTokens.length > 0 && !isSourcePickerDisabled ? 1 : 0.75,\n }}\n type=\"button\"\n >\n \n +\n \n Add more assets\n \n \n\n {/* Render each selected source asset, or an empty one if none */}\n \n {sourceRowsToRender.map(({ token, index, position }) => {\n const showTooltipBelow = position === 0;\n return (\n {\n sourceRowRefs.current[index] = element;\n }}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"5px\",\n opacity: 1,\n position: \"relative\",\n transform: \"translateY(0)\",\n transition: \"opacity 0.18s ease, transform 0.18s ease\",\n zIndex: tooltip === `asset-send-${index}` ? 1000 : 1,\n }}\n >\n \n \n {showSourceRouteSkeleton ? (\n \n ) : (\n <>\n {token?.userAmountMode === \"usd\" && (\n \n $\n \n )}\n {\n if (token) handleBlurAmount(index);\n setFocusedRow(null);\n }}\n onChange={(e) => {\n if (token)\n handleTokenAmountChange(index, e.target.value);\n else handleSendInput(e);\n }}\n onFocus={() => setFocusedRow(index)}\n placeholder=\"0\"\n ref={(element) => {\n sourceInputRefs.current[index] = element;\n }}\n style={{\n boxSizing: \"border-box\",\n color: (\n token\n ? Boolean(token.userAmount)\n : Boolean(isExactIn && amount)\n )\n ? \"#161615\"\n : \"#9E9E9C\",\n cursor: isAmountReadOnly ? \"default\" : \"text\",\n fontFamily:\n '\"Delight-Medium\", \"Delight\", system-ui, sans-serif',\n fontSize: \"29px\",\n fontWeight: 500,\n lineHeight: \"34px\",\n background: \"transparent\",\n border: \"none\",\n outline: \"none\",\n padding: 0,\n width: \"100%\",\n minWidth: 0,\n }}\n readOnly={isAmountReadOnly}\n type=\"text\"\n value={\n token\n ? focusedRow === index\n ? token.userAmount || \"\"\n : formatAmountInputDisplay(\n token.userAmount || \"\",\n )\n : isExactIn\n ? focusedRow === index\n ? amount\n : formatAmountInputDisplay(amount)\n : \"\"\n }\n />\n \n )}\n \n\n {/* Asset selector pill + cross button */}\n \n {showSourceRouteSkeleton ? (\n \n \n \n ) : (\n onOpenSourcePicker(index)}\n style={{\n alignItems: \"center\",\n backgroundColor: \"#FFFFFE\",\n borderColor: token ? \"#E8E8E7\" : \"#C8C8C7\",\n borderRadius: \"999px\",\n borderStyle: token ? \"solid\" : \"dashed\",\n borderWidth: \"1px\",\n boxShadow: token ? \"#1616150A 0px 1px 2px\" : \"none\",\n boxSizing: \"border-box\",\n display: \"flex\",\n gap: \"6px\",\n paddingBottom: \"3px\",\n paddingLeft: token ? \"3px\" : \"7px\",\n paddingRight: \"8px\",\n paddingTop: \"3px\",\n cursor: isSourcePickerDisabled\n ? \"not-allowed\"\n : \"pointer\",\n flexShrink: 0,\n opacity: isSourcePickerDisabled ? 0.72 : 1,\n }}\n >\n {token ? (\n token.isUnified ? (\n \n ) : (\n \n \n {token.chainLogo && (\n \n )}\n \n )\n ) : (\n \n )}\n \n {token ? token.symbol : \"Assets\"}\n \n \n \n )}\n {token && fromTokens.length > 1 && (\n {\n if (!onUpdateTokens) return;\n const next = [...fromTokens];\n next.splice(index, 1);\n onUpdateTokens(next);\n const total = getTokenAmountTotal(next);\n onAmountChange(\n total > 0 ? String(total) : \"\",\n \"send\",\n );\n }}\n style={{\n width: \"18px\",\n height: \"18px\",\n borderRadius: \"999px\",\n backgroundColor: \"#F0F0EF\",\n border: \"none\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n flexShrink: 0,\n }}\n >\n \n \n \n \n \n )}\n \n \n\n {/* USD value + balance row */}\n \n \n {showSourceRouteSkeleton ? (\n \n ) : (\n (() => {\n if (!token)\n return (\n \n ≈ ${usdValue || \"0.00\"}\n \n );\n const tokenBalance =\n Number(\n String(token.balance).replace(/[^0-9.]/g, \"\"),\n ) || 0;\n const fiatBalance =\n Number(\n String(token.balanceInFiat).replace(/[^0-9.]/g, \"\"),\n ) || 0;\n const price =\n tokenBalance > 0 ? fiatBalance / tokenBalance : 0;\n const isUsdMode = token.userAmountMode === \"usd\";\n const userAmtNum = Number(token.userAmount || 0);\n const quotedUsd = parseDecimal(token.userAmountUsd);\n const approxValue = isUsdMode\n ? price > 0\n ? (userAmtNum / price).toFixed(6)\n : \"0.000000\"\n : quotedUsd\n ? quotedUsd.toDecimalPlaces(2).toFixed()\n : (userAmtNum * price).toFixed(2);\n const approxPrefix = isUsdMode ? \"≈\" : \"≈ $\";\n const approxSuffix = isUsdMode\n ? ` ${token.symbol}`\n : \"\";\n\n return (\n handleToggleMode(index)}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"6px\",\n cursor:\n price > 0 && !isAmountReadOnly\n ? \"pointer\"\n : \"default\",\n }}\n >\n \n {approxPrefix}\n {approxValue}\n {approxSuffix}\n \n {price > 0 && }\n \n );\n })()\n )}\n \n\n \n {token && !isAmountReadOnly && (\n \n token\n ? handleSendPercentForToken(index, pct, token)\n : handleSendPercent(pct)\n }\n visible={Boolean(token) && focusedRow === index}\n />\n )}\n \n\n \n {showSourceRouteSkeleton || isBalanceLoading ? (\n \n ) : token ? (\n {\n setTooltip(`asset-send-${index}`);\n setTooltipTriggerRect(\n e.currentTarget.getBoundingClientRect(),\n );\n }}\n onMouseLeave={() => {\n setTooltip(null);\n setTooltipTriggerRect(null);\n }}\n style={{\n alignItems: \"center\",\n boxSizing: \"border-box\",\n display: \"flex\",\n gap: \"4px\",\n position: \"relative\",\n cursor: \"default\",\n whiteSpace: \"nowrap\",\n }}\n >\n \n Balance ·\n \n \n {formatTokenBalanceLabel(token)}\n \n\n {/* Tooltip */}\n {tooltip === `asset-send-${index}` &&\n tooltipTriggerRect &&\n createPortal(\n \n \n Asset Balance\n \n \n This is your current asset balance on this\n chain.\n \n ,\n document.body,\n )}\n \n ) : null}\n \n \n\n {/* PercentButtons moved inline next to balance */}\n \n );\n })}\n \n\n {hasSourceOverflow && (\n {\n const element = sourceListRef.current;\n if (!element) return;\n element.scrollTo({\n behavior: \"smooth\",\n top: isSourceListAtBottom ? 0 : element.scrollTop + 80,\n });\n }}\n style={{\n alignItems: \"center\",\n alignSelf: \"center\",\n background: \"transparent\",\n border: \"none\",\n color: \"#686866\",\n cursor: \"pointer\",\n display: \"flex\",\n fontFamily: '\"Geist\", system-ui, sans-serif',\n fontSize: \"13px\",\n fontWeight: 500,\n gap: \"4px\",\n lineHeight: \"18px\",\n marginTop: \"-2px\",\n padding: 0,\n }}\n type=\"button\"\n >\n Scroll to view more assets\n {isSourceListAtBottom ? \"↑\" : \"↓\"}\n \n )}\n\n {sourceRouteHelper && (\n \n {sourceRouteHelper}\n \n )}\n\n {/* Total USD */}\n {totalUsd > 0 && (\n \n \n ≈ ${totalUsd.toFixed(2)}\n \n \n TOTAL\n \n \n )}\n \n\n {/* ─── RECEIVE PANEL ─── */}\n \n \n Receive\n \n\n \n \n {isReceiveAmountLoading ? (\n \n \n \n ) : (\n \n )}\n\n {/* Destination asset pill */}\n \n {toToken ? (\n \n \n {toToken.chainLogo && (\n \n )}\n \n ) : (\n \n )}\n \n {toToken ? toToken.symbol : \"Assets\"}\n \n {!hideDestinationTokenDropdownIcon && }\n \n \n\n {/* USD value + balance row */}\n \n {isReceiveUsdLoading ? (\n \n ) : (\n \n {receiveAltValue}\n \n )}\n {toToken && focusedPanel === \"receive\" && (\n setTooltip(\"asset-receive\")}\n onMouseLeave={() => setTooltip(null)}\n style={{\n alignItems: \"center\",\n boxSizing: \"border-box\",\n display: \"flex\",\n gap: \"5px\",\n position: \"relative\",\n cursor: \"default\",\n }}\n >\n \n Asset Balance ·\n \n \n {isBalanceLoading ? (\n \n ) : (\n receiveBalanceLabel\n )}\n \n\n {/* Tooltip */}\n {tooltip === \"asset-receive\" && (\n \n \n Asset Balance\n \n \n This is your current asset balance on this chain.\n \n \n )}\n \n )}\n \n \n\n {/* Recipient section — only shown when handler exists */}\n {onOpenRecipientPicker && (\n <>\n \n \n \n Recipient\n \n \n \n {recipientAddress\n ? formatShortAddress(recipientAddress)\n : \"Select recipient\"}\n \n \n \n Edit\n \n \n \n \n \n )}\n \n \n );\n}\n", + "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\nimport Decimal from \"decimal.js\";\nimport React, { useEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n formatSelectedTokenBalanceLabel,\n formatUsdBalanceLabel,\n type SwapTokenOption,\n} from \"./swap-asset-selector\";\n\nconst tabularNums: React.CSSProperties = {\n fontFeatureSettings: '\"tnum\"',\n fontVariantNumeric: \"tabular-nums\",\n};\n\nconst brand = \"var(--foreground-brand)\";\n\ninterface SwapIdleFormProps {\n amount: string;\n defaultRecipientAddress?: string;\n fromTokens: SwapTokenOption[];\n isAmountReadOnly?: boolean;\n isBalanceLoading?: boolean;\n isDestinationPickerDisabled?: boolean;\n hideDestinationTokenDropdownIcon?: boolean;\n isReceiveAmountLoading?: boolean;\n isReceiveUsdLoading?: boolean;\n isSourcePickerDisabled?: boolean;\n onAmountChange: (val: string, panel: \"send\" | \"receive\") => void;\n onOpenDestPicker: () => void;\n onOpenRecipientPicker?: () => void;\n onOpenSourcePicker: (index?: number) => void;\n onUpdateTokens?: (tokens: SwapTokenOption[]) => void;\n receiveQuoteAmount?: string;\n receiveQuoteUsd?: string;\n recipientAddress?: string;\n sourceRouteMessage?: string;\n sourceRouteStatus?: \"loading\" | \"insufficient\";\n swapType: \"exactIn\" | \"exactOut\";\n toToken?: SwapTokenOption;\n totalBalance: string;\n usdValue: string;\n}\n\n/** Chevron down icon used in asset selector pills */\nconst ChevronDownIcon = () => (\n \n \n \n);\n\nconst ArrowUpDownIcon = () => (\n \n \n \n);\n\n/** Reusable percentage quick-select buttons row with transition wrapper */\nfunction PercentButtons({\n visible,\n onSelect,\n maxLabel = \"Max\",\n}: {\n visible: boolean;\n onSelect: (pct: number) => void;\n maxLabel?: string;\n}) {\n const [hoveredPct, setHoveredPct] = useState(null);\n\n return (\n \n {[20, 50, 100].map((pct) => {\n const label = pct === 100 ? maxLabel : `${pct}%`;\n const isHovered = hoveredPct === pct;\n\n return (\n {\n e.stopPropagation();\n onSelect(pct);\n }}\n onMouseDown={(e) => {\n e.preventDefault();\n }}\n onMouseEnter={() => setHoveredPct(pct)}\n onMouseLeave={() => setHoveredPct(null)}\n style={{\n alignItems: \"center\",\n backgroundColor: isHovered ? \"#FFFFFF\" : \"transparent\",\n borderRadius: \"4px\",\n boxShadow: isHovered ? \"#3C286414 0px 1px 2px\" : \"none\",\n boxSizing: \"border-box\",\n color: isHovered ? \"#1F1F1F\" : \"#8E8E89\",\n cursor: \"pointer\",\n display: \"flex\",\n fontFamily: '\"Geist\", system-ui, sans-serif',\n fontSize: \"10px\",\n fontWeight: 500,\n height: \"18px\",\n justifyContent: \"center\",\n flex: \"1 1 0%\",\n minWidth: 0,\n paddingInline: \"3px\",\n border: \"none\",\n transition: \"all 0.15s ease-out\",\n }}\n tabIndex={-1}\n type=\"button\"\n >\n {label}\n \n );\n })}\n \n );\n}\n\nfunction UnifiedTokenLogoBadge({\n token,\n size = 24,\n}: {\n token: SwapTokenOption;\n size?: number;\n}) {\n const [popover, setPopover] = useState<{\n left: number;\n top: number;\n width: number;\n maxHeight: number;\n } | null>(null);\n const triggerRef = useRef(null);\n const sources = token.sourceTokens ?? [];\n const chainCount =\n new Set(\n sources\n .map((source) => source.chainId ?? source.chainName)\n .filter(Boolean),\n ).size || sources.length;\n\n const showPopover = () => {\n const rect = triggerRef.current?.getBoundingClientRect();\n if (!rect || typeof window === \"undefined\") return;\n const width = 250;\n const maxHeight = 260;\n const viewportPadding = 8;\n const left = Math.min(\n Math.max(viewportPadding, rect.right - width),\n window.innerWidth - width - viewportPadding,\n );\n const belowTop = rect.bottom + 8;\n const top =\n belowTop + maxHeight > window.innerHeight\n ? Math.max(viewportPadding, rect.top - maxHeight - 8)\n : belowTop;\n setPopover({ left, top, width, maxHeight });\n };\n\n return (\n setPopover(null)}\n ref={triggerRef}\n style={{\n boxSizing: \"border-box\",\n flexShrink: 0,\n height: `${size}px`,\n position: \"relative\",\n width: `${size}px`,\n }}\n >\n \n {chainCount > 0 && (\n 9 ? \"3px\" : 0,\n position: \"absolute\",\n right: -3,\n }}\n >\n {chainCount}\n \n )}\n {popover &&\n sources.length > 0 &&\n typeof document !== \"undefined\" &&\n createPortal(\n \n \n \n Unified · {chainCount} {chainCount === 1 ? \"Chain\" : \"Chains\"}\n \n \n ≈ {formatUsdBalanceLabel(token.balanceInFiat)}\n \n \n \n {sources.map((source) => (\n \n \n \n \n {source.chainName || \"Unknown chain\"}\n \n \n \n {formatAmountInputDisplay(source.balance || \"0\")}\n \n \n ))}\n \n ,\n document.body,\n )}\n \n );\n}\n\nfunction PercentHoverButton({\n label,\n onClick,\n tabIndex,\n}: {\n label: string;\n onClick: () => void;\n tabIndex?: number;\n}) {\n const [hover, setHover] = useState(false);\n const [active, setActive] = useState(false);\n const handledPointerDownRef = useRef(false);\n const pointerResetTimerRef = useRef | null>(\n null,\n );\n\n useEffect(() => {\n return () => {\n if (pointerResetTimerRef.current) {\n clearTimeout(pointerResetTimerRef.current);\n }\n };\n }, []);\n\n const isHighlighted = hover || active;\n\n return (\n {\n if (handledPointerDownRef.current) {\n if (pointerResetTimerRef.current) {\n clearTimeout(pointerResetTimerRef.current);\n pointerResetTimerRef.current = null;\n }\n handledPointerDownRef.current = false;\n return;\n }\n onClick();\n }}\n onMouseDown={(event) => {\n event.preventDefault();\n setActive(true);\n }}\n onMouseEnter={() => setHover(true)}\n onMouseLeave={() => {\n setHover(false);\n setActive(false);\n }}\n onMouseUp={() => setActive(false)}\n onPointerDown={(event) => {\n if (event.pointerType === \"mouse\") return;\n event.preventDefault();\n if (pointerResetTimerRef.current) {\n clearTimeout(pointerResetTimerRef.current);\n }\n handledPointerDownRef.current = true;\n setActive(true);\n onClick();\n }}\n onPointerUp={() => {\n setActive(false);\n if (handledPointerDownRef.current) {\n pointerResetTimerRef.current = setTimeout(() => {\n handledPointerDownRef.current = false;\n pointerResetTimerRef.current = null;\n }, 350);\n }\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: isHighlighted ? \"#E8F0FF\" : \"#F4F4F3\",\n borderRadius: \"6px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flex: \"1 1 0%\",\n justifyContent: \"center\",\n paddingBlock: \"3px\",\n paddingInline: \"6px\",\n border: \"none\",\n cursor: \"pointer\",\n transition: \"background-color 0.2s ease-out\",\n }}\n tabIndex={tabIndex}\n >\n \n {label}\n \n \n );\n}\n\nfunction SkeletonBar({\n width,\n height,\n borderRadius = \"8px\",\n}: {\n width: string;\n height: string;\n borderRadius?: string;\n}) {\n return (\n \n );\n}\n\nfunction LogoCircle({\n src,\n alt,\n label,\n size,\n fontSize,\n outline,\n style,\n}: {\n src?: string;\n alt?: string;\n label?: string;\n size: number;\n fontSize: number;\n outline?: string;\n style?: React.CSSProperties;\n}) {\n const [failed, setFailed] = useState(!src);\n\n useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n const fallbackLabel = (label || alt || \"?\").trim().slice(0, 1).toUpperCase();\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n backgroundColor: \"#FFFFFE\",\n borderRadius: \"999px\",\n height: `${size}px`,\n objectFit: \"cover\",\n outline,\n width: `${size}px`,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {fallbackLabel || \"?\"}\n \n );\n}\n\nconst sameAddress = (a?: string, b?: string) =>\n Boolean(a && b && a.toLowerCase() === b.toLowerCase());\n\nconst formatShortAddress = (address?: string) => {\n if (!address) return \"\";\n return address.length > 12\n ? `${address.slice(0, 6)}…${address.slice(-4)}`\n : address;\n};\n\nconst formatTokenBalanceLabel = formatSelectedTokenBalanceLabel;\n\nconst parseDecimal = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = String(value).replace(/[^0-9.-]/g, \"\");\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst formatUsdValue = (value: Decimal) =>\n value.gt(0) && value.lt(0.01) ? \"<0.01\" : value.toDecimalPlaces(2).toFixed(2);\n\nconst MAX_AMOUNT_DISPLAY_DECIMALS = 8;\nconst getTokenInputDecimals = (token?: Pick) => {\n const decimals = Number(token?.decimals);\n return Number.isFinite(decimals) && decimals >= 0 ? Math.floor(decimals) : 18;\n};\n\nconst formatAmountInputDisplay = (value: string) => {\n if (!value) return \"\";\n try {\n return new Decimal(value)\n .toDecimalPlaces(MAX_AMOUNT_DISPLAY_DECIMALS, Decimal.ROUND_DOWN)\n .toFixed();\n } catch {\n return value;\n }\n};\n\nexport function SwapIdleForm({\n amount,\n receiveQuoteAmount,\n receiveQuoteUsd,\n isReceiveAmountLoading = false,\n isReceiveUsdLoading = false,\n sourceRouteStatus,\n sourceRouteMessage,\n onAmountChange,\n fromTokens,\n toToken,\n totalBalance,\n usdValue,\n onOpenSourcePicker,\n onOpenDestPicker,\n onOpenRecipientPicker,\n recipientAddress,\n defaultRecipientAddress,\n swapType,\n onUpdateTokens,\n isAmountReadOnly = false,\n isBalanceLoading = false,\n isDestinationPickerDisabled = false,\n hideDestinationTokenDropdownIcon = false,\n isSourcePickerDisabled = false,\n}: SwapIdleFormProps) {\n const [focusedPanel, setFocusedPanel] = useState<\"send\" | \"receive\" | null>(\n null,\n );\n const [focusedRow, setFocusedRow] = useState(null);\n const [tooltip, setTooltip] = useState(null);\n const [tooltipTriggerRect, setTooltipTriggerRect] = useState(\n null,\n );\n const sourceListRef = useRef(null);\n const sourceRowRefs = useRef>({});\n const sourceInputRefs = useRef>({});\n const previousSourceCountRef = useRef(fromTokens.length);\n const prevTokensRef = useRef(fromTokens);\n const [autofocusIndex, setAutofocusIndex] = useState(null);\n\n useEffect(() => {\n const prev = prevTokensRef.current;\n if (fromTokens.length > prev.length) {\n setAutofocusIndex(fromTokens.length - 1);\n } else if (fromTokens.length === prev.length) {\n for (let i = 0; i < fromTokens.length; i++) {\n const p = prev[i];\n const c = fromTokens[i];\n if (\n p &&\n c &&\n (p.contractAddress !== c.contractAddress ||\n p.chainId !== c.chainId) &&\n !c.userAmount\n ) {\n setAutofocusIndex(i);\n break;\n }\n }\n }\n prevTokensRef.current = fromTokens;\n }, [fromTokens]);\n\n useEffect(() => {\n const previousSourceCount = previousSourceCountRef.current;\n if (fromTokens.length > previousSourceCount && previousSourceCount > 0) {\n const newIndex = fromTokens.length - 1;\n requestAnimationFrame(() => {\n const input = sourceInputRefs.current[newIndex];\n if (input) {\n input.focus();\n input.select();\n }\n\n const container = sourceListRef.current;\n const row = sourceRowRefs.current[newIndex];\n if (\n !container ||\n !row ||\n container.scrollHeight <= container.clientHeight\n ) {\n return;\n }\n\n const containerRect = container.getBoundingClientRect();\n const rowRect = row.getBoundingClientRect();\n const nextTop =\n rowRect.top - containerRect.top + container.scrollTop - 8;\n\n container.scrollTo({\n behavior: \"smooth\",\n top: Math.max(0, nextTop),\n });\n });\n }\n previousSourceCountRef.current = fromTokens.length;\n }, [fromTokens.length]);\n\n const sanitizeInput = (raw: string, maxDecimals = 18): string => {\n let next = raw.replaceAll(/[^0-9.]/g, \"\");\n const parts = next.split(\".\");\n if (parts.length > 2) next = parts[0] + \".\" + parts.slice(1).join(\"\");\n const [integerPart, decimalPart] = next.split(\".\");\n if (decimalPart !== undefined) {\n next = `${integerPart}.${decimalPart.slice(0, Math.max(0, maxDecimals))}`;\n }\n if (next === \".\") next = \"0.\";\n // Strip leading zeros\n if (next.length > 1 && next.startsWith(\"0\") && next[1] !== \".\") {\n next = next.replace(/^0+/, \"\");\n if (next === \"\") next = \"0\";\n if (next.startsWith(\".\")) next = \"0\" + next;\n }\n return next;\n };\n\n const handleBlurAmount = (index: number) => {\n if (!onUpdateTokens) return;\n const token = fromTokens[index];\n if (!token || !token.userAmount) return;\n if (token.userAmount.includes(\".\")) {\n const stripped = token.userAmount.replace(/0+$/, \"\").replace(/\\.$/, \"\");\n if (stripped !== token.userAmount) {\n const next = [...fromTokens];\n next[index] = { ...token, userAmount: stripped };\n onUpdateTokens(next);\n }\n }\n };\n\n const handleSendInput = (e: React.ChangeEvent) => {\n if (isAmountReadOnly) return;\n const token = fromTokens.length === 1 ? fromTokens[0] : undefined;\n onAmountChange(\n sanitizeInput(e.target.value, getTokenInputDecimals(token)),\n \"send\",\n );\n };\n\n const handleTokenAmountChange = (index: number, val: string) => {\n if (isAmountReadOnly) return;\n if (!onUpdateTokens) return;\n const token = fromTokens[index];\n if (!token) return;\n\n let sanitized = sanitizeInput(\n val,\n token.userAmountMode === \"usd\"\n ? MAX_AMOUNT_DISPLAY_DECIMALS\n : getTokenInputDecimals(token),\n );\n\n const next = [...fromTokens];\n next[index] = { ...token, userAmount: sanitized };\n onUpdateTokens(next);\n\n // Also update total amount for backwards compatibility if needed\n const total = next.reduce((sum, t) => sum + Number(t.userAmount || 0), 0);\n onAmountChange(total > 0 ? String(total) : \"\", \"send\");\n };\n\n const handleToggleMode = (index: number) => {\n if (isAmountReadOnly) return;\n if (!onUpdateTokens) return;\n const token = fromTokens[index];\n if (!token) return;\n\n const tokenBalance =\n Number(String(token.balance).replace(/[^0-9.]/g, \"\")) || 0;\n const fiatBalance =\n Number(String(token.balanceInFiat).replace(/[^0-9.]/g, \"\")) || 0;\n const price = tokenBalance > 0 ? fiatBalance / tokenBalance : 0;\n if (price === 0) return;\n\n const currentVal = Number(token.userAmount || 0);\n const next = [...fromTokens];\n if (token.userAmountMode === \"usd\") {\n const newTokenVal = currentVal > 0 ? (currentVal / price).toString() : \"\";\n next[index] = {\n ...token,\n userAmountMode: \"token\",\n userAmount: newTokenVal ? newTokenVal.substring(0, 10) : \"\",\n };\n } else {\n const newUsdVal = currentVal > 0 ? (currentVal * price).toFixed(2) : \"\";\n next[index] = { ...token, userAmountMode: \"usd\", userAmount: newUsdVal };\n }\n onUpdateTokens(next);\n const total = getTokenAmountTotal(next);\n onAmountChange(total > 0 ? String(total) : \"\", \"send\");\n };\n\n const getSourceUsdValue = React.useCallback((token: SwapTokenOption) => {\n if (!token || !token.userAmount) return 0;\n const quotedUsd = parseDecimal(token.userAmountUsd);\n if (quotedUsd && quotedUsd.gte(0)) return quotedUsd.toNumber();\n const tokenBalance =\n Number(String(token.balance).replace(/[^0-9.]/g, \"\")) || 0;\n const fiatBalance =\n Number(String(token.balanceInFiat).replace(/[^0-9.]/g, \"\")) || 0;\n const price = tokenBalance > 0 ? fiatBalance / tokenBalance : 0;\n const amountNumber = Number(token.userAmount || 0);\n if (!Number.isFinite(amountNumber)) return 0;\n if (token.userAmountMode === \"usd\") return amountNumber;\n return amountNumber * price;\n }, []);\n\n const totalUsd = React.useMemo(() => {\n return fromTokens.reduce((sum, token) => sum + getSourceUsdValue(token), 0);\n }, [fromTokens, getSourceUsdValue]);\n\n const hasSourceOverflow = fromTokens.length > 3;\n const [isSourceListAtBottom, setIsSourceListAtBottom] = useState(false);\n const updateSourceListScrollState = React.useCallback(() => {\n const element = sourceListRef.current;\n if (!element || !hasSourceOverflow) {\n setIsSourceListAtBottom(false);\n return;\n }\n\n const distanceFromBottom =\n element.scrollHeight - element.scrollTop - element.clientHeight;\n setIsSourceListAtBottom(distanceFromBottom <= 2);\n }, [hasSourceOverflow]);\n\n useEffect(() => {\n requestAnimationFrame(updateSourceListScrollState);\n }, [fromTokens.length, updateSourceListScrollState]);\n\n const sourceRowsToRender: Array<{\n token: SwapTokenOption | null;\n index: number;\n position: number;\n }> =\n fromTokens.length > 0\n ? fromTokens.map((token, index) => ({ token, index, position: index }))\n : [{ token: null, index: 0, position: 0 }];\n\n const isExactIn = swapType === \"exactIn\";\n const showSourceRouteSkeleton = !isExactIn && sourceRouteStatus === \"loading\";\n const sourceRouteHelper =\n sourceRouteStatus === \"insufficient\" ? sourceRouteMessage : undefined;\n const receiveBalanceLabel = formatTokenBalanceLabel(toToken);\n const getReceiveUsdRate = () => {\n const quoteTokenAmount = parseDecimal(receiveQuoteAmount);\n const quoteUsdAmount = parseDecimal(receiveQuoteUsd);\n if (quoteTokenAmount?.gt(0) && quoteUsdAmount?.gt(0)) {\n return quoteUsdAmount.div(quoteTokenAmount);\n }\n\n const tokenBalance = parseDecimal(toToken?.balance);\n const fiatBalance = parseDecimal(toToken?.balanceInFiat);\n if (tokenBalance?.gt(0) && fiatBalance?.gt(0)) {\n return fiatBalance.div(tokenBalance);\n }\n\n return undefined;\n };\n const receiveInputValue = isExactIn ? (receiveQuoteAmount ?? \"\") : amount;\n const receiveDisplayValue =\n focusedPanel === \"receive\"\n ? receiveInputValue\n : formatAmountInputDisplay(receiveInputValue);\n const receiveAmountTextColor =\n (!isExactIn && amount) || (isExactIn && receiveQuoteAmount)\n ? \"#161615\"\n : \"#9E9E9C\";\n const receiveUsdRate = getReceiveUsdRate();\n const receiveTokenAmount = parseDecimal(receiveInputValue);\n const receiveUsdAmount = receiveQuoteUsd\n ? parseDecimal(receiveQuoteUsd)\n : receiveTokenAmount && receiveUsdRate\n ? receiveTokenAmount.mul(receiveUsdRate)\n : undefined;\n const receiveAltValue = `≈ $${\n receiveUsdAmount ? formatUsdValue(receiveUsdAmount) : \"0.00\"\n }`;\n const isDefaultRecipient = sameAddress(\n recipientAddress,\n defaultRecipientAddress,\n );\n const recipientColor = recipientAddress\n ? isDefaultRecipient\n ? brand\n : \"#B7791F\"\n : \"#848483\";\n const getTokenAmountTotal = (tokens: SwapTokenOption[]) =>\n tokens.reduce((sum, item) => sum + Number(item.userAmount || 0), 0);\n\n const handleSendPercentForToken = (\n index: number,\n pct: number,\n token: SwapTokenOption,\n ) => {\n if (isAmountReadOnly) return;\n if (!token.balance || !onUpdateTokens) return;\n let finalVal = \"\";\n const isUsdMode = token.userAmountMode === \"usd\";\n\n if (isUsdMode) {\n const fiatBalStr = String(token.balanceInFiat || \"0\");\n const fiatBalance = parseDecimal(fiatBalStr);\n if (!fiatBalance) return;\n if (pct === 100) {\n finalVal = fiatBalance\n .toDecimalPlaces(MAX_AMOUNT_DISPLAY_DECIMALS, Decimal.ROUND_DOWN)\n .toFixed();\n } else {\n finalVal = fiatBalance\n .mul(pct)\n .div(100)\n .toDecimalPlaces(MAX_AMOUNT_DISPLAY_DECIMALS, Decimal.ROUND_DOWN)\n .toFixed();\n }\n } else {\n const balanceStr = String(token.balance || \"0\");\n const tokenBalance = parseDecimal(balanceStr);\n if (!tokenBalance) return;\n const tokenDecimals = getTokenInputDecimals(token);\n if (pct === 100) {\n finalVal = tokenBalance\n .toDecimalPlaces(tokenDecimals, Decimal.ROUND_DOWN)\n .toFixed();\n } else {\n finalVal = tokenBalance\n .mul(pct)\n .div(100)\n .toDecimalPlaces(tokenDecimals, Decimal.ROUND_DOWN)\n .toFixed();\n }\n }\n\n const next = [...fromTokens];\n next[index] = {\n ...next[index],\n userAmount: finalVal,\n userAmountMode: isUsdMode ? \"usd\" : \"token\",\n };\n onUpdateTokens(next);\n const total = getTokenAmountTotal(next);\n onAmountChange(total > 0 ? String(total) : \"\", \"send\");\n };\n\n const handleSendPercent = (pct: number) => {\n if (isAmountReadOnly) return;\n if (!totalBalance) return;\n const bal = parseFloat(totalBalance.replace(/[^0-9.]/g, \"\"));\n if (isNaN(bal)) return;\n const val = bal * (pct / 100);\n // If there's only one token, or no tokens, update the main amount\n if (fromTokens.length <= 1) {\n if (fromTokens.length === 1 && onUpdateTokens) {\n handleSendPercentForToken(0, pct, fromTokens[0]);\n return;\n }\n onAmountChange(val.toFixed(6).replace(/\\.?0+$/, \"\"), \"send\");\n }\n };\n\n return (\n \n {(isReceiveAmountLoading || isReceiveUsdLoading || sourceRouteStatus) && (\n \n )}\n {/* ─── SEND PANEL ─── */}\n \n {/* Header row: SEND + add asset */}\n \n \n Send\n \n onOpenSourcePicker()}\n style={{\n alignItems: \"center\",\n background: \"transparent\",\n border: \"none\",\n borderRadius: \"6px\",\n display: \"flex\",\n gap: \"4px\",\n padding: \"2px 0\",\n color:\n fromTokens.length > 0 && !isSourcePickerDisabled\n ? brand\n : \"#A8A8A6\",\n cursor:\n fromTokens.length > 0 && !isSourcePickerDisabled\n ? \"pointer\"\n : \"not-allowed\",\n fontFamily: '\"Geist\", system-ui, sans-serif',\n fontSize: \"12px\",\n fontWeight: 500,\n lineHeight: \"18px\",\n opacity:\n fromTokens.length > 0 && !isSourcePickerDisabled ? 1 : 0.75,\n }}\n type=\"button\"\n >\n \n +\n \n Add more assets\n \n \n\n {/* Render each selected source asset, or an empty one if none */}\n \n {sourceRowsToRender.map(({ token, index, position }) => {\n const showTooltipBelow = position === 0;\n return (\n {\n sourceRowRefs.current[index] = element;\n }}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"5px\",\n opacity: 1,\n position: \"relative\",\n transform: \"translateY(0)\",\n transition: \"opacity 0.18s ease, transform 0.18s ease\",\n zIndex: tooltip === `asset-send-${index}` ? 1000 : 1,\n }}\n >\n \n \n {showSourceRouteSkeleton ? (\n \n ) : (\n <>\n {token?.userAmountMode === \"usd\" && (\n \n $\n \n )}\n {\n if (token) handleBlurAmount(index);\n setFocusedRow(null);\n }}\n onChange={(e) => {\n if (token)\n handleTokenAmountChange(index, e.target.value);\n else handleSendInput(e);\n }}\n onFocus={() => setFocusedRow(index)}\n placeholder=\"0\"\n ref={(element) => {\n sourceInputRefs.current[index] = element;\n }}\n style={{\n boxSizing: \"border-box\",\n color: (\n token\n ? Boolean(token.userAmount)\n : Boolean(isExactIn && amount)\n )\n ? \"#161615\"\n : \"#9E9E9C\",\n cursor: isAmountReadOnly ? \"default\" : \"text\",\n fontFamily:\n '\"Delight-Medium\", \"Delight\", system-ui, sans-serif',\n fontSize: \"29px\",\n fontWeight: 500,\n lineHeight: \"34px\",\n background: \"transparent\",\n border: \"none\",\n outline: \"none\",\n padding: 0,\n width: \"100%\",\n minWidth: 0,\n }}\n readOnly={isAmountReadOnly}\n type=\"text\"\n value={\n token\n ? focusedRow === index\n ? token.userAmount || \"\"\n : formatAmountInputDisplay(\n token.userAmount || \"\",\n )\n : isExactIn\n ? focusedRow === index\n ? amount\n : formatAmountInputDisplay(amount)\n : \"\"\n }\n />\n \n )}\n \n\n {/* Asset selector pill + cross button */}\n \n {showSourceRouteSkeleton ? (\n \n \n \n ) : (\n onOpenSourcePicker(index)}\n style={{\n alignItems: \"center\",\n backgroundColor: \"#FFFFFE\",\n borderColor: token ? \"#E8E8E7\" : \"#C8C8C7\",\n borderRadius: \"999px\",\n borderStyle: token ? \"solid\" : \"dashed\",\n borderWidth: \"1px\",\n boxShadow: token ? \"#1616150A 0px 1px 2px\" : \"none\",\n boxSizing: \"border-box\",\n display: \"flex\",\n gap: \"8px\",\n paddingBottom: \"5px\",\n paddingLeft: token ? \"5px\" : \"10px\",\n paddingRight: \"10px\",\n paddingTop: \"5px\",\n cursor: isSourcePickerDisabled\n ? \"not-allowed\"\n : \"pointer\",\n flexShrink: 0,\n opacity: isSourcePickerDisabled ? 0.72 : 1,\n }}\n >\n {token ? (\n token.isUnified ? (\n \n ) : (\n \n \n {token.chainLogo && (\n \n )}\n \n )\n ) : (\n \n )}\n \n {token ? token.symbol : \"Assets\"}\n \n \n \n )}\n {token && fromTokens.length > 1 && (\n {\n if (!onUpdateTokens) return;\n const next = [...fromTokens];\n next.splice(index, 1);\n onUpdateTokens(next);\n const total = getTokenAmountTotal(next);\n onAmountChange(\n total > 0 ? String(total) : \"\",\n \"send\",\n );\n }}\n style={{\n width: \"18px\",\n height: \"18px\",\n borderRadius: \"999px\",\n backgroundColor: \"#F0F0EF\",\n border: \"none\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n flexShrink: 0,\n }}\n >\n \n \n \n \n \n )}\n \n \n\n {/* USD value + balance row */}\n \n \n {showSourceRouteSkeleton ? (\n \n ) : (\n (() => {\n if (!token)\n return (\n \n ≈ ${usdValue || \"0.00\"}\n \n );\n const tokenBalance =\n Number(\n String(token.balance).replace(/[^0-9.]/g, \"\"),\n ) || 0;\n const fiatBalance =\n Number(\n String(token.balanceInFiat).replace(/[^0-9.]/g, \"\"),\n ) || 0;\n const price =\n tokenBalance > 0 ? fiatBalance / tokenBalance : 0;\n const isUsdMode = token.userAmountMode === \"usd\";\n const userAmtNum = Number(token.userAmount || 0);\n const quotedUsd = parseDecimal(token.userAmountUsd);\n const approxValue = isUsdMode\n ? price > 0\n ? (userAmtNum / price).toFixed(6)\n : \"0.000000\"\n : quotedUsd\n ? quotedUsd.toDecimalPlaces(2).toFixed()\n : (userAmtNum * price).toFixed(2);\n const approxPrefix = isUsdMode ? \"≈\" : \"≈ $\";\n const approxSuffix = isUsdMode\n ? ` ${token.symbol}`\n : \"\";\n\n return (\n handleToggleMode(index)}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"6px\",\n cursor:\n price > 0 && !isAmountReadOnly\n ? \"pointer\"\n : \"default\",\n }}\n >\n \n {approxPrefix}\n {approxValue}\n {approxSuffix}\n \n {price > 0 && }\n \n );\n })()\n )}\n \n\n \n {token && !isAmountReadOnly && (\n \n token\n ? handleSendPercentForToken(index, pct, token)\n : handleSendPercent(pct)\n }\n visible={Boolean(token) && focusedRow === index}\n />\n )}\n \n\n \n {showSourceRouteSkeleton || isBalanceLoading ? (\n \n ) : token ? (\n {\n setTooltip(`asset-send-${index}`);\n setTooltipTriggerRect(\n e.currentTarget.getBoundingClientRect(),\n );\n }}\n onMouseLeave={() => {\n setTooltip(null);\n setTooltipTriggerRect(null);\n }}\n style={{\n alignItems: \"center\",\n boxSizing: \"border-box\",\n display: \"flex\",\n gap: \"4px\",\n position: \"relative\",\n cursor: \"default\",\n whiteSpace: \"nowrap\",\n }}\n >\n \n Balance ·\n \n \n {formatTokenBalanceLabel(token)}\n \n\n {/* Tooltip */}\n {tooltip === `asset-send-${index}` &&\n tooltipTriggerRect &&\n createPortal(\n \n \n Asset Balance\n \n \n This is your current asset balance on this\n chain.\n \n ,\n document.body,\n )}\n \n ) : null}\n \n \n\n {/* PercentButtons moved inline next to balance */}\n \n );\n })}\n \n\n {hasSourceOverflow && (\n {\n const element = sourceListRef.current;\n if (!element) return;\n element.scrollTo({\n behavior: \"smooth\",\n top: isSourceListAtBottom ? 0 : element.scrollTop + 80,\n });\n }}\n style={{\n alignItems: \"center\",\n alignSelf: \"center\",\n background: \"transparent\",\n border: \"none\",\n color: \"#686866\",\n cursor: \"pointer\",\n display: \"flex\",\n fontFamily: '\"Geist\", system-ui, sans-serif',\n fontSize: \"13px\",\n fontWeight: 500,\n gap: \"4px\",\n lineHeight: \"18px\",\n marginTop: \"-2px\",\n padding: 0,\n }}\n type=\"button\"\n >\n Scroll to view more assets\n {isSourceListAtBottom ? \"↑\" : \"↓\"}\n \n )}\n\n {sourceRouteHelper && (\n \n {sourceRouteHelper}\n \n )}\n\n {/* Total USD */}\n {totalUsd > 0 && (\n \n \n ≈ ${totalUsd.toFixed(2)}\n \n \n TOTAL\n \n \n )}\n \n\n {/* ─── RECEIVE PANEL ─── */}\n \n \n Receive\n \n\n \n \n {isReceiveAmountLoading ? (\n \n \n \n ) : (\n \n )}\n\n {/* Destination asset pill */}\n \n {toToken ? (\n \n \n {toToken.chainLogo && (\n \n )}\n \n ) : (\n \n )}\n \n {toToken ? toToken.symbol : \"Assets\"}\n \n {!hideDestinationTokenDropdownIcon && }\n \n \n\n {/* USD value + balance row */}\n \n {isReceiveUsdLoading ? (\n \n ) : (\n \n {receiveAltValue}\n \n )}\n {toToken && focusedPanel === \"receive\" && (\n setTooltip(\"asset-receive\")}\n onMouseLeave={() => setTooltip(null)}\n style={{\n alignItems: \"center\",\n boxSizing: \"border-box\",\n display: \"flex\",\n gap: \"5px\",\n position: \"relative\",\n cursor: \"default\",\n }}\n >\n \n Asset Balance ·\n \n \n {isBalanceLoading ? (\n \n ) : (\n receiveBalanceLabel\n )}\n \n\n {/* Tooltip */}\n {tooltip === \"asset-receive\" && (\n \n \n Asset Balance\n \n \n This is your current asset balance on this chain.\n \n \n )}\n \n )}\n \n \n\n {/* Recipient section — only shown when handler exists */}\n {onOpenRecipientPicker && (\n <>\n \n \n \n Recipient\n \n \n \n {recipientAddress\n ? formatShortAddress(recipientAddress)\n : \"Select recipient\"}\n \n \n \n Edit\n \n \n \n \n \n )}\n \n \n );\n}\n", "type": "registry:component", "target": "components/nexus/components/swap-idle-form.tsx" }, @@ -98,7 +110,7 @@ }, { "path": "registry/avail-widgets/nexus-widget/nexus-widget.tsx", - "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\"use client\";\n\nimport {\n ERROR_CODES,\n type EthereumProvider,\n} from \"@avail-project/nexus-core\";\nimport Decimal from \"decimal.js\";\nimport { AlertCircle, ArrowLeft, ChevronDown, Loader2 } from \"lucide-react\";\nimport React, {\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { flushSync } from \"react-dom\";\nimport {\n createPublicClient,\n encodeFunctionData,\n erc20Abi,\n http,\n isAddress,\n parseUnits,\n zeroAddress,\n} from \"viem\";\nimport { mainnet } from \"viem/chains\";\nimport { normalize } from \"viem/ens\";\nimport {\n useAccount,\n useConnect,\n useConnectorClient,\n usePublicClient,\n useWalletClient,\n} from \"wagmi\";\nimport { ErrorBoundary } from \"../common/components/ErrorBoundary\";\nimport { useTransactionSteps } from \"../common/tx/useTransactionSteps\";\nimport type {\n BridgeStepType,\n SwapStepType,\n} from \"../common/types/transaction-flow\";\nimport {\n CHAIN_METADATA,\n getShortChainName,\n isSwapSupportedBySdkChainList,\n SUPPORTED_CHAINS,\n TOKEN_CONTRACT_ADDRESSES,\n TOKEN_METADATA,\n} from \"../common/utils/constant\";\nimport { type UserAsset, useNexus } from \"../nexus/NexusProvider\";\nimport { Button } from \"../ui/button\";\nimport { Dialog, DialogContent, DialogTrigger } from \"../ui/dialog\";\nimport { DepositIdleForm } from \"./components/deposit-idle-form\";\nimport {\n type NexusWidgetProgressEvent,\n NexusWidgetProgressScreen,\n} from \"./components/nexus-widget-progress-screen\";\nimport {\n getCachedReceiveTokenMatch,\n preloadReceiveTokens,\n ReceiveAssetSelector,\n} from \"./components/receive-asset-selector\";\nimport { RecipientInput } from \"./components/recipient-input\";\nimport { SendIdleForm } from \"./components/send-idle-form\";\nimport { StatusAlert } from \"./components/status-alerts\";\nimport {\n deriveTokenOptions,\n SwapAssetSelector,\n type SwapTokenOption,\n} from \"./components/swap-asset-selector\";\nimport { SwapIdleForm } from \"./components/swap-idle-form\";\nimport {\n type BridgeProvider,\n type SwapIntentData,\n type SwapIntentDestination,\n type SwapIntentSource,\n SwapIntentPreview,\n} from \"./components/swap-intent-preview\";\nimport {\n NEXUS_WIDGET_DEFAULT_PRIMARY_COLOR,\n NEXUS_WIDGET_FAST_SPINNER_STYLE,\n nexusWidgetTheme,\n} from \"./theme\";\nimport {\n type NexusWidgetAppearance,\n type NexusWidgetConfig,\n type NexusWidgetDepositConfig,\n type NexusWidgetDestination,\n type NexusWidgetDestinationToken,\n type NexusWidgetDepositOpportunityConfig,\n type NexusWidgetDepositOpportunityMetadata,\n type NexusWidgetMode,\n type NexusWidgetPrefillToken,\n type NexusWidgetProps,\n type NexusWidgetRuntimePrefill,\n type SwapType,\n} from \"./types\";\nimport { findCitreaReceiveToken } from \"./utils/citrea-tokens\";\nimport {\n type DepositSourceFilter,\n getDepositSourceId,\n resolveDepositSourceSelection,\n} from \"./utils/deposit-source-selection\";\n\n// ---------------------------------------------------------------------------\n// Types for swap step machine\n// ---------------------------------------------------------------------------\n\ntype SwapStep =\n | \"idle\" // main screen\n | \"choose-swap-asset\" // pick source token\n | \"choose-receive-asset\" // pick receive token\n | \"enter-recipient\" // pick recipient (send mode)\n | \"preview-intent\" // intent preview card\n | \"progress\" // transaction in flight\n | \"success\" // completed seamlessly\n | \"failed\" // failed swap receipt\n | \"history\"; // transaction history\n\ntype SourceFilterTab = \"all\" | \"native\" | \"stables\";\n\ntype SwapHistoryStatus =\n | \"pending\"\n | \"fulfilled\"\n | \"failed\"\n | \"timeout\"\n | \"refund-initiated\";\n\ninterface SwapHistoryEntry {\n autoRefundAvailable?: boolean;\n createdAt: number;\n durationSeconds?: number;\n endedAt?: number;\n error?: string;\n failedStepType?: string;\n failureDescription?: string;\n failureMessage?: string;\n feeUsd?: string;\n finalExplorerUrl?: string | null;\n fromTokens: SwapTokenOption[];\n id: string;\n intentData: SwapIntentData | null;\n intentExplorerUrl?: string | null;\n intentId?: number;\n mode: NexusWidgetMode;\n opportunity?: NexusWidgetDepositOpportunityMetadata;\n recipientAddress?: string;\n requestedToAmount?: string;\n requestedToValue?: string;\n sourceExplorerUrl?: string | null;\n startedAt: number;\n status: SwapHistoryStatus;\n toToken?: SwapTokenOption;\n}\n\ntype HistorySourceRow = {\n amount: string;\n chainId?: number;\n chainLogo?: string;\n chainName: string;\n contractAddress?: string;\n key: string;\n symbol: string;\n tokenLogo?: string;\n value?: unknown;\n};\n\ntype SwapQuoteIssue = {\n type: \"insufficientSources\";\n message: string;\n missingUsd?: string;\n};\n\ntype ReceiveAmountIssue = {\n ctaLabel: string;\n message: string;\n type: \"configuredAmountLimit\" | \"receiveLimitExceeded\" | \"unpricedReceiveToken\";\n};\n\ntype CachedIntentUsdRate = {\n amount: string;\n rate: string;\n updatedAt: number;\n value: string;\n};\n\ntype PredictiveQuote = {\n key: string;\n mode: \"exactIn\" | \"exactOut\";\n sources?: SwapTokenOption[];\n toAmount?: string;\n toUsd?: string;\n};\n\ntype IntentFetchTiming = {\n background: boolean;\n endTimeMs?: number;\n fetchDurationMs?: number;\n operation: string;\n quoteInputKey: string;\n runId: number;\n sourceCount?: number;\n startTimeMs: number;\n walletAddress?: string | null;\n};\n\ntype PredictiveQuoteBaseline = {\n destinationUsdRate: string;\n exactInDestinationAmountPerSourceUsd?: string;\n exactOutSourceUsdPerDestinationUsd?: string;\n updatedAt: number;\n};\n\nconst DESTINATION_RECEIVE_LIMIT_USD_BY_CHAIN_ID: Record = {\n [SUPPORTED_CHAINS.MEGAETH]: 10_000,\n [SUPPORTED_CHAINS.CITREA]: 2000,\n [SUPPORTED_CHAINS.SCROLL]: 500,\n};\n\nconst SCIENTIFIC_DECIMAL_REGEX = /^-?(?:\\d+\\.?\\d*|\\.\\d+)e[+-]?\\d+$/i;\n\nconst QUOTE_REFRESH_INTERVAL_MS = 30000;\nconst EXACT_OUT_INPUT_DEBOUNCE_MS = 1300;\nconst DRAWER_CLOSE_MS = 220;\nconst BALANCE_REFRESH_AFTER_TERMINAL_MS = 3000;\nconst MODAL_HEIGHT_TRANSITION_MS = 220;\nconst ROOT_HEIGHT_TRANSITION_MS = 140;\nconst ASSET_SELECTOR_DRAWER_HEIGHT = \"90%\";\nconst TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT = 620;\nconst CONFIGURED_RECEIVE_SELECTOR_BASE_HEIGHT = 170;\nconst CONFIGURED_RECEIVE_SELECTOR_ROW_HEIGHT = 62;\nconst BASIS_POINTS = 10000;\nconst PREDICTIVE_EXACT_IN_DISCOUNT_BPS = 50;\nconst PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_PCT = 0.1;\nconst PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_MAX_USD = 2;\nconst PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_PCT = 0.02;\nconst PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_MAX_USD = 1;\nconst PREDICTIVE_QUOTE_DISPLAY_DECIMALS = 8;\nconst DEPOSIT_TOKEN_DISPLAY_DECIMALS = 8;\nconst ETHEREUM_MAINNET_CHAIN_ID = 1;\nconst SDK_EXACT_OUT_STABLE_SYMBOLS = new Set([\n \"USDC\",\n \"USDT\",\n \"DAI\",\n \"BUSD\",\n \"TUSD\",\n \"FRAX\",\n \"LUSD\",\n \"USDD\",\n \"USDP\",\n \"GUSD\",\n]);\nconst SWAP_HISTORY_STORAGE_KEY_PREFIX = \"nexus-widget-transaction-history-v1\";\nconst TIMEOUT_LABEL = \"Timed Out\";\nconst PROGRESS_EVENT_NAMES = {\n BRIDGE_PLAN_LIST: \"bridge_plan_list\",\n BRIDGE_PLAN_PROGRESS: \"bridge_plan_progress\",\n SWAP_PLAN_LIST: \"swap_plan_list\",\n SWAP_PLAN_PROGRESS: \"swap_plan_progress\",\n} as const;\nconst PLAN_FINAL_STATES = new Set([\"completed\", \"confirmed\", \"submitted\"]);\nconst PLAN_STEP_FUNDS_MAY_HAVE_MOVED_STATES = new Set([\n \"completed\",\n \"confirmed\",\n \"submitted\",\n \"failed\",\n]);\nconst theme = nexusWidgetTheme;\nconst tooltipSurface = theme.colors.surface;\nconst tooltipText = theme.colors.textStrong;\nconst tooltipBorder = theme.colors.border;\nconst uiFont = theme.fonts.sans;\nconst modalHeightTransitionStyle = {\n interpolateSize: \"allow-keywords\",\n} as React.CSSProperties;\n\nconst getCappedTokenDisplayDecimals = (decimals?: number) => {\n const parsedDecimals = Number(decimals);\n if (!Number.isFinite(parsedDecimals) || parsedDecimals < 0) {\n return DEPOSIT_TOKEN_DISPLAY_DECIMALS;\n }\n return Math.min(Math.floor(parsedDecimals), DEPOSIT_TOKEN_DISPLAY_DECIMALS);\n};\nconst modalHeightTransition = `height ${MODAL_HEIGHT_TRANSITION_MS}ms ease, max-height ${MODAL_HEIGHT_TRANSITION_MS}ms ease`;\n\nconst normalizeSdkExactOutStableSymbol = (symbol?: string) =>\n (symbol ?? \"\")\n .trim()\n .toUpperCase()\n .replaceAll(\"\\u20ae\", \"T\")\n .replaceAll(/[^A-Z0-9]/g, \"\");\n\nconst isSdkExactOutStableSymbol = (symbol?: string) =>\n SDK_EXACT_OUT_STABLE_SYMBOLS.has(normalizeSdkExactOutStableSymbol(symbol));\n\nconst applyPredictiveExactOutBufferUsd = (\n amount: Decimal,\n pct: number,\n maxUsd: number\n) => Decimal.min(amount.mul(pct), new Decimal(maxUsd));\n\nconst getPredictiveExactOutSourceTargetUsd = (\n destinationUsdNeedingSources: Decimal,\n cachedSourceUsdRatio?: Decimal\n) => {\n if (destinationUsdNeedingSources.lte(0)) return new Decimal(0);\n if (cachedSourceUsdRatio?.gt(0)) {\n return destinationUsdNeedingSources.mul(cachedSourceUsdRatio);\n }\n\n const destinationBuffer = applyPredictiveExactOutBufferUsd(\n destinationUsdNeedingSources,\n PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_PCT,\n PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_MAX_USD\n );\n const destinationBuffered = destinationUsdNeedingSources.plus(\n destinationBuffer\n );\n const sourceBuffer = applyPredictiveExactOutBufferUsd(\n destinationBuffered,\n PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_PCT,\n PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_MAX_USD\n );\n\n return destinationBuffered.plus(sourceBuffer);\n};\n\nconst getSwapHistoryStorageKey = (ownerAddress?: string) =>\n `${SWAP_HISTORY_STORAGE_KEY_PREFIX}:${ownerAddress?.toLowerCase() || \"anonymous\"}`;\n\nconst getTokenSelectionKey = (token?: SwapTokenOption | null) => {\n if (!token) return \"\";\n if (token.isUnified) {\n return `unified:${token.unifiedSymbol ?? token.symbol}`;\n }\n return `${token.chainId ?? \"unknown\"}:${token.contractAddress.toLowerCase()}`;\n};\n\nconst getDisplaySourceTokenKey = (token?: SwapTokenOption | null) => {\n if (!token) return \"\";\n const chainId = token.chainId ?? \"unknown\";\n const address = token.contractAddress?.toLowerCase();\n return address\n ? `${chainId}:${address}`\n : `${chainId}:symbol:${token.symbol.toUpperCase()}`;\n};\n\nconst sumDecimalStrings = (left?: string, right?: string) => {\n const leftAmount = parseDecimalLoose(left) ?? new Decimal(0);\n const rightAmount = parseDecimalLoose(right) ?? new Decimal(0);\n const sum = leftAmount.plus(rightAmount);\n return sum.gt(0) ? sum.toFixed() : undefined;\n};\n\nconst mergeDisplaySourceTokens = (tokens: SwapTokenOption[]) => {\n const merged = new Map();\n\n for (const token of tokens) {\n const key = getDisplaySourceTokenKey(token);\n if (!key) continue;\n const existing = merged.get(key);\n if (!existing) {\n merged.set(key, token);\n continue;\n }\n\n const userAmount = sumDecimalStrings(\n existing.userAmount || existing.balance,\n token.userAmount || token.balance\n );\n const userAmountUsd = sumDecimalStrings(\n existing.userAmountUsd || existing.balanceInFiat,\n token.userAmountUsd || token.balanceInFiat\n );\n\n merged.set(key, {\n ...existing,\n balance: userAmount ?? existing.balance,\n balanceInFiat: userAmountUsd ?? existing.balanceInFiat,\n chainLogo: existing.chainLogo || token.chainLogo,\n logo: existing.logo || token.logo,\n userAmount: userAmount ?? existing.userAmount,\n userAmountUsd: userAmountUsd ?? existing.userAmountUsd,\n });\n }\n\n return Array.from(merged.values());\n};\n\nconst getTokenQuoteKey = (token?: SwapTokenOption | null) => {\n if (!token) return \"\";\n return [\n getTokenSelectionKey(token),\n token.symbol ?? \"\",\n token.decimals ?? \"\",\n ].join(\":\");\n};\n\nconst getSourceTokensQuoteKey = (tokens: SwapTokenOption[]) =>\n tokens\n .filter((token) => {\n const amt = token.userAmount ?? \"\";\n const cleaned = amt.replaceAll(/[^0-9.]/g, \"\");\n const num = Number.parseFloat(cleaned);\n return !Number.isNaN(num) && num > 0;\n })\n .map((token) =>\n [\n getTokenSelectionKey(token),\n token.symbol ?? \"\",\n token.decimals ?? \"\",\n token.userAmount ?? \"\",\n token.userAmountUsd ?? \"\",\n token.userAmountMode ?? \"\",\n ].join(\":\")\n )\n .join(\"|\");\n\nconst getSourceTokenSyncKey = (token: SwapTokenOption) =>\n [\n getTokenSelectionKey(token),\n token.userAmount ?? \"\",\n token.userAmountUsd ?? \"\",\n token.balance ?? \"\",\n token.balanceInFiat ?? \"\",\n ].join(\":\");\n\nconst getSourceTokenListSyncKey = (tokens: SwapTokenOption[]) =>\n tokens.map(getSourceTokenSyncKey).join(\"|\");\n\nconst isSameTokenSelection = (\n a?: SwapTokenOption | null,\n b?: SwapTokenOption | null\n) => Boolean(a && b && getTokenSelectionKey(a) === getTokenSelectionKey(b));\n\nconst getDepositConfigIdentity = (deposit?: NexusWidgetDepositOpportunityMetadata | null) => {\n if (!deposit) return \"\";\n return [\n deposit.chainId,\n deposit.tokenAddress.toLowerCase(),\n deposit.tokenSymbol,\n deposit.tokenDecimals,\n deposit.protocol ?? \"\",\n deposit.title ?? \"\",\n ].join(\":\");\n};\n\nconst isSameDepositConfig = (\n a?: NexusWidgetDepositOpportunityConfig | null,\n b?: NexusWidgetDepositOpportunityConfig | null\n) => {\n if (!a || !b) return false;\n return getDepositConfigIdentity(a) === getDepositConfigIdentity(b);\n};\n\nconst getConfiguredReceiveSelectorRootHeight = (tokenCount: number) => {\n if (tokenCount <= 0) return TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT;\n const visibleRows = Math.min(Math.max(tokenCount, 1), 6);\n return Math.min(\n TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT,\n Math.ceil(\n (CONFIGURED_RECEIVE_SELECTOR_BASE_HEIGHT +\n visibleRows * CONFIGURED_RECEIVE_SELECTOR_ROW_HEIGHT) /\n 0.9\n )\n );\n};\n\nconst getConfiguredDeposit = (\n config: RuntimeNexusWidgetConfig\n): NexusWidgetDepositOpportunityConfig | undefined => config.deposit ?? config.deposits?.[0];\n\ntype RuntimeDestinationPair = {\n chain: number;\n decimals?: number;\n logo?: string;\n symbol?: string;\n token: `0x${string}`;\n};\n\ntype RuntimeNexusWidgetAppearance = NexusWidgetAppearance & {\n logoUrl?: string;\n themeMode?: \"auto\" | \"light\" | \"dark\";\n widgetHeading?: string;\n};\n\ntype RuntimeNexusWidgetConfig = {\n allowedDestinationChains?: number[];\n allowedDestinationPairs?: RuntimeDestinationPair[];\n allowedSourcePairs?: RuntimeDestinationPair[];\n appearance?: RuntimeNexusWidgetAppearance;\n deposit?: NexusWidgetDepositOpportunityConfig;\n deposits?: NexusWidgetDepositOpportunityConfig[];\n mode: NexusWidgetMode;\n prefill?: NexusWidgetRuntimePrefill;\n};\n\ntype RuntimeNexusWidgetAmountInput = {\n max?: string;\n min?: string;\n mode: \"fixed\" | \"user\";\n value?: string;\n};\n\ntype NormalizedNexusWidgetConfig = {\n activeMode: NexusWidgetMode;\n amountInput?: RuntimeNexusWidgetAmountInput;\n appearance?: RuntimeNexusWidgetAppearance;\n config: RuntimeNexusWidgetConfig;\n depositOptions: NexusWidgetDepositOpportunityConfig[];\n isAmountFixed: boolean;\n isRecipientLocked: boolean;\n};\n\nconst normalizeConfiguredString = (value: unknown) => {\n if (value === null || value === undefined) return undefined;\n const text = String(value).trim();\n return text || undefined;\n};\n\nconst normalizeConfiguredAddress = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n return text && isAddress(text) ? text : undefined;\n};\n\nconst isPositiveGasLimit = (value: unknown): value is bigint =>\n typeof value === \"bigint\" && value > BigInt(0);\n\nconst normalizePositiveNumericString = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n if (!text) return undefined;\n try {\n const decimal = new Decimal(text);\n return decimal.isFinite() && decimal.gt(0) ? text : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst normalizeNonNegativeNumericString = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n if (!text) return undefined;\n try {\n const decimal = new Decimal(text);\n return decimal.isFinite() && decimal.gte(0) ? text : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst normalizeHttpUrlString = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n if (!text) return undefined;\n return /^https?:\\/\\//i.test(text) ? text : undefined;\n};\n\nconst normalizeNexusWidgetPrimaryColor = (value?: string) => {\n return normalizeConfiguredString(value);\n};\n\nconst expandHexColor = (value: string) => {\n const text = value.trim();\n if (/^#[0-9a-fA-F]{6}$/.test(text)) return text;\n if (/^#[0-9a-fA-F]{3}$/.test(text)) {\n const [r, g, b] = text.slice(1);\n return `#${r}${r}${g}${g}${b}${b}`;\n }\n return undefined;\n};\n\nconst parseNumberComponent = (value: string, max: number) => {\n const text = value.trim();\n if (!text) return undefined;\n if (text.endsWith(\"%\")) {\n const percent = Number.parseFloat(text.slice(0, -1));\n return Number.isFinite(percent)\n ? Math.max(0, Math.min(max, (percent / 100) * max))\n : undefined;\n }\n const parsed = Number.parseFloat(text);\n return Number.isFinite(parsed) ? Math.max(0, Math.min(max, parsed)) : undefined;\n};\n\nconst parseAngleComponent = (value: string) => {\n const parsed = Number.parseFloat(value.trim());\n if (!Number.isFinite(parsed)) return undefined;\n return ((parsed % 360) + 360) % 360;\n};\n\nconst hslToRgb = (h: number, s: number, l: number): [number, number, number] => {\n const saturation = Math.max(0, Math.min(100, s)) / 100;\n const lightness = Math.max(0, Math.min(100, l)) / 100;\n const c = (1 - Math.abs(2 * lightness - 1)) * saturation;\n const hp = h / 60;\n const x = c * (1 - Math.abs((hp % 2) - 1));\n let r1 = 0;\n let g1 = 0;\n let b1 = 0;\n if (hp >= 0 && hp < 1) {\n r1 = c;\n g1 = x;\n } else if (hp >= 1 && hp < 2) {\n r1 = x;\n g1 = c;\n } else if (hp >= 2 && hp < 3) {\n g1 = c;\n b1 = x;\n } else if (hp >= 3 && hp < 4) {\n g1 = x;\n b1 = c;\n } else if (hp >= 4 && hp < 5) {\n r1 = x;\n b1 = c;\n } else {\n r1 = c;\n b1 = x;\n }\n const m = lightness - c / 2;\n return [\n Math.round((r1 + m) * 255),\n Math.round((g1 + m) * 255),\n Math.round((b1 + m) * 255),\n ];\n};\n\nconst parseCssColorToRgb = (\n color: string,\n allowBrowserResolution = true\n): [number, number, number] | undefined => {\n const text = normalizeConfiguredString(color);\n if (!text) return undefined;\n\n const hex = expandHexColor(text);\n if (hex) {\n return [\n Number.parseInt(hex.slice(1, 3), 16),\n Number.parseInt(hex.slice(3, 5), 16),\n Number.parseInt(hex.slice(5, 7), 16),\n ];\n }\n\n const rgbMatch = text.match(/^rgba?\\((.*)\\)$/i);\n if (rgbMatch) {\n const parts = rgbMatch[1].trim().split(/[\\s,\\/]+/).filter(Boolean);\n if (parts.length >= 3) {\n const r = parseNumberComponent(parts[0], 255);\n const g = parseNumberComponent(parts[1], 255);\n const b = parseNumberComponent(parts[2], 255);\n if (r !== undefined && g !== undefined && b !== undefined) {\n return [Math.round(r), Math.round(g), Math.round(b)];\n }\n }\n }\n\n const hslMatch = text.match(/^hsla?\\((.*)\\)$/i);\n if (hslMatch) {\n const parts = hslMatch[1].trim().split(/[\\s,\\/]+/).filter(Boolean);\n if (parts.length >= 3) {\n const h = parseAngleComponent(parts[0]);\n const s = parseNumberComponent(parts[1], 100);\n const l = parseNumberComponent(parts[2], 100);\n if (h !== undefined && s !== undefined && l !== undefined) {\n return hslToRgb(h, s, l);\n }\n }\n }\n\n if (allowBrowserResolution && typeof document !== \"undefined\") {\n const canvas = document.createElement(\"canvas\");\n const context = canvas.getContext(\"2d\");\n if (context) {\n const baseline = \"#010203\";\n context.fillStyle = baseline;\n context.fillStyle = text;\n const resolved = String(context.fillStyle);\n if (resolved !== baseline || text.toLowerCase() === baseline) {\n return parseCssColorToRgb(resolved, false);\n }\n }\n }\n\n return undefined;\n};\n\nconst getReadableTextColor = (background: string) => {\n const rgb = parseCssColorToRgb(background);\n if (!rgb) return \"#FFFFFE\";\n const [rValue, gValue, bValue] = rgb;\n const r = rValue / 255;\n const g = gValue / 255;\n const b = bValue / 255;\n const linear = [r, g, b].map((channel) =>\n channel <= 0.03928\n ? channel / 12.92\n : Math.pow((channel + 0.055) / 1.055, 2.4)\n );\n const luminance = 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];\n return luminance > 0.54 ? \"#161615\" : \"#FFFFFE\";\n};\n\nconst normalizeNexusWidgetAmountInput = (\n config: NexusWidgetConfig\n): RuntimeNexusWidgetAmountInput | undefined => {\n if (config.mode === \"swap\") return undefined;\n const value = normalizePositiveNumericString(\n \"amount\" in (config.prefill ?? {}) ? config.prefill?.amount : undefined\n );\n const min = normalizeNonNegativeNumericString(config.validation?.minAmount);\n const max = normalizePositiveNumericString(config.validation?.maxAmount);\n return value || min || max ? { mode: \"user\", value, min, max } : undefined;\n};\n\nconst normalizeNexusWidgetAppearance = (\n appearance?: NexusWidgetAppearance\n): RuntimeNexusWidgetAppearance | undefined => {\n if (!appearance) return undefined;\n const appName = normalizeConfiguredString(appearance.appName);\n const appLogoURL = normalizeHttpUrlString(appearance.appLogoURL);\n const heading = normalizeConfiguredString(appearance.heading);\n const mode =\n appearance.mode === \"dark\" || appearance.mode === \"light\"\n ? appearance.mode\n : \"system\";\n const primaryColor = normalizeConfiguredString(appearance.primaryColor);\n return {\n ...appearance,\n appLogoURL,\n appName,\n heading,\n logoUrl: appLogoURL,\n mode,\n primaryColor,\n themeMode: mode === \"system\" ? \"auto\" : mode,\n widgetHeading: heading,\n };\n};\n\nconst normalizeConfiguredChainId = (value: unknown) => {\n const chainId = Number(value);\n return Number.isFinite(chainId) && chainId > 0 ? chainId : undefined;\n};\n\nconst getConfiguredDestinationChainId = (\n destination?: NexusWidgetDestination\n) => normalizeConfiguredChainId(destination?.chain);\n\nconst getConfiguredDestinationTokenSymbol = (\n token: NexusWidgetDestinationToken\n) => normalizeConfiguredString(token.symbol);\n\nconst getConfiguredDestinationTokenAddress = (\n token: NexusWidgetDestinationToken\n) => normalizeConfiguredAddress(token.address);\n\nconst toSwapTokenOptionFromConfiguredDestinationToken = (\n token: NexusWidgetDestinationToken,\n chainId: number\n): SwapTokenOption | undefined => {\n const contractAddress = getConfiguredDestinationTokenAddress(token);\n const symbol = getConfiguredDestinationTokenSymbol(token);\n if (!contractAddress || !symbol) return undefined;\n const chainMeta = CHAIN_METADATA[chainId];\n return {\n balance: \"0\",\n balanceInFiat: \"$0.00\",\n chainId,\n chainLogo: chainMeta?.logo,\n chainName: getShortChainName(chainId, chainMeta?.name ?? `Chain ${chainId}`),\n contractAddress,\n decimals: token.decimals ?? 18,\n logo: token.logo ?? \"\",\n name: symbol,\n symbol,\n };\n};\n\nconst toSwapTokenOptionFromDepositConfig = (\n deposit: NexusWidgetDepositOpportunityConfig\n): SwapTokenOption => {\n const chainMeta = CHAIN_METADATA[deposit.chainId];\n return {\n balance: \"0\",\n balanceInFiat: \"$0.00\",\n chainId: deposit.chainId,\n chainLogo: chainMeta?.logo,\n chainName: getShortChainName(\n deposit.chainId,\n chainMeta?.name ?? `Chain ${deposit.chainId}`\n ),\n contractAddress: deposit.tokenAddress,\n decimals: deposit.tokenDecimals,\n logo: deposit.tokenLogo || deposit.logo || \"\",\n name: deposit.tokenSymbol,\n symbol: deposit.tokenSymbol,\n };\n};\n\nconst applyNexusWidgetAppearanceToDeposit = (\n deposit: NexusWidgetDepositOpportunityConfig,\n appearance?: RuntimeNexusWidgetAppearance\n): NexusWidgetDepositOpportunityConfig => {\n const appName = normalizeConfiguredString(appearance?.appName);\n const logoUrl = normalizeConfiguredString(appearance?.logoUrl);\n if (!appName && !logoUrl) return deposit;\n return {\n ...deposit,\n depositTargetLogo: logoUrl ?? deposit.depositTargetLogo,\n logo: logoUrl ?? deposit.logo,\n protocol: appName ?? deposit.protocol,\n title: appName ?? deposit.title,\n };\n};\n\nconst getConfiguredDestinationPairs = (\n destination?: NexusWidgetDestination\n) => {\n const pairs: RuntimeNexusWidgetConfig[\"allowedDestinationPairs\"] = [];\n const chain = getConfiguredDestinationChainId(destination);\n if (!chain) return pairs;\n for (const token of destination?.tokens ?? []) {\n const address = getConfiguredDestinationTokenAddress(token);\n if (!address) continue;\n pairs.push({ chain, token: address });\n }\n return pairs;\n};\n\nconst hasConfiguredDestinationTokenList = (\n destination?: NexusWidgetDestination\n) => Boolean(destination?.tokens?.length);\n\nconst getConfiguredPrefillTokenPair = (\n prefill?: { token?: NexusWidgetPrefillToken }\n): RuntimeDestinationPair | undefined => {\n const token = prefill?.token;\n if (!token) return undefined;\n const chain = normalizeConfiguredChainId(token.chain);\n const address = getConfiguredDestinationTokenAddress(token);\n const symbol = getConfiguredDestinationTokenSymbol(token);\n if (!chain || !address || !symbol) return undefined;\n return {\n chain,\n decimals: token.decimals,\n logo: normalizeConfiguredString(token.logo),\n symbol,\n token: address,\n };\n};\n\nconst getConfiguredDestinationChainIds = (\n destination?: NexusWidgetDestination\n) => {\n const chain = getConfiguredDestinationChainId(destination);\n return chain ? [chain] : [];\n};\n\nconst toDepositConfigFromDestination = (\n config: NexusWidgetDepositConfig,\n token: NexusWidgetDestinationToken,\n appearance?: RuntimeNexusWidgetAppearance\n): NexusWidgetDepositOpportunityConfig | undefined => {\n const chainId = getConfiguredDestinationChainId(config.destination);\n const depositAddress = normalizeConfiguredAddress(config.depositAddress);\n const tokenAddress = getConfiguredDestinationTokenAddress(token);\n const tokenSymbol = getConfiguredDestinationTokenSymbol(token);\n if (\n !chainId ||\n !depositAddress ||\n !tokenAddress ||\n !tokenSymbol ||\n typeof config.executeDeposit !== \"function\"\n ) {\n return undefined;\n }\n\n const chainMeta = CHAIN_METADATA[chainId];\n return {\n chainId,\n depositTargetLogo: appearance?.logoUrl,\n executeDeposit: config.executeDeposit,\n logo: appearance?.logoUrl ?? token.logo ?? undefined,\n protocol:\n appearance?.appName ??\n appearance?.widgetHeading ??\n \"Deposit\",\n subtitle:\n chainMeta?.name\n ? `on ${getShortChainName(chainId, chainMeta.name)}`\n : undefined,\n title: appearance?.appName ?? appearance?.widgetHeading ?? \"Deposit\",\n tokenAddress,\n tokenDecimals: token.decimals ?? 18,\n tokenLogo: token.logo,\n tokenSymbol,\n };\n};\n\nconst getNexusWidgetDepositOptions = (\n config: NexusWidgetConfig,\n appearance?: RuntimeNexusWidgetAppearance\n) => {\n if (config.mode !== \"deposit\") return [];\n return (config.destination.tokens ?? [])\n .map((token) => toDepositConfigFromDestination(config, token, appearance))\n .filter((deposit): deposit is NexusWidgetDepositOpportunityConfig => Boolean(deposit));\n};\n\nconst normalizeNexusWidgetConfig = (\n rawConfig: NexusWidgetConfig\n): NormalizedNexusWidgetConfig => {\n const appearance = normalizeNexusWidgetAppearance(rawConfig.appearance);\n const amountInput = normalizeNexusWidgetAmountInput(rawConfig);\n const prefill: NexusWidgetRuntimePrefill = {};\n let activeMode: NexusWidgetMode =\n rawConfig.mode === \"deposit\" ||\n rawConfig.mode === \"send\" ||\n rawConfig.mode === \"swap\"\n ? rawConfig.mode\n : \"swap\";\n let isRecipientLocked = false;\n const depositOptions = getNexusWidgetDepositOptions(rawConfig, appearance).map(\n (deposit) => applyNexusWidgetAppearanceToDeposit(deposit, appearance)\n );\n let allowedDestinationPairs:\n | RuntimeNexusWidgetConfig[\"allowedDestinationPairs\"]\n | undefined;\n let allowedDestinationChains:\n | RuntimeNexusWidgetConfig[\"allowedDestinationChains\"]\n | undefined;\n\n if (amountInput?.value) {\n prefill.amount = amountInput.value;\n }\n\n if (rawConfig.mode === \"deposit\") {\n activeMode = \"deposit\";\n const pairs = getConfiguredDestinationPairs(rawConfig.destination);\n if (pairs.length > 0) {\n allowedDestinationPairs = pairs;\n prefill.destination = pairs[0];\n }\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (chainIds.length > 0) {\n allowedDestinationChains = chainIds;\n }\n } else if (rawConfig.mode === \"send\") {\n const recipient = normalizeConfiguredAddress(rawConfig.recipientAddress);\n if (recipient) {\n prefill.recipient = recipient;\n isRecipientLocked = true;\n }\n\n const hasDestinationTokens = hasConfiguredDestinationTokenList(\n rawConfig.destination\n );\n const pairs = getConfiguredDestinationPairs(rawConfig.destination);\n if (pairs.length > 0) {\n allowedDestinationPairs = pairs;\n prefill.destination ??= pairs[0];\n } else if (!hasDestinationTokens) {\n const prefillToken = getConfiguredPrefillTokenPair(rawConfig.prefill);\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (\n prefillToken &&\n (!chainIds.length || chainIds.includes(prefillToken.chain))\n ) {\n prefill.destination = prefillToken;\n }\n }\n\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (chainIds.length > 0) {\n allowedDestinationChains = chainIds;\n }\n } else if (rawConfig.mode === \"swap\") {\n const recipient = normalizeConfiguredAddress(rawConfig.recipientAddress);\n if (recipient) {\n prefill.recipient = recipient;\n }\n\n const hasDestinationTokens = hasConfiguredDestinationTokenList(\n rawConfig.destination\n );\n const pairs = getConfiguredDestinationPairs(rawConfig.destination);\n if (pairs.length > 0) {\n allowedDestinationPairs = pairs;\n prefill.destination ??= pairs[0];\n } else if (!hasDestinationTokens) {\n const prefillToken = getConfiguredPrefillTokenPair(rawConfig.prefill);\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (\n prefillToken &&\n (!chainIds.length || chainIds.includes(prefillToken.chain))\n ) {\n prefill.destination = prefillToken;\n }\n }\n\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (chainIds.length > 0) {\n allowedDestinationChains = chainIds;\n }\n }\n\n const runtimeConfig: RuntimeNexusWidgetConfig = {\n allowedDestinationChains,\n allowedDestinationPairs,\n deposit:\n activeMode === \"deposit\"\n ? depositOptions[0]\n : undefined,\n deposits: activeMode === \"deposit\" ? depositOptions : [],\n appearance,\n mode: activeMode,\n prefill,\n };\n\n return {\n activeMode,\n amountInput,\n appearance,\n config: runtimeConfig,\n depositOptions,\n isAmountFixed: false,\n isRecipientLocked,\n };\n};\n\nconst getDepositForTokenSelection = (\n deposits: NexusWidgetDepositOpportunityConfig[],\n token?: SwapTokenOption | null\n) => {\n if (!token) return undefined;\n return deposits.find(\n (deposit) =>\n deposit.chainId === token.chainId &&\n deposit.tokenAddress.toLowerCase() ===\n token.contractAddress.toLowerCase()\n );\n};\n\nconst sanitizeOpportunityForHistory = (\n opportunity?: NexusWidgetDepositOpportunityMetadata\n): NexusWidgetDepositOpportunityMetadata | undefined => {\n if (!opportunity) return undefined;\n return {\n label: opportunity.label,\n protocol: opportunity.protocol,\n logo: opportunity.logo,\n title: opportunity.title,\n subtitle: opportunity.subtitle,\n chainId: opportunity.chainId,\n tokenSymbol: opportunity.tokenSymbol,\n tokenDecimals: opportunity.tokenDecimals,\n tokenLogo: opportunity.tokenLogo,\n tokenAddress: opportunity.tokenAddress,\n apy: opportunity.apy,\n description: opportunity.description,\n };\n};\n\nconst sanitizeHistoryEntry = (entry: SwapHistoryEntry): SwapHistoryEntry => ({\n ...entry,\n createdAt: entry.createdAt ?? entry.startedAt ?? Date.now(),\n failureMessage:\n entry.status === \"timeout\" ? TIMEOUT_LABEL : entry.failureMessage,\n opportunity: sanitizeOpportunityForHistory(entry.opportunity),\n});\n\nconst sortSwapHistoryEntries = (entries: SwapHistoryEntry[]) =>\n [...entries].sort(\n (a, b) =>\n (b.createdAt ?? b.startedAt ?? 0) - (a.createdAt ?? a.startedAt ?? 0)\n );\n\nconst isStoredHistoryStatus = (value: unknown): value is SwapHistoryStatus =>\n value === \"pending\" ||\n value === \"fulfilled\" ||\n value === \"failed\" ||\n value === \"timeout\" ||\n value === \"refund-initiated\";\n\nconst isStoredMode = (value: unknown): value is NexusWidgetMode =>\n value === \"swap\" || value === \"deposit\" || value === \"send\";\n\nconst normalizeStoredHistoryEntry = (\n value: unknown\n): SwapHistoryEntry | null => {\n if (!value || typeof value !== \"object\") return null;\n const entry = value as Partial;\n const startedAt =\n typeof entry.startedAt === \"number\" && Number.isFinite(entry.startedAt)\n ? entry.startedAt\n : undefined;\n const createdAt =\n typeof entry.createdAt === \"number\" && Number.isFinite(entry.createdAt)\n ? entry.createdAt\n : startedAt;\n\n if (\n !entry.id ||\n typeof entry.id !== \"string\" ||\n !isStoredMode(entry.mode) ||\n !isStoredHistoryStatus(entry.status) ||\n !createdAt ||\n !startedAt\n ) {\n return null;\n }\n\n return {\n ...entry,\n id: entry.id,\n mode: entry.mode,\n status: entry.status,\n createdAt,\n startedAt,\n failureMessage:\n entry.status === \"timeout\" ? TIMEOUT_LABEL : entry.failureMessage,\n intentData: entry.intentData ?? null,\n fromTokens: Array.isArray(entry.fromTokens) ? entry.fromTokens : [],\n opportunity: sanitizeOpportunityForHistory(entry.opportunity),\n } as SwapHistoryEntry;\n};\n\nconst readSwapHistoryFromStorage = (storageKey: string): SwapHistoryEntry[] => {\n if (typeof window === \"undefined\") return [];\n\n try {\n const raw = window.localStorage.getItem(storageKey);\n if (!raw) return [];\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n return sortSwapHistoryEntries(\n parsed\n .map(normalizeStoredHistoryEntry)\n .filter((entry): entry is SwapHistoryEntry => Boolean(entry))\n );\n } catch {\n return [];\n }\n};\n\nconst writeSwapHistoryToStorage = (\n storageKey: string,\n entries: SwapHistoryEntry[]\n) => {\n if (typeof window === \"undefined\") return;\n\n try {\n const persistableEntries =\n sortSwapHistoryEntries(entries).map(sanitizeHistoryEntry);\n window.localStorage.setItem(\n storageKey,\n JSON.stringify(persistableEntries, (_key, value) =>\n typeof value === \"bigint\" ? value.toString() : value\n )\n );\n } catch {\n // localStorage can be unavailable or full; in-memory history still works.\n }\n};\n\nfunction QuoteRefreshCountdown({\n progress,\n isRefreshing,\n secondsRemaining,\n}: {\n progress: number;\n isRefreshing: boolean;\n secondsRemaining: number;\n}) {\n const [showTooltip, setShowTooltip] = useState(false);\n const radius = 7;\n const circumference = 2 * Math.PI * radius;\n const clampedProgress = Math.max(0, Math.min(1, progress));\n const tooltipLabel = isRefreshing\n ? \"Refreshing quotes...\"\n : `Refreshing quotes in ${Math.max(0, secondsRemaining)} second${\n secondsRemaining === 1 ? \"\" : \"s\"\n }`;\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n style={{\n alignItems: \"center\",\n backgroundColor: \"#FFFFFE\",\n borderRadius: \"999px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n height: \"22px\",\n justifyContent: \"center\",\n outline: \"1px solid #E8E8E7\",\n position: \"relative\",\n width: \"22px\",\n }}\n tabIndex={0}\n >\n {showTooltip && (\n \n {tooltipLabel}\n \n )}\n \n \n \n \n \n );\n}\n\nconst normalizeDecimalInputText = (value: unknown) => {\n const raw = String(value).trim();\n if (!raw) return \"\";\n if (SCIENTIFIC_DECIMAL_REGEX.test(raw)) return raw;\n return raw.replace(/[^0-9.-]/g, \"\");\n};\n\nconst parseDecimalLoose = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = normalizeDecimalInputText(value);\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst toViemDecimalString = (value: unknown, decimals: number) => {\n const parsed = parseDecimalLoose(value);\n if (!parsed || parsed.lte(0)) return \"0\";\n return parsed\n .toDecimalPlaces(Math.max(0, decimals), Decimal.ROUND_DOWN)\n .toFixed();\n};\n\nconst formatDecimalDisplay = (\n value: unknown,\n options: { min?: number; max?: number } = {}\n) => {\n const amount = parseDecimalLoose(value) ?? new Decimal(0);\n const max = options.max ?? 2;\n return amount.toDecimalPlaces(max).toFixed();\n};\n\nconst formatUsdDisplay = (value: unknown) => {\n const amount = parseDecimalLoose(value) ?? new Decimal(0);\n if (amount.gt(0) && amount.lt(0.01)) return \"<$0.01\";\n return `$${formatDecimalDisplay(amount, { min: 2, max: 2 })}`;\n};\n\nconst formatTokenDisplay = (value: unknown) => {\n const amount = parseDecimalLoose(value) ?? new Decimal(0);\n return formatDecimalDisplay(amount, { max: 8 });\n};\n\nconst getSwapTokenUsdValue = (token: SwapTokenOption) =>\n parseDecimalLoose(token.userAmountUsd) ??\n parseDecimalLoose(token.balanceInFiat) ??\n new Decimal(0);\n\nconst getSwapTokenBalanceUsdValue = (token: SwapTokenOption) =>\n parseDecimalLoose(token.balanceInFiat) ?? new Decimal(0);\n\nconst sortSwapTokensByUsdDesc = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const usdDelta = getSwapTokenUsdValue(b).cmp(getSwapTokenUsdValue(a));\n if (usdDelta !== 0) return usdDelta;\n return (a.symbol ?? \"\").localeCompare(b.symbol ?? \"\");\n });\n\nconst sortDisplaySourcesByBalanceUsdDesc = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const balanceUsdDelta = getSwapTokenBalanceUsdValue(b).cmp(\n getSwapTokenBalanceUsdValue(a)\n );\n if (balanceUsdDelta !== 0) return balanceUsdDelta;\n\n const spendUsdDelta = getSwapTokenUsdValue(b).cmp(getSwapTokenUsdValue(a));\n if (spendUsdDelta !== 0) return spendUsdDelta;\n\n return `${a.symbol ?? \"\"} ${a.chainName ?? \"\"}`.localeCompare(\n `${b.symbol ?? \"\"} ${b.chainName ?? \"\"}`\n );\n });\n\nconst getIntentSourceUsdValue = (source: SwapIntentData[\"sources\"][number]) =>\n parseDecimalLoose(source.value) ?? new Decimal(0);\n\nconst sortIntentSourcesByUsdDesc = (sources: SwapIntentData[\"sources\"]) =>\n [...sources].sort((a, b) => {\n const usdDelta = getIntentSourceUsdValue(b).cmp(getIntentSourceUsdValue(a));\n if (usdDelta !== 0) return usdDelta;\n return (a.token?.symbol ?? \"\").localeCompare(b.token?.symbol ?? \"\");\n });\n\nconst extractIntentIdFromUrl = (url?: string | null) => {\n if (!url) return undefined;\n const match = url.match(/(\\d+)(?:\\/)?$/);\n if (!match) return undefined;\n const parsed = Number(match[1]);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;\n};\n\nconst getNonEmptyString = (...values: unknown[]) => {\n for (const value of values) {\n if (typeof value !== \"string\") continue;\n const trimmed = value.trim();\n if (trimmed.length > 0) return trimmed;\n }\n return null;\n};\n\nconst isHttpUrl = (value?: string | null): value is string =>\n Boolean(value && /^https?:\\/\\//i.test(value));\n\nconst hasValidIntentExplorer = (\n entry: Pick\n) => isHttpUrl(entry.intentExplorerUrl);\n\nconst getHistoryExplorerUrl = (\n entry: Pick<\n SwapHistoryEntry,\n \"finalExplorerUrl\" | \"intentExplorerUrl\" | \"sourceExplorerUrl\"\n >\n) =>\n [\n entry.intentExplorerUrl,\n entry.finalExplorerUrl,\n entry.sourceExplorerUrl,\n ].find(isHttpUrl) ?? null;\n\nconst getFiniteNumber = (...values: unknown[]) => {\n for (const value of values) {\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value !== \"string\") continue;\n const parsed = Number(value);\n if (Number.isFinite(parsed)) return parsed;\n }\n return undefined;\n};\n\nconst getObjectChainId = (value: any) =>\n getFiniteNumber(\n value?.chainId,\n value?.chain?.id,\n value?.chain?.chainId,\n value?.toChainId,\n value?.destinationChainId,\n value?.data?.chainId,\n value?.data?.chain?.id,\n value?.data?.chain?.chainId,\n value?.data?.toChainId,\n value?.data?.destinationChainId,\n value?.result?.chainId,\n value?.result?.chain?.id\n );\n\nconst getExplorerBaseUrl = (chainId?: number, ...candidates: unknown[]) => {\n const directCandidates = candidates.flatMap((candidate: any) => [\n candidate?.blockExplorerUrl,\n candidate?.blockExplorerURL,\n candidate?.chainBlockExplorerUrl,\n candidate?.explorerBaseUrl,\n candidate?.explorerUrlBase,\n candidate?.blockExplorerUrls?.[0],\n candidate?.blockExplorers?.default?.url,\n candidate?.chain?.blockExplorerUrl,\n candidate?.chain?.blockExplorerURL,\n candidate?.chain?.chainBlockExplorerUrl,\n candidate?.chain?.explorerBaseUrl,\n candidate?.chain?.explorerUrlBase,\n candidate?.chain?.blockExplorerUrls?.[0],\n candidate?.chain?.blockExplorers?.default?.url,\n candidate?.data?.blockExplorerUrl,\n candidate?.data?.blockExplorerURL,\n candidate?.data?.chainBlockExplorerUrl,\n candidate?.data?.blockExplorerUrls?.[0],\n candidate?.data?.blockExplorers?.default?.url,\n candidate?.data?.chain?.blockExplorerUrl,\n candidate?.data?.chain?.blockExplorerURL,\n candidate?.data?.chain?.chainBlockExplorerUrl,\n candidate?.data?.chain?.blockExplorerUrls?.[0],\n candidate?.data?.chain?.blockExplorers?.default?.url,\n ]);\n return getNonEmptyString(\n ...directCandidates,\n chainId ? CHAIN_METADATA[chainId]?.blockExplorerUrls?.[0] : undefined,\n chainId\n ? (CHAIN_METADATA[chainId] as any)?.blockExplorers?.default?.url\n : undefined\n );\n};\n\nconst getTransactionHash = (...values: unknown[]) => {\n for (const value of values) {\n if (typeof value !== \"string\") continue;\n const trimmed = value.trim();\n if (/^0x[a-fA-F0-9]{64}$/.test(trimmed)) return trimmed;\n }\n return null;\n};\n\nconst getIntentHash = (...values: unknown[]) => getTransactionHash(...values);\n\nconst getObjectIntentHash = (value: any) =>\n getIntentHash(\n value?.intentHash,\n value?.intent_hash,\n value?.intent?.hash,\n value?.intent?.intentHash,\n value?.intent?.intent_hash,\n value?.requestHash,\n value?.request_hash,\n value?.request?.hash,\n value?.request?.requestHash,\n value?.rffHash,\n value?.rff_hash,\n value?.rff?.hash,\n value?.data?.intentHash,\n value?.data?.intent_hash,\n value?.data?.intent?.hash,\n value?.data?.intent?.intentHash,\n value?.data?.intent?.intent_hash,\n value?.data?.requestHash,\n value?.data?.request_hash,\n value?.data?.request?.hash,\n value?.data?.request?.requestHash,\n value?.data?.rffHash,\n value?.data?.rff_hash,\n value?.data?.rff?.hash,\n value?.result?.intentHash,\n value?.result?.intent_hash,\n value?.result?.intent?.hash,\n value?.result?.requestHash,\n value?.result?.request_hash,\n value?.result?.rffHash,\n value?.result?.rff_hash\n );\n\nconst getNexusExplorerNetwork = (network?: unknown) => {\n const normalized =\n typeof network === \"string\" ? network.trim().toLowerCase() : \"\";\n if (normalized === \"canary\" || normalized === \"testnet\") return normalized;\n return \"mainnet\";\n};\n\nconst getRffExplorerUrl = (network: unknown, intentHash?: string | null) =>\n intentHash\n ? `https://nexus-v2.${getNexusExplorerNetwork(network)}.avail.so/rff/${intentHash}`\n : null;\n\nconst getObjectTransactionHash = (value: any) =>\n getTransactionHash(\n value?.txHash,\n value?.transactionHash,\n value?.executeTxHash,\n value?.executeTransactionHash,\n value?.transferTransactionHash,\n value?.receipt?.transactionHash,\n value?.tx?.hash,\n value?.transaction?.hash,\n value?.data?.txHash,\n value?.data?.transactionHash,\n value?.data?.executeTxHash,\n value?.data?.executeTransactionHash,\n value?.data?.transferTransactionHash,\n value?.data?.receipt?.transactionHash,\n value?.data?.tx?.hash,\n value?.data?.transaction?.hash,\n value?.result?.txHash,\n value?.result?.transactionHash,\n value?.result?.receipt?.transactionHash\n );\n\nconst getExplorerTxUrl = (\n chainId?: number,\n txHash?: string | null,\n ...candidates: unknown[]\n) => {\n if (!chainId || !txHash) return null;\n const baseUrl = getExplorerBaseUrl(chainId, ...candidates);\n return baseUrl ? `${String(baseUrl).replace(/\\/$/, \"\")}/tx/${txHash}` : null;\n};\n\nconst getSdkSwapResult = (result: any) => {\n const candidate = result?.swapResult ?? result?.result;\n return candidate && typeof candidate === \"object\" ? candidate : null;\n};\n\nconst getSdkTransactionHash = (result: any) =>\n getObjectTransactionHash(result) ||\n getObjectTransactionHash(result?.executeResponse) ||\n getObjectTransactionHash(result?.execute) ||\n getObjectTransactionHash(result?.transfer) ||\n getObjectTransactionHash(result?.swapResult) ||\n getObjectTransactionHash(result?.result) ||\n null;\n\nconst getSdkExplorerUrl = (result: any) =>\n getNonEmptyString(\n result?.explorerUrl,\n result?.explorerURL,\n result?.txExplorerUrl,\n result?.transactionExplorerUrl,\n result?.execute?.explorerUrl,\n result?.execute?.explorerURL,\n result?.execute?.txExplorerUrl,\n result?.execute?.transactionExplorerUrl,\n result?.executeResponse?.explorerUrl,\n result?.executeResponse?.explorerURL,\n result?.executeResponse?.txExplorerUrl,\n result?.executeResponse?.transactionExplorerUrl,\n result?.executeExplorerUrl,\n result?.transferExplorerUrl,\n result?.swapResult?.explorerUrl,\n result?.swapResult?.explorerURL,\n result?.swapResult?.txExplorerUrl,\n result?.swapResult?.transactionExplorerUrl,\n result?.result?.explorerUrl,\n result?.result?.explorerURL,\n result?.result?.txExplorerUrl,\n result?.result?.transactionExplorerUrl\n );\n\nconst getSdkIntentExplorerUrl = (result: any, swapResult?: any) =>\n getNonEmptyString(\n swapResult?.intentExplorerUrl,\n swapResult?.intentExplorerURL,\n swapResult?.intentUrl,\n swapResult?.intentURL,\n swapResult?.rffUrl,\n swapResult?.rffURL,\n swapResult?.rffExplorerUrl,\n swapResult?.rffExplorerURL,\n swapResult?.explorerUrl,\n swapResult?.explorerURL,\n result?.intentExplorerUrl,\n result?.intentExplorerURL,\n result?.intentUrl,\n result?.intentURL,\n result?.rffUrl,\n result?.rffURL,\n result?.rffExplorerUrl,\n result?.rffExplorerURL,\n result?.swapResult?.intentExplorerUrl,\n result?.swapResult?.intentExplorerURL,\n result?.swapResult?.intentUrl,\n result?.swapResult?.intentURL,\n result?.swapResult?.rffUrl,\n result?.swapResult?.rffURL,\n result?.swapResult?.rffExplorerUrl,\n result?.swapResult?.rffExplorerURL,\n result?.swapResult?.explorerUrl,\n result?.swapResult?.explorerURL,\n result?.result?.intentExplorerUrl,\n result?.result?.intentExplorerURL,\n result?.result?.intentUrl,\n result?.result?.intentURL,\n result?.result?.rffUrl,\n result?.result?.rffURL,\n result?.result?.rffExplorerUrl,\n result?.result?.rffExplorerURL,\n result?.result?.explorerUrl,\n result?.result?.explorerURL\n );\n\nconst getSdkIntentExplorerUrlForNetwork = (\n network: unknown,\n result: any,\n swapResult?: any\n) =>\n getSdkIntentExplorerUrl(result, swapResult) ||\n getRffExplorerUrl(\n network,\n getObjectIntentHash(swapResult) || getObjectIntentHash(result)\n );\n\nfunction MiniLogo({\n src,\n label,\n size = 30,\n fontSize = 13,\n outline,\n style,\n}: {\n src?: string;\n label?: string;\n size?: number;\n fontSize?: number;\n outline?: string;\n style?: React.CSSProperties;\n}) {\n const [failed, setFailed] = useState(!src);\n\n useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n background: \"#FFFFFE\",\n borderRadius: \"999px\",\n height: size,\n objectFit: \"cover\",\n outline,\n width: size,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {(label || \"?\").trim().slice(0, 1).toUpperCase()}\n \n );\n}\n\nfunction TokenLogoPair({\n tokenLogo,\n chainLogo,\n tokenSymbol,\n chainName,\n tokenOutline,\n size = 34,\n}: {\n tokenLogo?: string;\n chainLogo?: string;\n tokenSymbol?: string;\n chainName?: string;\n tokenOutline?: string;\n size?: number;\n}) {\n return (\n \n \n {chainLogo && (\n \n )}\n \n );\n}\n\nfunction SourceLogoStack({\n sources,\n size = 24,\n maxVisible = 3,\n}: {\n sources: HistorySourceRow[];\n size?: number;\n maxVisible?: number;\n}) {\n const visibleSources = sources.slice(0, maxVisible);\n const hiddenCount = Math.max(0, sources.length - visibleSources.length);\n\n return (\n \n {visibleSources.map((source, index) => (\n \n \n \n ))}\n {hiddenCount > 0 && (\n \n +{hiddenCount}\n \n )}\n \n );\n}\n\nfunction TruncatedAddress({\n address,\n color = \"var(--foreground-brand)\",\n}: {\n address: string;\n color?: string;\n}) {\n const [showTooltip, setShowTooltip] = useState(false);\n const label =\n address.length > 12\n ? `${address.slice(0, 6)}...${address.slice(-4)}`\n : address;\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n style={{\n color,\n display: \"inline-flex\",\n fontFamily: uiFont,\n fontSize: \"15px\",\n fontWeight: 500,\n lineHeight: \"20px\",\n outline: \"none\",\n position: \"relative\",\n }}\n tabIndex={0}\n >\n {label}\n {showTooltip && (\n \n {address}\n \n )}\n \n );\n}\n\nconst getDisplayDestinationSourceRow = (\n entry: SwapHistoryEntry\n): HistorySourceRow | null => {\n if (entry.mode !== \"deposit\" && entry.mode !== \"send\") return null;\n if (!entry.toToken || !entry.requestedToAmount) return null;\n\n const requestedAmount = parseDecimalLoose(entry.requestedToAmount);\n const intentDestinationAmount = parseDecimalLoose(\n entry.intentData?.destination.amount\n );\n const destinationBalanceAmount = parseDecimalLoose(\n entry.toToken.balance?.replace(entry.toToken.symbol, \"\")\n );\n if (\n !requestedAmount ||\n !destinationBalanceAmount ||\n requestedAmount.lte(0) ||\n destinationBalanceAmount.lte(0)\n ) {\n return null;\n }\n\n const intentCoversAmount = intentDestinationAmount ?? new Decimal(0);\n const displayAmount = Decimal.min(\n destinationBalanceAmount,\n Decimal.max(0, requestedAmount.minus(intentCoversAmount))\n );\n if (displayAmount.lte(0)) return null;\n\n const requestedValue = parseDecimalLoose(entry.requestedToValue);\n const destinationValue = parseDecimalLoose(\n entry.intentData?.destination.value\n );\n const rate =\n requestedValue && requestedAmount.gt(0)\n ? requestedValue.div(requestedAmount)\n : destinationValue && intentCoversAmount.gt(0)\n ? destinationValue.div(intentCoversAmount)\n : undefined;\n\n return {\n key: `destination-balance-${entry.toToken.chainId}-${entry.toToken.contractAddress}`,\n chainId: entry.toToken.chainId,\n contractAddress: entry.toToken.contractAddress,\n tokenLogo: entry.intentData?.destination.token.logo || entry.toToken.logo,\n chainLogo: entry.toToken.chainLogo,\n symbol: entry.toToken.symbol,\n chainName: getShortChainName(\n entry.toToken.chainId,\n entry.toToken.chainName\n ),\n amount: displayAmount\n .toDecimalPlaces(\n Math.max(0, entry.toToken.decimals ?? 18),\n Decimal.ROUND_DOWN\n )\n .toFixed(),\n value: rate\n ? displayAmount.mul(rate).toFixed()\n : entry.toToken.balanceInFiat,\n };\n};\n\nconst getHistorySourceRowMergeKey = (row: HistorySourceRow) => {\n const chainKey = row.chainId ?? row.chainName;\n const address = row.contractAddress?.toLowerCase();\n return address\n ? `${chainKey}:${address}`\n : `${chainKey}:symbol:${row.symbol.toUpperCase()}`;\n};\n\nconst mergeHistorySourceRows = (rows: HistorySourceRow[]) => {\n const merged = new Map();\n\n for (const row of rows) {\n const key = getHistorySourceRowMergeKey(row);\n const existing = merged.get(key);\n if (!existing) {\n merged.set(key, row);\n continue;\n }\n\n merged.set(key, {\n ...existing,\n amount: sumDecimalStrings(existing.amount, row.amount) ?? existing.amount,\n chainLogo: existing.chainLogo || row.chainLogo,\n key: existing.key,\n tokenLogo: existing.tokenLogo || row.tokenLogo,\n value: sumDecimalStrings(\n String(existing.value ?? \"\"),\n String(row.value ?? \"\")\n ),\n });\n }\n\n return Array.from(merged.values());\n};\n\nconst getProgressStepType = (step?: SwapStepType | BridgeStepType | null) =>\n String((step as any)?.type ?? (step as any)?.typeID ?? \"\").toUpperCase();\n\nconst isSourceMovementProgressStep = (type: string, rawStepType: string) =>\n rawStepType === \"source_swap\" ||\n rawStepType === \"eoa_to_ephemeral_transfer\" ||\n rawStepType === \"bridge_deposit\" ||\n rawStepType === \"vault_deposit\" ||\n type.includes(\"SOURCE_SWAP\") ||\n type.includes(\"EOA_TO_EPHEMERAL_TRANSFER\") ||\n type.includes(\"BRIDGE_DEPOSIT\") ||\n type.includes(\"VAULT_DEPOSIT\");\n\nconst isBridgeRefundStepType = (type: string) =>\n type.includes(\"BRIDGE_INTENT_SUBMISSION\") || type.includes(\"BRIDGE_DEPOSIT\");\n\nconst isSwapSkippedStepType = (type: string) => type.includes(\"SWAP_SKIPPED\");\n\nconst isAutoRefundAvailableProgressEvent = (event?: NexusWidgetProgressEvent) =>\n event?.name === PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS &&\n isBridgeRefundStepType(getProgressStepType(event.step));\n\nconst normalizeBridgeProvider = (\n value: unknown\n): BridgeProvider | undefined => {\n if (value === \"nexus\" || value === \"mayan\" || value === null) {\n return value;\n }\n return undefined;\n};\n\nconst normalizePlanStepType = (stepType: unknown, state?: unknown) => {\n const normalized = String(stepType ?? \"\").toLowerCase();\n const normalizedState = String(state ?? \"\").toLowerCase();\n\n if (normalized === \"execute_transaction\") {\n return normalizedState === \"confirmed\" || normalizedState === \"completed\"\n ? \"TRANSACTION_CONFIRMED\"\n : \"TRANSACTION_SENT\";\n }\n\n const mapped: Record = {\n allowance_approval: \"APPROVAL\",\n bridge_deposit: \"BRIDGE_DEPOSIT\",\n bridge_fill: \"BRIDGE_FILL\",\n bridge_intent_submission: \"BRIDGE_INTENT_SUBMISSION\",\n destination_swap: \"DESTINATION_SWAP\",\n eoa_to_ephemeral_transfer: \"EOA_TO_EPHEMERAL_TRANSFER\",\n execute_approval: \"APPROVAL\",\n request_signing: \"REQUEST_SIGNING\",\n request_submission: \"REQUEST_SUBMISSION\",\n source_swap: \"SOURCE_SWAP\",\n vault_deposit: \"BRIDGE_DEPOSIT\",\n };\n\n return mapped[normalized] ?? normalized.toUpperCase();\n};\n\nconst normalizePlanStep = (\n stepLike: unknown,\n fallbackStepType?: unknown,\n state?: unknown,\n completed?: boolean\n): SwapStepType | BridgeStepType => {\n const source =\n stepLike && typeof stepLike === \"object\" ? (stepLike as any) : {};\n const rawStepType = fallbackStepType ?? source.stepType ?? source.type;\n const progressType = normalizePlanStepType(\n rawStepType ?? source.typeID,\n state\n );\n const progressKey =\n source.id ?? source.stepId ?? source.typeID ?? progressType;\n\n return {\n ...source,\n completed,\n rawType: rawStepType,\n type: progressType,\n typeID: String(progressKey),\n } as SwapStepType | BridgeStepType;\n};\n\nconst getPlanStepChainId = (event: any, step: any) =>\n getObjectChainId(event) ?? getObjectChainId(step);\n\nconst getPlanStepTransactionHash = (event: any, step: any) =>\n getObjectTransactionHash(event) ?? getObjectTransactionHash(step);\n\nconst getPlanStepExplorerUrl = (event: any, step: any) => {\n const directExplorerUrl = getNonEmptyString(\n event?.explorerUrl,\n event?.explorerURL,\n event?.txExplorerUrl,\n event?.transactionExplorerUrl,\n step?.explorerUrl,\n step?.explorerURL,\n step?.txExplorerUrl,\n step?.transactionExplorerUrl,\n step?.data?.explorerUrl,\n step?.data?.explorerURL,\n step?.data?.txExplorerUrl,\n step?.data?.transactionExplorerUrl\n );\n if (directExplorerUrl) return directExplorerUrl;\n\n return getExplorerTxUrl(\n getPlanStepChainId(event, step),\n getPlanStepTransactionHash(event, step),\n event,\n step\n );\n};\n\nconst getPlanStepIntentExplorerUrl = (event: any, step: any) =>\n getNonEmptyString(\n event?.intentExplorerUrl,\n event?.intentExplorerURL,\n event?.intentUrl,\n event?.intentURL,\n event?.rffUrl,\n event?.rffURL,\n event?.rffExplorerUrl,\n event?.rffExplorerURL,\n step?.intentExplorerUrl,\n step?.intentExplorerURL,\n step?.intentUrl,\n step?.intentURL,\n step?.rffUrl,\n step?.rffURL,\n step?.rffExplorerUrl,\n step?.rffExplorerURL,\n step?.data?.intentExplorerUrl,\n step?.data?.intentExplorerURL,\n step?.data?.intentUrl,\n step?.data?.intentURL,\n step?.data?.rffUrl,\n step?.data?.rffURL,\n step?.data?.rffExplorerUrl,\n step?.data?.rffExplorerURL\n );\n\nconst isIntentSubmissionLikeEvent = (event: any, step?: any) => {\n const text = [\n event?.type,\n event?.event,\n event?.name,\n event?.status,\n event?.stepType,\n step?.type,\n step?.typeID,\n step?.rawType,\n ]\n .filter((value) => value !== null && value !== undefined)\n .join(\" \")\n .toLowerCase();\n\n return (\n text.includes(\"intent\") ||\n text.includes(\"request_submission\") ||\n text.includes(\"rff\")\n );\n};\n\nconst getGenericEventHash = (event: any, step?: any) =>\n getTransactionHash(\n event?.hash,\n event?.data?.hash,\n event?.result?.hash,\n step?.hash,\n step?.data?.hash,\n step?.result?.hash\n );\n\nconst getEventIntentExplorerUrl = (\n network: unknown,\n event: any,\n step?: any\n) => {\n const directUrl = step\n ? getPlanStepIntentExplorerUrl(event, step)\n : getNonEmptyString(\n event?.intentExplorerUrl,\n event?.intentExplorerURL,\n event?.intentUrl,\n event?.intentURL,\n event?.rffUrl,\n event?.rffURL,\n event?.rffExplorerUrl,\n event?.rffExplorerURL,\n event?.data?.intentExplorerUrl,\n event?.data?.intentExplorerURL,\n event?.data?.intentUrl,\n event?.data?.intentURL,\n event?.data?.rffUrl,\n event?.data?.rffURL,\n event?.data?.rffExplorerUrl,\n event?.data?.rffExplorerURL\n );\n if (directUrl) return directUrl;\n\n return getRffExplorerUrl(\n network,\n getObjectIntentHash(event) ||\n getObjectIntentHash(step) ||\n (isIntentSubmissionLikeEvent(event, step)\n ? getGenericEventHash(event, step)\n : null)\n );\n};\n\nconst isTimeoutLikeError = (error: unknown) => {\n const err = error as {\n code?: unknown;\n name?: unknown;\n message?: unknown;\n shortMessage?: unknown;\n };\n const text = [\n err?.code,\n err?.name,\n err?.message,\n err?.shortMessage,\n typeof error === \"string\" ? error : undefined,\n ]\n .filter((value) => value !== null && value !== undefined)\n .join(\" \");\n\n return /timeout|timed out|time out|deadline exceeded|expired while waiting|wait.*expired|poll.*expired/i.test(\n text\n );\n};\n\nconst getSdkEventType = (event: any) =>\n event?.type ?? event?.name ?? event?.event ?? \"unknown\";\n\nconst summarizeSdkProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n index?: number\n) => ({\n completed: (step as any)?.completed,\n index,\n rawType: (step as any)?.rawType ?? (step as any)?.stepType,\n state: (step as any)?.state,\n type: getProgressStepType(step),\n});\n\nconst logSdkSwapEvent = (\n label: string,\n event: any,\n meta?: Record\n) => {\n console.log(`[NexusWidget SDK][swap] ${label}`, {\n event,\n eventType: getSdkEventType(event),\n ...meta,\n });\n};\n\nconst logSdkIntentEvent = (\n label: string,\n data: any,\n meta?: Record\n) => {\n console.log(`[NexusWidget SDK][intent] ${label}`, {\n hasAllow: typeof data?.allow === \"function\",\n hasDeny: typeof data?.deny === \"function\",\n hasRefresh: typeof data?.refresh === \"function\",\n intent: data?.intent,\n raw: data,\n ...meta,\n });\n};\n\nconst logSdkIntentInput = (\n operation: string,\n input: unknown,\n meta?: Record\n) => {\n console.log(`[NexusWidget SDK][intent input] ${operation}`, {\n input,\n ...meta,\n });\n};\n\nconst normalizeSdkIntentString = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n return String(value);\n};\n\nconst normalizeSdkIntentAmount = (\n value: unknown,\n fallback: string = \"0\"\n) => normalizeSdkIntentString(value) ?? fallback;\n\nconst getLogoFromMetadata = (metadata: any) =>\n metadata?.logo ??\n metadata?.logoURI ??\n metadata?.logoUri ??\n metadata?.logoUrl ??\n metadata?.icon ??\n \"\";\n\nconst normalizeSdkIntentChain = (chain: any) => {\n const id = Number(chain?.id ?? chain?.chainId);\n if (!Number.isFinite(id)) return undefined;\n const chainMeta = CHAIN_METADATA[id];\n return {\n id,\n logo: getLogoFromMetadata(chain) || chainMeta?.logo || \"\",\n name: chain?.name ?? chainMeta?.name ?? \"\",\n };\n};\n\nconst normalizeSdkIntentToken = (token: any, chainId?: number) => {\n const chainMeta = chainId ? CHAIN_METADATA[chainId] : undefined;\n const decimals = Number(token?.decimals ?? chainMeta?.nativeCurrency.decimals);\n return {\n contractAddress:\n token?.contractAddress ??\n token?.address ??\n token?.tokenAddress ??\n zeroAddress,\n decimals: Number.isFinite(decimals) ? decimals : 18,\n logo: getLogoFromMetadata(token) || undefined,\n symbol: token?.symbol ?? token?.tokenSymbol ?? \"\",\n };\n};\n\nconst normalizeSdkIntentGas = (\n gas: any,\n chainId?: number\n): SwapIntentDestination[\"gas\"] => ({\n amount: normalizeSdkIntentAmount(gas?.amount),\n value: normalizeSdkIntentString(gas?.value),\n token: normalizeSdkIntentToken(gas?.token ?? gas, chainId),\n});\n\nconst normalizeSdkIntentSource = (\n source: any\n): SwapIntentSource | undefined => {\n const chain = normalizeSdkIntentChain(source?.chain);\n if (!chain) return undefined;\n return {\n ...source,\n amount: normalizeSdkIntentAmount(source?.amount),\n chain,\n token: normalizeSdkIntentToken(source?.token, chain.id),\n value: normalizeSdkIntentString(source?.value),\n };\n};\n\nconst normalizeSdkIntentDestination = (\n destination: any\n): SwapIntentDestination | undefined => {\n const chain = normalizeSdkIntentChain(destination?.chain);\n if (!chain) return undefined;\n return {\n ...destination,\n amount: normalizeSdkIntentAmount(destination?.amount),\n chain,\n gas: normalizeSdkIntentGas(destination?.gas, chain.id),\n token: normalizeSdkIntentToken(destination?.token, chain.id),\n value: normalizeSdkIntentString(destination?.value),\n };\n};\n\nconst normalizeSwapIntentData = (intent: any): SwapIntentData | null => {\n const destination = normalizeSdkIntentDestination(intent?.destination);\n if (!destination) return null;\n\n return {\n ...intent,\n destination,\n sources: Array.isArray(intent?.sources)\n ? intent.sources\n .map(normalizeSdkIntentSource)\n .filter(\n (source: SwapIntentSource | undefined): source is SwapIntentSource =>\n Boolean(source)\n )\n : [],\n };\n};\n\nconst normalizeSwapAndExecuteRequirementIntent = (\n intent: any\n): SwapIntentData | null => {\n const requirement = intent?.executeRequirement ?? intent?.executionRequirement;\n if (!requirement) return null;\n const destination = normalizeSdkIntentDestination({\n amount: requirement?.token?.amount,\n chain: requirement?.chain,\n gas: requirement?.gas,\n token: requirement?.token,\n value: requirement?.token?.value,\n });\n if (!destination) return null;\n\n return {\n ...intent,\n bridgeProvider: normalizeBridgeProvider(intent?.bridgeProvider),\n destination,\n feesAndBuffer: intent?.feesAndBuffer,\n sources: [],\n };\n};\n\nconst normalizeRenderableSwapIntentData = (\n rawIntent: any,\n bridgeProvider?: BridgeProvider\n): SwapIntentData | null => {\n const direct = normalizeSwapIntentData(rawIntent);\n const normalizedIntent = direct\n ? null\n : normalizeSwapIntentData(rawIntent?.normalizedIntent);\n const nestedSwap =\n direct || normalizedIntent ? null : normalizeSwapIntentData(rawIntent?.swap);\n const requirement = direct || normalizedIntent || nestedSwap\n ? null\n : normalizeSwapAndExecuteRequirementIntent(rawIntent);\n const normalized = direct ?? normalizedIntent ?? nestedSwap ?? requirement;\n if (!normalized) return null;\n\n return bridgeProvider === undefined\n ? normalized\n : { ...normalized, bridgeProvider };\n};\n\nconst logSwapPlanSteps = (\n eventType: \"plan_preview\" | \"plan_confirmed\",\n stepList: Array,\n rawSteps: unknown\n) => {\n console.log(`[NexusWidget SDK][swap] ${eventType} step list`, {\n count: stepList.length,\n eventType,\n rawSteps,\n steps: stepList.map((step, index) => summarizeSdkProgressStep(step, index)),\n });\n};\n\nconst logSwapPlanProgress = (\n event: any,\n step: SwapStepType | BridgeStepType,\n eventName: string,\n completed: boolean\n) => {\n console.log(\"[NexusWidget SDK][swap] plan_progress\", {\n completed,\n eventName,\n eventType: getSdkEventType(event),\n normalizedStep: summarizeSdkProgressStep(step),\n rawEvent: event,\n rawStep: event?.step,\n state: event?.state,\n stepType: event?.stepType,\n });\n};\n\nconst getFailureMessageForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n mode: NexusWidgetMode,\n autoRefundAvailable = false\n) => {\n if (autoRefundAvailable) {\n return \"Swap Failed. Refund Initiated\";\n }\n\n const type = getProgressStepType(step);\n if (\n type.includes(\"CREATE_PERMIT_FOR_SOURCE_SWAP\") ||\n type.includes(\"CREATE_PERMIT_EOA_TO_EPHEMERAL\") ||\n type.includes(\"EOA_EXECUTE_CALL\") ||\n type.includes(\"SOURCE_SWAP\") ||\n type.includes(\"COLLECTION\")\n ) {\n return \"Collection Failed\";\n }\n if (type.includes(\"DESTINATION_SWAP\") || type.includes(\"FULFIL\")) {\n return \"Destination Swap Failed\";\n }\n if (\n type.includes(\"TRANSACTION\") ||\n type.includes(\"APPROVAL\") ||\n type.includes(\"DEPOSIT\")\n ) {\n return mode === \"send\"\n ? \"Send failed. Funds are in your wallet\"\n : mode === \"deposit\"\n ? \"Deposit failed. Funds are in your wallet\"\n : \"Swap Failed\";\n }\n if (\n type.includes(\"SWAP\") ||\n type.includes(\"BRIDGE\") ||\n type.includes(\"INTENT\") ||\n type.includes(\"DETERMINING\")\n ) {\n return \"Swap Failed\";\n }\n return mode === \"send\"\n ? \"Send failed. Funds are in your wallet\"\n : mode === \"deposit\"\n ? \"Deposit failed. Funds are in your wallet\"\n : \"Swap Failed\";\n};\n\nconst getBridgeTokenSymbolForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined\n) => {\n const rawStep = step as any;\n return (\n getNonEmptyString(\n rawStep?.bridgeToken?.symbol,\n rawStep?.data?.bridgeToken?.symbol,\n rawStep?.bridgeTokenSymbol,\n rawStep?.data?.bridgeTokenSymbol,\n rawStep?.swaps?.[0]?.input?.symbol,\n rawStep?.data?.swaps?.[0]?.input?.symbol,\n rawStep?.input?.symbol,\n rawStep?.data?.input?.symbol,\n rawStep?.asset?.symbol,\n rawStep?.data?.asset?.symbol\n ) ?? \"USDC\"\n );\n};\n\nconst getFailureDescriptionForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n autoRefundAvailable = false\n) => {\n if (autoRefundAvailable) return undefined;\n const type = getProgressStepType(step);\n if (!type.includes(\"DESTINATION_SWAP\")) return undefined;\n const bridgeTokenSymbol = getBridgeTokenSymbolForProgressStep(step);\n return `${bridgeTokenSymbol} has been bridged and you have those funds in your wallet.`;\n};\n\nconst getSourceRows = (entry: SwapHistoryEntry): HistorySourceRow[] => {\n const sources = entry.intentData?.sources ?? [];\n const displayDestinationSourceRow = getDisplayDestinationSourceRow(entry);\n if (sources.length > 0) {\n const sourceRows = sources.map((source, index) => {\n const fallback = entry.fromTokens.find(\n (token) =>\n token.chainId === source.chain.id &&\n (token.contractAddress?.toLowerCase() ===\n source.token.contractAddress?.toLowerCase() ||\n token.symbol === source.token.symbol)\n );\n\n return {\n key: `${source.chain.id}-${source.token.contractAddress}-${index}`,\n chainId: source.chain.id,\n contractAddress: source.token.contractAddress,\n tokenLogo: source.token.logo || fallback?.logo,\n chainLogo: source.chain.logo || fallback?.chainLogo,\n symbol: source.token.symbol,\n chainName: getShortChainName(source.chain.id, source.chain.name),\n amount: source.amount,\n value: source.value,\n };\n });\n\n return mergeHistorySourceRows(\n displayDestinationSourceRow\n ? [displayDestinationSourceRow, ...sourceRows]\n : sourceRows\n );\n }\n\n const fallbackRows = entry.fromTokens.map((token, index) => ({\n key: `${token.chainId}-${token.contractAddress}-${index}`,\n chainId: token.chainId,\n contractAddress: token.contractAddress,\n tokenLogo: token.logo,\n chainLogo: token.chainLogo,\n symbol: token.symbol,\n chainName: getShortChainName(token.chainId, token.chainName),\n amount: token.userAmount || \"0\",\n value: token.balanceInFiat,\n }));\n\n return mergeHistorySourceRows(\n displayDestinationSourceRow\n ? [displayDestinationSourceRow, ...fallbackRows]\n : fallbackRows\n );\n};\n\nfunction SourceRowsList({\n entry,\n maxHeight = 236,\n borderTopFirst = true,\n scrollAfterRows = 4,\n}: {\n entry: SwapHistoryEntry;\n maxHeight?: number;\n borderTopFirst?: boolean;\n scrollAfterRows?: number;\n}) {\n const rows = getSourceRows(entry);\n const shouldScroll = rows.length > scrollAfterRows;\n const scrollRef = useRef(null);\n\n return (\n
\n \n {rows.map((row, index) => (\n 0 ? \"1px solid #E8E8E7\" : \"none\",\n display: \"flex\",\n justifyContent: \"space-between\",\n minHeight: \"64px\",\n padding: \"10px 20px\",\n }}\n >\n \n \n
\n \n {row.symbol}\n \n \n on {row.chainName || \"Unknown chain\"}\n \n
\n
\n \n \n {formatTokenDisplay(row.amount)} {row.symbol}\n \n \n {formatUsdDisplay(row.value)}\n \n \n \n ))}\n \n {shouldScroll && (\n \n scrollRef.current?.scrollBy({ top: 72, behavior: \"smooth\" })\n }\n style={{\n alignItems: \"center\",\n background: \"#FFFFFE\",\n border: \"1px solid #E8E8E7\",\n borderRadius: \"999px\",\n bottom: \"6px\",\n boxShadow: \"0 2px 8px rgba(22,22,21,0.08)\",\n display: \"flex\",\n height: \"22px\",\n justifyContent: \"center\",\n left: \"50%\",\n padding: 0,\n position: \"absolute\",\n transform: \"translateX(-50%)\",\n width: \"22px\",\n }}\n type=\"button\"\n >\n \n \n )}\n \n );\n}\n\nfunction SwapReceiptPanel({\n entry,\n onDone,\n}: {\n entry: SwapHistoryEntry;\n onDone: () => void;\n}) {\n const [showSourceDetails, setShowSourceDetails] = useState(false);\n const destination = entry.intentData?.destination;\n const isFailed = entry.status === \"failed\";\n const isTimeout = entry.status === \"timeout\";\n const isDeposit = entry.mode === \"deposit\";\n const isSend = entry.mode === \"send\";\n const isRecipientTransfer = isSend || Boolean(entry.recipientAddress);\n const tokenSymbol = destination?.token.symbol || entry.toToken?.symbol || \"\";\n const chainName = getShortChainName(\n destination?.chain.id ?? entry.toToken?.chainId,\n destination?.chain.name || entry.toToken?.chainName || \"\"\n );\n const depositVenue =\n entry.opportunity?.title || entry.opportunity?.protocol || chainName;\n const amount = destination?.amount || \"\";\n const destinationLogo =\n destination?.token.logo ||\n entry.toToken?.logo ||\n entry.opportunity?.tokenLogo ||\n (isDeposit ? entry.opportunity?.logo : undefined);\n const requestedExactOutAmount =\n (isDeposit || isSend) && entry.requestedToAmount\n ? entry.requestedToAmount\n : undefined;\n const requestedExactOutValue =\n (isDeposit || isSend) && entry.requestedToValue\n ? entry.requestedToValue\n : undefined;\n const value = requestedExactOutValue || destination?.value;\n const displayAmount = requestedExactOutAmount || amount;\n const showIntentExplorer = hasValidIntentExplorer(entry);\n const intentLabel = entry.intentId\n ? `Intent #${entry.intentId}`\n : \"View Explorer\";\n const sourceRows = getSourceRows(entry);\n const sourceCount = sourceRows.length;\n const sourceTotalUsd = sourceRows.reduce(\n (sum, source) => sum.plus(parseDecimalLoose(source.value) ?? 0),\n new Decimal(0)\n );\n const defaultSwapFailureHeadline = entry.autoRefundAvailable\n ? \"Swap Failed. Refund Initiated\"\n : \"Swap Failed\";\n const entryFailureMessage =\n entry.status === \"timeout\" ? TIMEOUT_LABEL : entry.failureMessage;\n const storedFailureMessage =\n !entry.autoRefundAvailable && entryFailureMessage?.includes(\"Refund\")\n ? undefined\n : entryFailureMessage;\n const failureHeadline =\n storedFailureMessage ||\n (isDeposit\n ? \"Deposit failed. Funds are in your wallet\"\n : isRecipientTransfer\n ? \"Send failed. Funds are in your wallet\"\n : defaultSwapFailureHeadline);\n const failureDescription = isFailed ? entry.failureDescription : undefined;\n const timeoutHeadline = TIMEOUT_LABEL;\n const timeoutDescription = isTimeout\n ? entry.failureDescription ||\n \"This transaction is still pending. Check the intent explorer for the latest status.\"\n : undefined;\n const receiptLocation = isDeposit ? depositVenue : chainName;\n const receiptSummary = receiptLocation ? `on ${receiptLocation}` : \"\";\n\n return (\n \n \n \n \n \n {isFailed ? \"x\" : isTimeout ? \"!\" : \"✓\"}\n \n \n
\n {isTimeout\n ? timeoutHeadline\n : isFailed\n ? failureHeadline\n : isDeposit\n ? \"You deposited\"\n : isRecipientTransfer\n ? \"You sent\"\n : \"You received\"}\n
\n {(failureDescription || timeoutDescription) && (\n \n {failureDescription || timeoutDescription}\n \n )}\n \n {displayAmount ? formatTokenDisplay(displayAmount) : \"--\"}\n \n {tokenSymbol}\n \n \n
\n ≈ {formatUsdDisplay(value)}\n
\n {receiptSummary && (\n \n {receiptSummary}\n \n )}\n \n\n \n \n \n {isDeposit || isSend ? \"You Paid\" : \"You Swapped\"}\n \n \n \n {formatUsdDisplay(sourceTotalUsd)}\n \n setShowSourceDetails((current) => !current)}\n style={{\n alignItems: \"center\",\n background: \"transparent\",\n border: \"none\",\n color: \"var(--foreground-brand)\",\n cursor: \"pointer\",\n display: \"inline-flex\",\n fontFamily: uiFont,\n fontSize: \"12px\",\n gap: \"4px\",\n padding: 0,\n }}\n type=\"button\"\n >\n {showSourceDetails\n ? \"Hide Details\"\n : `${sourceCount} asset${sourceCount === 1 ? \"\" : \"s\"}`}\n \n \n \n \n \n
\n \n
\n \n {isRecipientTransfer && entry.recipientAddress && (\n \n \n Recipient\n \n \n \n )}\n {showIntentExplorer && (\n \n \n Intent Explorer\n \n \n {intentLabel} ↗\n \n \n )}\n {entry.finalExplorerUrl && (\n \n \n Final Transaction\n \n \n View Explorer ↗\n \n \n )}\n \n \n Total Fees\n \n \n {formatUsdDisplay(entry.feeUsd)}\n \n \n \n\n \n Done\n \n \n );\n}\n\nconst getRelativeTime = (time: number, now: number) => {\n const seconds = Math.max(1, Math.floor((now - time) / 1000));\n if (seconds < 60) return `${seconds} second${seconds === 1 ? \"\" : \"s\"} ago`;\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes} minute${minutes === 1 ? \"\" : \"s\"} ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours} hour${hours === 1 ? \"\" : \"s\"} ago`;\n const days = Math.floor(hours / 24);\n return `${days} day${days === 1 ? \"\" : \"s\"} ago`;\n};\n\nfunction HistoryStatusPill({ status }: { status: SwapHistoryStatus }) {\n const config =\n status === \"fulfilled\"\n ? { label: \"Fulfilled\", bg: \"#E8F6EF\", fg: \"#168A47\" }\n : status === \"pending\"\n ? { label: \"Pending\", bg: \"#FFF3DE\", fg: \"#B7791F\" }\n : status === \"timeout\"\n ? { label: TIMEOUT_LABEL, bg: \"#FFF3DE\", fg: \"#B7791F\" }\n : status === \"refund-initiated\"\n ? { label: \"Refund Initiated\", bg: \"#FFF3DE\", fg: \"#B7791F\" }\n : { label: \"Failed\", bg: \"#FFE6EA\", fg: \"#E92C2C\" };\n\n return (\n \n {config.label}\n \n );\n}\n\nfunction SwapHistoryPanel({\n entries,\n now,\n}: {\n entries: SwapHistoryEntry[];\n now: number;\n}) {\n if (entries.length === 0) {\n return (\n \n \n \n ↻\n \n \n \n No transactions yet\n \n \n Your transaction history will appear here once you make your first\n swap, deposit, or send.\n \n \n );\n }\n\n const sortedEntries = sortSwapHistoryEntries(entries);\n const shouldScroll = sortedEntries.length > 5;\n\n return (\n \n {sortedEntries.map((entry) => {\n const destination = entry.intentData?.destination;\n const destinationLogo =\n destination?.token.logo ||\n entry.toToken?.logo ||\n entry.opportunity?.tokenLogo ||\n \"\";\n const destinationChainLogo =\n destination?.chain.logo || entry.toToken?.chainLogo || \"\";\n const destinationChainName = getShortChainName(\n destination?.chain.id ?? entry.toToken?.chainId,\n destination?.chain.name || entry.toToken?.chainName || \"\"\n );\n const destinationSymbol =\n destination?.token.symbol || entry.toToken?.symbol || \"\";\n const destinationValue =\n (entry.mode === \"deposit\" || entry.mode === \"send\") &&\n entry.requestedToValue\n ? entry.requestedToValue\n : destination?.value;\n const destinationAmount =\n (entry.mode === \"deposit\" || entry.mode === \"send\") &&\n entry.requestedToAmount\n ? entry.requestedToAmount\n : destination?.amount || \"\";\n const canShowRefund =\n entry.status === \"failed\" && Boolean(entry.autoRefundAvailable);\n const status = canShowRefund ? \"refund-initiated\" : entry.status;\n const sourceRows = getSourceRows(entry);\n const historyExplorerUrl = getHistoryExplorerUrl(entry);\n\n return (\n \n \n \n \n
\n \n {destinationAmount\n ? formatTokenDisplay(destinationAmount)\n : \"--\"}\n \n {destinationSymbol}\n \n
\n \n ≈ {formatUsdDisplay(destinationValue)}\n \n \n \n \n \n \n {getRelativeTime(entry.createdAt ?? entry.startedAt, now)}\n \n \n \n\n {canShowRefund && (\n \n \n Refund Initiated\n \n \n )}\n\n \n \n {sourceRows.length > 0 && (\n \n )}\n \n →\n \n \n \n {historyExplorerUrl && (\n \n \n View\n \n \n \n \n \n \n )}\n \n \n );\n })}\n \n );\n}\n\n// ---------------------------------------------------------------------------\n// NexusWidget\n// ---------------------------------------------------------------------------\n\nexport function NexusWidget(props: NexusWidgetProps) {\n return (\n \n
\n Something went wrong\n
\n \n An unexpected error occurred. Please refresh the page or try\n resetting the widget.\n \n window.location.reload()}\n style={{\n backgroundColor: \"var(--foreground-brand)\",\n border: \"none\",\n borderRadius: \"8px\",\n color: \"#FFFFFE\",\n cursor: \"pointer\",\n fontSize: \"15px\",\n fontWeight: 500,\n padding: \"8px 16px\",\n transition: \"background-color 0.15s ease-out\",\n }}\n type=\"button\"\n >\n Reload Page\n \n \n }\n >\n \n \n );\n}\n\nfunction NexusWidgetInner({\n config: rawConfig,\n embed = true,\n className,\n connectedAddress,\n open: controlledOpen,\n onOpenChange,\n defaultOpen = false,\n onComplete,\n onConnectClick,\n onStart,\n onError,\n onReceiveAssetChange,\n onClose,\n onConnectWallet,\n}: NexusWidgetProps) {\n const {\n nexusSDK,\n bridgableBalance,\n swapBalance,\n swapBalanceLoading,\n getFiatValue,\n resolveTokenUsdRate,\n swapSupportedChainsAndTokens,\n supportedChainsAndTokens,\n fetchSwapBalance,\n handleInit,\n swapIntent: providerSwapIntent,\n network,\n loading: nexusLoading,\n } = useNexus();\n const appConfig = useMemo(\n () => ({\n nexusNetwork: network ?? \"mainnet\",\n }),\n [network]\n );\n const isSwapBalancePending =\n Boolean(nexusSDK) && (swapBalanceLoading || swapBalance === null);\n\n const normalizedWidgetConfig = useMemo(\n () => normalizeNexusWidgetConfig(rawConfig),\n [rawConfig]\n );\n const config = normalizedWidgetConfig.config;\n const activeMode = normalizedWidgetConfig.activeMode;\n const amountInputConfig = normalizedWidgetConfig.amountInput;\n const appearanceConfig = normalizedWidgetConfig.appearance;\n const configuredDepositOptions = normalizedWidgetConfig.depositOptions;\n const isConfiguredAmountFixed = normalizedWidgetConfig.isAmountFixed;\n const isConfiguredRecipientLocked =\n normalizedWidgetConfig.isRecipientLocked;\n const primaryColor = normalizeNexusWidgetPrimaryColor(\n appearanceConfig?.primaryColor\n );\n const primaryButtonBackground =\n primaryColor ?? NEXUS_WIDGET_DEFAULT_PRIMARY_COLOR;\n const primaryButtonForeground = getReadableTextColor(\n primaryButtonBackground\n );\n const theme = useMemo(\n () => ({\n ...nexusWidgetTheme,\n colors: {\n ...nexusWidgetTheme.colors,\n primary: \"var(--foreground-brand)\",\n primaryText: \"var(--foreground-brand)\",\n },\n }),\n [primaryColor]\n );\n const configuredDeposit = getConfiguredDeposit(config);\n const configuredDepositIdentity = getDepositConfigIdentity(configuredDeposit);\n const configuredDestinationTokenOptions = useMemo(() => {\n const destination = rawConfig.destination;\n const destinationChainId = getConfiguredDestinationChainId(destination);\n const destinationTokens =\n rawConfig.mode === \"deposit\"\n ? configuredDepositOptions.map(toSwapTokenOptionFromDepositConfig)\n : destination?.tokens && destinationChainId\n ? destination.tokens\n .map((token) =>\n toSwapTokenOptionFromConfiguredDestinationToken(\n token,\n destinationChainId\n )\n )\n .filter((token): token is SwapTokenOption => Boolean(token))\n : [];\n\n const tokensByKey = new Map();\n for (const token of destinationTokens) {\n tokensByKey.set(getTokenSelectionKey(token), token);\n }\n return Array.from(tokensByKey.values());\n }, [configuredDepositOptions, rawConfig.destination]);\n if (activeMode === \"deposit\" && !configuredDeposit) {\n throw new Error(\n \"NexusWidget deposit mode requires destination.chain, at least one destination token, depositAddress, and executeDeposit.\"\n );\n }\n const showCloseButton = !embed && Boolean(onClose);\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const isControlledOpen = controlledOpen !== undefined;\n const isModalOpen = isControlledOpen ? controlledOpen : internalOpen;\n\n // Preload receive tokens once SDK is available\n useEffect(() => {\n if (nexusSDK) {\n console.log(\n \"[preloadReceiveTokens] Calling preloadReceiveTokens from NexusWidget useEffect (nexusSDK available)\"\n );\n preloadReceiveTokens();\n }\n }, [nexusSDK]);\n\n const {\n address: accountAddress,\n connector,\n status: walletStatus,\n } = useAccount();\n const {\n connectors,\n connectAsync,\n isPending: isWalletConnectPending,\n } = useConnect();\n const { data: walletClient } = useWalletClient();\n const { data: connectorClient } = useConnectorClient();\n const publicClient = usePublicClient();\n const walletClientAddress = walletClient?.account?.address;\n const ownerAddress =\n connectedAddress &&\n isAddress(connectedAddress) &&\n connectedAddress.toLowerCase() !== zeroAddress\n ? connectedAddress\n : accountAddress &&\n isAddress(accountAddress) &&\n accountAddress.toLowerCase() !== zeroAddress\n ? accountAddress\n : walletClientAddress &&\n isAddress(walletClientAddress) &&\n walletClientAddress.toLowerCase() !== zeroAddress\n ? walletClientAddress\n : undefined;\n const getEffectiveWalletProvider = useCallback(\n async (activeConnector = connector) => {\n let connectorProvider: unknown;\n try {\n connectorProvider = await activeConnector?.getProvider();\n } catch {\n connectorProvider = undefined;\n }\n\n const connectorClientProvider = connectorClient\n ? {\n request: (args: unknown) => connectorClient.request(args as any),\n }\n : undefined;\n const walletClientProvider = walletClient\n ? {\n request: (args: unknown) => walletClient.request(args as any),\n }\n : undefined;\n const windowProvider =\n typeof window !== \"undefined\"\n ? (window as Window & { ethereum?: EthereumProvider }).ethereum\n : undefined;\n const effectiveProvider =\n connectorProvider &&\n typeof (connectorProvider as EthereumProvider).request === \"function\"\n ? (connectorProvider as EthereumProvider)\n : (connectorClientProvider ?? walletClientProvider ?? windowProvider);\n\n return effectiveProvider &&\n typeof effectiveProvider.request === \"function\"\n ? (effectiveProvider as EthereumProvider)\n : undefined;\n },\n [connector, connectorClient, walletClient]\n );\n const historyStorageKey = getSwapHistoryStorageKey(ownerAddress);\n\n useEffect(() => {\n if (walletStatus !== \"connected\" || !ownerAddress) {\n return;\n }\n\n let cancelled = false;\n void (async () => {\n const effectiveProvider = await getEffectiveWalletProvider();\n if (cancelled || !effectiveProvider) {\n return;\n }\n await handleInit(effectiveProvider, ownerAddress);\n })().catch((error) => {\n if (!cancelled) {\n console.error(\"Unable to initialize Nexus for wallet address:\", error);\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [getEffectiveWalletProvider, handleInit, ownerAddress, walletStatus]);\n\n // Global form state\n const [amount, setAmount] = useState(\"\");\n const [recipientAddress, setRecipientAddress] = useState(\"\");\n const [editingAssetIndex, setEditingAssetIndex] = useState(\n null\n );\n const [txError, setTxError] = useState(null);\n const [walletActionPending, setWalletActionPending] = useState(false);\n const defaultRecipientAddress = ownerAddress ?? \"\";\n const effectiveRecipientAddress =\n activeMode === \"swap\"\n ? recipientAddress || defaultRecipientAddress\n : recipientAddress;\n const hasSameOwnerSendRecipient =\n activeMode === \"send\" &&\n Boolean(\n ownerAddress &&\n recipientAddress &&\n isAddress(recipientAddress) &&\n recipientAddress.toLowerCase() === ownerAddress.toLowerCase()\n );\n const hasCustomSwapRecipient =\n activeMode === \"swap\" &&\n Boolean(\n recipientAddress &&\n (!defaultRecipientAddress ||\n recipientAddress.toLowerCase() !==\n defaultRecipientAddress.toLowerCase())\n );\n const transferRecipientAddress =\n activeMode === \"send\"\n ? recipientAddress\n : hasCustomSwapRecipient\n ? recipientAddress\n : undefined;\n\n // Swap-specific\n const [swapType, setSwapType] = useState(\"exactIn\");\n const [swapStep, setSwapStep] = useState(\"idle\");\n const drawerCloseTimerRef = useRef | null>(\n null\n );\n const terminalBalanceRefreshTimerRef = useRef | null>(null);\n const [closingDrawerStep, setClosingDrawerStep] = useState(\n null\n );\n const rootContentRef = useRef(null);\n const [rootContentHeight, setRootContentHeight] = useState(\n null\n );\n const rootContentHeightRef = useRef(null);\n const [hasMeasuredRootContent, setHasMeasuredRootContent] = useState(false);\n const [shouldAnimateRootHeight, setShouldAnimateRootHeight] = useState(false);\n const [isPreviewTransitioning, setIsPreviewTransitioning] = useState(false);\n const rootHeightTransitionTimerRef = useRef | null>(null);\n const [fromTokens, setFromTokens] = useState([]);\n const [sourceSelectionTouched, setSourceSelectionTouched] = useState(false);\n const [sourceSelectionRevision, setSourceSelectionRevision] = useState(0);\n const [\n sourceSelectionRequiredUsdDisplay,\n setSourceSelectionRequiredUsdDisplay,\n ] = useState(undefined);\n const [sourcePickerDraftTokens, setSourcePickerDraftTokens] = useState<\n SwapTokenOption[] | null\n >(null);\n const sourcePickerDraftTokensRef = useRef(null);\n const sourcePickerDraftDepositFilterRef = useRef(\"all\");\n const sourcePickerDraftTouchedRef = useRef(false);\n const sourcePickerDraftModeRef = useRef<\"all\" | \"selected\">(\"all\");\n const [exactOutQuoteSourceMode, setExactOutQuoteSourceMode] = useState<\n \"all\" | \"selected\"\n >(\"all\");\n const exactOutQuoteSourceModeRef = useRef<\"all\" | \"selected\">(\"all\");\n const [toToken, setToToken] = useState(\n undefined\n );\n const [fromTokensQuoteKey, setFromTokensQuoteKey] = useState(\"\");\n\n useEffect(() => {\n const key = getSourceTokensQuoteKey(\n activeMode === \"swap\" && swapType === \"exactIn\"\n ? getReadyExactInSourceTokens(fromTokens)\n : fromTokens\n );\n setFromTokensQuoteKey(key);\n }, [activeMode, swapType, fromTokens]);\n const toTokenQuoteKey = getTokenQuoteKey(toToken);\n const appliedTokenPrefillRef = useRef(null);\n\n useEffect(() => {\n if (!toToken?.chainId || !toToken.contractAddress) return;\n\n let active = true;\n const selectedTokenKey = getTokenSelectionKey(toToken);\n const applyLoadedReceiveToken = () => {\n if (!active) return;\n const loadedToken = getCachedReceiveTokenMatch(toToken);\n if (!loadedToken) return;\n\n setToToken((current) => {\n if (!current || getTokenSelectionKey(current) !== selectedTokenKey) {\n return current;\n }\n\n const chainMeta = current.chainId\n ? CHAIN_METADATA[current.chainId]\n : undefined;\n const next = {\n ...current,\n chainLogo:\n loadedToken.chainLogo || current.chainLogo || chainMeta?.logo,\n chainName: getShortChainName(\n current.chainId,\n loadedToken.chainName || current.chainName || chainMeta?.name\n ),\n decimals: loadedToken.decimals ?? current.decimals,\n logo: loadedToken.logo || current.logo,\n name: loadedToken.name || current.name,\n priceUSD: loadedToken.priceUSD ?? current.priceUSD,\n symbol: loadedToken.symbol || current.symbol,\n };\n\n if (\n current.decimals === next.decimals &&\n current.chainLogo === next.chainLogo &&\n current.chainName === next.chainName &&\n current.logo === next.logo &&\n current.name === next.name &&\n current.priceUSD === next.priceUSD &&\n current.symbol === next.symbol\n ) {\n return current;\n }\n\n return next;\n });\n };\n\n applyLoadedReceiveToken();\n const receiveTokensPromise = preloadReceiveTokens();\n receiveTokensPromise?.then(applyLoadedReceiveToken).catch((error) => {\n if (active) {\n console.warn(\"Unable to refresh receive token metadata\", error);\n }\n });\n\n return () => {\n active = false;\n };\n }, [\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n const setExactOutQuoteSourceModeValue = useCallback(\n (mode: \"all\" | \"selected\") => {\n exactOutQuoteSourceModeRef.current = mode;\n setExactOutQuoteSourceMode(mode);\n },\n []\n );\n\n useEffect(() => {\n if (!nexusSDK) return;\n void fetchSwapBalance();\n }, [fetchSwapBalance, nexusSDK]);\n\n useEffect(() => {\n setSourceSelectionTouched(false);\n setExactOutQuoteSourceModeValue(\"all\");\n }, [activeMode, setExactOutQuoteSourceModeValue]);\n\n useEffect(() => {\n if (activeMode !== \"swap\") return;\n\n setRecipientAddress((current) => {\n if (!defaultRecipientAddress) return current ? \"\" : current;\n return current.toLowerCase() === defaultRecipientAddress.toLowerCase()\n ? current\n : defaultRecipientAddress;\n });\n }, [activeMode, defaultRecipientAddress]);\n\n const {\n steps,\n seed,\n onStepsList,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n const [progressEvents, setProgressEvents] = useState(\n []\n );\n const progressEventsRef = useRef([]);\n const swapStepsListRef = useRef([]);\n const [failedProgressStep, setFailedProgressStep] = useState<\n SwapStepType | BridgeStepType | null\n >(null);\n const [explorerUrls, setExplorerUrls] = useState<{\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n }>({ sourceExplorerUrl: null, destinationExplorerUrl: null });\n const swapRunIdRef = useRef(0);\n\n const widgetSessionIdRef = useRef(\n typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`\n );\n const widgetAttemptIdRef = useRef(null);\n const widgetOpenedTsRef = useRef(Date.now());\n const previewViewedTsRef = useRef(null);\n const previewConfirmedTsRef = useRef(null);\n const attemptCountRef = useRef(0);\n const fundsMovedRef = useRef(false);\n const intentUrlRef = useRef(null);\n const hadSimulationSuccessRef = useRef(false);\n const hadPreviewViewedRef = useRef(false);\n const widgetOpenedFiredRef = useRef(false);\n const reachedTerminalRef = useRef(false);\n const lastIntentSourceTokensRef = useRef([]);\n const lastAutoIntentSourceTokensRef = useRef([]);\n const immediateQuoteAfterSourceEditRef = useRef(false);\n const amountEnteredLastValueRef = useRef(\"\");\n const lastInputMethodRef = useRef<\n | \"typed\"\n | \"percent_20\"\n | \"percent_25\"\n | \"percent_50\"\n | \"percent_75\"\n | \"percent_max\"\n >(\"typed\");\n const prevSourceTouchedRef = useRef(false);\n const previousAutoSourceCountRef = useRef(0);\n const analyticsRef = useRef<{\n track: (event: string, properties?: Record) => void;\n } | null>(null);\n const selectedOpportunityRef = useRef(\n undefined\n );\n\n const newAttemptId = useCallback(() => {\n return typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`;\n }, []);\n\n const rotateAttempt = useCallback(() => {\n widgetAttemptIdRef.current = newAttemptId();\n previewViewedTsRef.current = null;\n previewConfirmedTsRef.current = null;\n fundsMovedRef.current = false;\n intentUrlRef.current = null;\n hadSimulationSuccessRef.current = false;\n hadPreviewViewedRef.current = false;\n reachedTerminalRef.current = false;\n }, [newAttemptId]);\n const [intentToAmount, setIntentToAmount] = useState(\n undefined\n );\n const [intentFeeUsd, setIntentFeeUsd] = useState(\n undefined\n );\n const [intentLoading, setIntentLoading] = useState(false);\n const [quoteRefreshing, setQuoteRefreshing] = useState(false);\n const [receiveMaxCalculating, setReceiveMaxCalculating] = useState(false);\n const [maxCalculationPercent, setMaxCalculationPercent] = useState<\n number | null\n >(null);\n const intentDestinationUsdRateCacheRef = useRef<\n Record\n >({});\n const intentSymbolUsdRateCacheRef = useRef<\n Record\n >({});\n const predictiveQuoteCacheRef = useRef<\n Record\n >({});\n const predictiveQuoteRunRef = useRef(0);\n const latestIntentFetchRunIdRef = useRef(0);\n const intentFetchTimingRef = useRef(null);\n const intentUiRenderLoggedRunIdRef = useRef(null);\n const [intentFetchTiming, setIntentFetchTiming] =\n useState(null);\n const [predictiveQuote, setPredictiveQuote] =\n useState(null);\n const maxPercentRunRef = useRef(0);\n const [previewQuoteRefreshing, setPreviewQuoteRefreshing] = useState(false);\n const [quoteRefreshProgress, setQuoteRefreshProgress] = useState(0);\n const [quoteRefreshSecondsRemaining, setQuoteRefreshSecondsRemaining] =\n useState(0);\n const [intentData, setIntentData] = useState(null);\n const [swapQuoteIssue, setSwapQuoteIssue] = useState(\n null\n );\n const [receiveAmountIssue, setReceiveAmountIssue] =\n useState(null);\n const receiveAmountIssueRef = useRef(null);\n const receiveAmountIssueKeyRef = useRef(\"\");\n const [transferExplorerUrl, setTransferExplorerUrl] = useState(\n null\n );\n const swapStepRef = useRef(swapStep);\n const syncingIntentSourcesRef = useRef(false);\n const lastSwapIntentRefreshAtRef = useRef(0);\n const [destinationBalance, setDestinationBalance] = useState(\n null\n );\n const [swapHistory, setSwapHistory] = useState(() =>\n readSwapHistoryFromStorage(historyStorageKey)\n );\n const [currentSwapId, setCurrentSwapId] = useState(null);\n const [historyNow, setHistoryNow] = useState(() => Date.now());\n const currentSwapIdRef = useRef(null);\n const currentSwapStartedAtRef = useRef(0);\n const historyStorageKeyRef = useRef(historyStorageKey);\n const skipNextHistoryPersistRef = useRef(false);\n const explorerUrlsRef = useRef<{\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n }>({ sourceExplorerUrl: null, destinationExplorerUrl: null });\n const activeQuoteInputKeyRef = useRef(\"\");\n\n // Ref to store swap intent hook allow/deny callbacks\n const swapIntentRef = useRef<{\n intent?: SwapIntentData;\n allow: () => void;\n deny: () => void;\n refresh: () => Promise;\n runId?: number;\n quoteInputKey?: string;\n } | null>(null);\n\n const startIntentFetchTiming = useCallback(\n ({\n background,\n operation,\n quoteInputKey,\n runId,\n }: {\n background: boolean;\n operation: string;\n quoteInputKey: string;\n runId: number;\n }) => {\n const startTimeMs = Date.now();\n const timing: IntentFetchTiming = {\n background,\n operation,\n quoteInputKey,\n runId,\n startTimeMs,\n walletAddress: ownerAddress ?? null,\n };\n\n latestIntentFetchRunIdRef.current = runId;\n intentFetchTimingRef.current = timing;\n intentUiRenderLoggedRunIdRef.current = null;\n setIntentFetchTiming(timing);\n console.log(\"[NexusWidget] intent fetch start\", {\n runId,\n walletAddress: timing.walletAddress,\n operation,\n quoteInputKey,\n background,\n dateNow: startTimeMs,\n date: new Date(startTimeMs),\n });\n return timing;\n },\n [ownerAddress]\n );\n\n const finishIntentFetchTiming = useCallback(\n (runId: number, status: \"resolved\" | \"failed\" = \"resolved\") => {\n const timing = intentFetchTimingRef.current;\n if (!timing || timing.runId !== runId) return;\n if (timing.endTimeMs !== undefined) return;\n\n const endTimeMs = Date.now();\n if (latestIntentFetchRunIdRef.current !== runId) {\n return;\n }\n\n const fetchDurationMs = endTimeMs - timing.startTimeMs;\n const completedTiming: IntentFetchTiming = {\n ...timing,\n endTimeMs,\n fetchDurationMs,\n };\n intentFetchTimingRef.current = completedTiming;\n setIntentFetchTiming(completedTiming);\n console.log(\"[NexusWidget] intent fetch end\", {\n runId,\n walletAddress: timing.walletAddress,\n operation: timing.operation,\n quoteInputKey: timing.quoteInputKey,\n status,\n dateNow: endTimeMs,\n date: new Date(endTimeMs),\n });\n console.log(\"[NexusWidget] intent fetch total\", {\n runId,\n walletAddress: timing.walletAddress,\n operation: timing.operation,\n quoteInputKey: timing.quoteInputKey,\n totalTimeSeconds: fetchDurationMs / 1000,\n elapsedMs: fetchDurationMs,\n dateNow: endTimeMs,\n });\n },\n []\n );\n\n useEffect(() => {\n swapStepRef.current = swapStep;\n }, [swapStep]);\n\n useEffect(() => {\n return () => {\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n }\n if (terminalBalanceRefreshTimerRef.current) {\n clearTimeout(terminalBalanceRefreshTimerRef.current);\n }\n if (rootHeightTransitionTimerRef.current) {\n clearTimeout(rootHeightTransitionTimerRef.current);\n }\n };\n }, []);\n\n const isQuoteEditLocked = useCallback(\n () => swapStepRef.current === \"choose-swap-asset\",\n []\n );\n\n const getQuoteRequestDelay = useCallback(() => {\n if (immediateQuoteAfterSourceEditRef.current) {\n immediateQuoteAfterSourceEditRef.current = false;\n return 0;\n }\n return EXACT_OUT_INPUT_DEBOUNCE_MS;\n }, []);\n\n const startRootHeightTransition = useCallback(() => {\n setShouldAnimateRootHeight(true);\n if (rootHeightTransitionTimerRef.current) {\n clearTimeout(rootHeightTransitionTimerRef.current);\n }\n rootHeightTransitionTimerRef.current = setTimeout(() => {\n setShouldAnimateRootHeight(false);\n rootHeightTransitionTimerRef.current = null;\n }, ROOT_HEIGHT_TRANSITION_MS);\n }, []);\n\n const closeDrawerToIdle = useCallback(() => {\n const isDrawerStep =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n swapStep === \"enter-recipient\";\n\n if (!isDrawerStep) {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n return;\n }\n\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n }\n\n setClosingDrawerStep(swapStep);\n drawerCloseTimerRef.current = setTimeout(() => {\n if (\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\"\n ) {\n startRootHeightTransition();\n }\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n setClosingDrawerStep(null);\n drawerCloseTimerRef.current = null;\n }, DRAWER_CLOSE_MS);\n }, [startRootHeightTransition, swapStep]);\n\n const openDrawerStep = useCallback((nextStep: SwapStep) => {\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n drawerCloseTimerRef.current = null;\n }\n if (\n nextStep === \"choose-swap-asset\" ||\n nextStep === \"choose-receive-asset\"\n ) {\n startRootHeightTransition();\n }\n setClosingDrawerStep(null);\n swapStepRef.current = nextStep;\n setSwapStep(nextStep);\n }, [startRootHeightTransition]);\n\n const syncRootContentHeight = useCallback((animate = false) => {\n const element = rootContentRef.current;\n if (!element) return;\n\n const nextHeight = Math.ceil(\n Math.max(element.getBoundingClientRect().height, element.scrollHeight)\n );\n if (nextHeight <= 0) return;\n\n if (rootContentHeightRef.current === nextHeight) {\n setHasMeasuredRootContent(true);\n return;\n }\n\n rootContentHeightRef.current = nextHeight;\n setShouldAnimateRootHeight(animate);\n if (rootHeightTransitionTimerRef.current) {\n clearTimeout(rootHeightTransitionTimerRef.current);\n rootHeightTransitionTimerRef.current = null;\n }\n if (animate) {\n rootHeightTransitionTimerRef.current = setTimeout(() => {\n setShouldAnimateRootHeight(false);\n rootHeightTransitionTimerRef.current = null;\n }, ROOT_HEIGHT_TRANSITION_MS);\n }\n setRootContentHeight(nextHeight);\n setHasMeasuredRootContent(true);\n }, []);\n\n useLayoutEffect(() => {\n syncRootContentHeight(true);\n\n const element = rootContentRef.current;\n if (!element || typeof ResizeObserver === \"undefined\") return;\n\n let frame = 0;\n const observer = new ResizeObserver(() => {\n if (frame) {\n window.cancelAnimationFrame(frame);\n }\n frame = window.requestAnimationFrame(() => {\n syncRootContentHeight(false);\n });\n });\n\n observer.observe(element);\n\n return () => {\n if (frame) {\n window.cancelAnimationFrame(frame);\n }\n observer.disconnect();\n };\n }, [activeMode, swapStep, syncRootContentHeight]);\n\n useEffect(() => {\n currentSwapIdRef.current = currentSwapId;\n }, [currentSwapId]);\n\n useEffect(() => {\n if (historyStorageKeyRef.current === historyStorageKey) return;\n historyStorageKeyRef.current = historyStorageKey;\n skipNextHistoryPersistRef.current = true;\n setSwapHistory(readSwapHistoryFromStorage(historyStorageKey));\n }, [historyStorageKey]);\n\n useEffect(() => {\n if (skipNextHistoryPersistRef.current) {\n skipNextHistoryPersistRef.current = false;\n return;\n }\n\n writeSwapHistoryToStorage(historyStorageKey, swapHistory);\n }, [historyStorageKey, swapHistory]);\n\n useEffect(() => {\n if (swapStep !== \"history\") return;\n const timer = window.setInterval(() => setHistoryNow(Date.now()), 30000);\n return () => window.clearInterval(timer);\n }, [swapStep]);\n\n const normalizeAddress = (value?: string | null) =>\n (value ?? \"\").toLowerCase();\n\n const getTokenLookupAddress = (value?: string | null) => {\n if (!value) return \"\";\n return isNativeTokenAddress(value) ? zeroAddress : value.toLowerCase();\n };\n\n const findBalanceTokenLogo = (\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId) return \"\";\n const lookupAddress = getTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n let symbolMatchLogo = \"\";\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== chainId) continue;\n const breakdownAddress = getTokenLookupAddress(\n breakdown.contractAddress\n );\n const addressMatches =\n lookupAddress && breakdownAddress === lookupAddress;\n const symbolMatches =\n lookupSymbol &&\n [breakdown.symbol, asset.symbol]\n .filter(Boolean)\n .some((candidate) => candidate.toUpperCase() === lookupSymbol);\n const logo = getLogoFromMetadata(breakdown) || getLogoFromMetadata(asset);\n\n if (addressMatches && logo) return logo;\n if (!symbolMatchLogo && symbolMatches && logo) {\n symbolMatchLogo = logo;\n }\n }\n }\n\n return symbolMatchLogo;\n };\n\n const findSupportedTokenLogo = (\n chains: any[] | null | undefined,\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId) return \"\";\n const chain = chains?.find(\n (item: any) => Number(item?.id ?? item?.chainId) === chainId\n );\n const tokens = chain?.tokens ?? chain?.assets ?? [];\n const lookupAddress = getTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n\n const addressMatch =\n lookupAddress &&\n tokens.find((token: any) => {\n const tokenAddress = getTokenLookupAddress(\n token?.contractAddress ?? token?.address ?? token?.tokenAddress\n );\n return tokenAddress === lookupAddress;\n });\n const symbolMatch =\n lookupSymbol &&\n tokens.find((token: any) => {\n const tokenSymbol = (token?.symbol ?? token?.tokenSymbol ?? \"\")\n .toString()\n .toUpperCase();\n return tokenSymbol === lookupSymbol;\n });\n\n return getLogoFromMetadata(addressMatch) || getLogoFromMetadata(symbolMatch);\n };\n\n const resolveTokenLogo = (\n token:\n | {\n address?: string;\n contractAddress?: string;\n logo?: string;\n symbol?: string;\n tokenAddress?: string;\n tokenSymbol?: string;\n }\n | undefined,\n chainId?: number\n ) => {\n const explicitLogo = getLogoFromMetadata(token);\n if (explicitLogo) return explicitLogo;\n\n const contractAddress =\n token?.contractAddress ?? token?.address ?? token?.tokenAddress;\n const symbol = token?.symbol ?? token?.tokenSymbol;\n const citreaToken = findCitreaReceiveToken({\n address: contractAddress,\n chainId,\n symbol,\n });\n const tokenMeta = symbol\n ? TOKEN_METADATA[symbol as keyof typeof TOKEN_METADATA]\n : undefined;\n\n return (\n findBalanceTokenLogo(chainId, contractAddress, symbol) ||\n findSupportedTokenLogo(\n swapSupportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol\n ) ||\n findSupportedTokenLogo(\n supportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol\n ) ||\n citreaToken?.logo ||\n tokenMeta?.logo ||\n (contractAddress && isNativeTokenAddress(contractAddress)\n ? CHAIN_METADATA[chainId ?? 0]?.logo\n : \"\") ||\n \"\"\n );\n };\n\n const enrichIntentToken = <\n T extends {\n address?: string;\n contractAddress?: string;\n logo?: string;\n symbol?: string;\n tokenAddress?: string;\n tokenSymbol?: string;\n },\n >(\n token: T,\n chainId?: number\n ): T => {\n const logo = resolveTokenLogo(token, chainId);\n return logo && logo !== token.logo ? { ...token, logo } : token;\n };\n\n const enrichTokenOptionLogo = (\n token?: SwapTokenOption\n ): SwapTokenOption | undefined => {\n if (!token) return undefined;\n\n const chainMeta = token.chainId ? CHAIN_METADATA[token.chainId] : undefined;\n const logo = resolveTokenLogo(token, token.chainId);\n const next = {\n ...token,\n chainLogo: token.chainLogo || chainMeta?.logo,\n logo: token.logo || logo,\n };\n\n if (next.chainLogo === token.chainLogo && next.logo === token.logo) {\n return token;\n }\n\n return next;\n };\n\n const enrichSwapIntentTokenMetadata = (\n intent: SwapIntentData\n ): SwapIntentData => ({\n ...intent,\n destination: {\n ...intent.destination,\n token: enrichIntentToken(\n intent.destination.token,\n intent.destination.chain.id\n ),\n },\n sources: (intent.sources ?? []).map((source) => ({\n ...source,\n token: enrichIntentToken(source.token, source.chain.id),\n })),\n });\n\n const buildIntentSourceToken = (\n source: SwapIntentData[\"sources\"][number]\n ): SwapTokenOption => {\n let matchedAsset: any;\n let matchedBreakdown: any;\n const sourceAddress = normalizeAddress(source.token.contractAddress);\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const addressMatches =\n normalizeAddress(breakdown.contractAddress) === sourceAddress;\n const symbolMatches =\n breakdown.symbol === source.token.symbol ||\n asset.symbol === source.token.symbol;\n if (\n breakdown.chain?.id === source.chain.id &&\n (addressMatches || symbolMatches)\n ) {\n matchedAsset = asset;\n matchedBreakdown = breakdown;\n break;\n }\n }\n if (matchedBreakdown) break;\n }\n\n const chainMeta = CHAIN_METADATA[source.chain.id];\n const sourceValue = Number((source as any).value ?? 0);\n const isNativeSource = isNativeTokenAddress(source.token.contractAddress);\n const nativeCurrency = chainMeta?.nativeCurrency;\n const sourceSymbol =\n isNativeSource && (!source.token.symbol || !matchedAsset?.logo)\n ? nativeCurrency?.symbol || source.token.symbol\n : source.token.symbol || nativeCurrency?.symbol || \"\";\n const sourceDecimals =\n isNativeSource && nativeCurrency?.decimals !== undefined\n ? nativeCurrency.decimals\n : source.token.decimals;\n const sourceLogo = resolveTokenLogo(source.token, source.chain.id);\n\n return {\n contractAddress: source.token.contractAddress,\n symbol: sourceSymbol,\n name: sourceSymbol,\n logo: sourceLogo ?? \"\",\n decimals: sourceDecimals,\n balance: matchedBreakdown?.balance\n ? `${matchedBreakdown.balance} ${sourceSymbol}`\n : `${source.amount} ${sourceSymbol}`,\n balanceInFiat:\n matchedBreakdown?.balanceInFiat != null\n ? `$${Number(matchedBreakdown.balanceInFiat).toFixed(2)}`\n : Number.isFinite(sourceValue)\n ? `$${sourceValue.toFixed(2)}`\n : \"$0.00\",\n chainId: source.chain.id,\n chainName: getShortChainName(\n source.chain.id,\n chainMeta?.name ?? source.chain.name\n ),\n chainLogo: chainMeta?.logo ?? source.chain.logo,\n userAmount: source.amount,\n userAmountUsd: Number.isFinite(sourceValue) ? source.value : undefined,\n userAmountMode: \"token\",\n };\n };\n\n const clearPendingSwapIntent = (\n clearQuote = true,\n options: { keepQuoteRefreshing?: boolean } = {}\n ) => {\n swapRunIdRef.current += 1;\n swapIntentRef.current?.deny();\n swapIntentRef.current = null;\n setIntentLoading(false);\n setTxError(null);\n if (!options.keepQuoteRefreshing) {\n setQuoteRefreshing(false);\n }\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setSwapQuoteIssue(null);\n resetProgressEvents();\n if (swapStepsListRef.current.length > 0 || steps.length > 0) {\n swapStepsListRef.current = [];\n resetSteps();\n } else {\n swapStepsListRef.current = [];\n }\n if (clearQuote) {\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n if (!options.keepQuoteRefreshing) {\n setPredictiveQuote(null);\n }\n }\n };\n\n const clearSelectedSources = () => {\n setFromTokens((current) => (current.length === 0 ? current : []));\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setExactOutQuoteSourceModeValue(\"all\");\n };\n\n const resetExactOutSourcesToAuto = () => {\n setFromTokens((current) => (current.length === 0 ? current : []));\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setExactOutQuoteSourceModeValue(\"all\");\n setSourceSelectionRevision((current) => current + 1);\n };\n\n const getSourceAmountInput = (tokens: SwapTokenOption[]) => {\n const total = tokens.reduce(\n (sum, token) => sum + Number(token.userAmount || 0),\n 0\n );\n return total > 0 ? String(total) : \"\";\n };\n\n const parseFiatNumber = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = normalizeDecimalInputText(value);\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n };\n\n const minimumSourceUsd = new Decimal(1);\n const hasMinimumSourceUsdValue = (value: unknown) =>\n (parseFiatNumber(value) ?? new Decimal(0)).gte(minimumSourceUsd);\n const hasMinimumSourceUsdBalance = (\n token: Pick\n ) => hasMinimumSourceUsdValue(token.balanceInFiat);\n const filterMinimumSourceUsdTokens = (tokens: SwapTokenOption[]) =>\n tokens.filter(hasMinimumSourceUsdBalance);\n\n const getTokenUsdRateCacheKeyFromParts = (\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId || !symbol) return \"\";\n return [\n chainId,\n (contractAddress || zeroAddress).toLowerCase(),\n symbol.toUpperCase(),\n ].join(\":\");\n };\n\n const getTokenUsdRateCacheKey = (\n token?: Pick\n ) =>\n getTokenUsdRateCacheKeyFromParts(\n token?.chainId,\n token?.contractAddress,\n token?.symbol\n );\n\n const getSymbolUsdRateCacheKey = (symbol?: string) =>\n symbol ? symbol.trim().toUpperCase() : \"\";\n\n const getCachedIntentUsdRate = (\n token?: Pick\n ) => {\n const tokenKey = getTokenUsdRateCacheKey(token);\n const cached = tokenKey\n ? intentDestinationUsdRateCacheRef.current[tokenKey]\n : undefined;\n const rate = parseFiatNumber(cached?.rate);\n return rate && rate.gt(0) ? rate : undefined;\n };\n\n const cacheDestinationUsdRateFromIntent = (\n intent?: SwapIntentData | null\n ) => {\n const destination = intent?.destination;\n const amount = parseFiatNumber(destination?.amount);\n const value = parseFiatNumber(destination?.value);\n const chainId = destination?.chain?.id;\n const symbol = destination?.token?.symbol;\n\n if (\n !amount ||\n !value ||\n amount.lte(0) ||\n value.lte(0) ||\n !chainId ||\n !symbol\n ) {\n return;\n }\n\n const rate = value.div(amount);\n if (!rate.isFinite() || rate.lte(0)) return;\n\n const cached: CachedIntentUsdRate = {\n amount: amount.toFixed(),\n rate: rate.toDecimalPlaces(18).toFixed(),\n updatedAt: Date.now(),\n value: value.toFixed(),\n };\n const tokenKey = getTokenUsdRateCacheKeyFromParts(\n chainId,\n destination?.token?.contractAddress,\n symbol\n );\n if (tokenKey) {\n intentDestinationUsdRateCacheRef.current[tokenKey] = cached;\n }\n\n const symbolKey = getSymbolUsdRateCacheKey(symbol);\n if (symbolKey) {\n intentSymbolUsdRateCacheRef.current[symbolKey] = cached;\n }\n };\n\n const getSwapBalanceTotalUsd = () =>\n (swapBalance ?? []).reduce((sum, asset) => {\n const breakdown = asset.breakdown ?? [];\n if (breakdown.length > 0) {\n return sum.plus(\n breakdown.reduce((breakdownSum, item) => {\n const value = parseFiatNumber(item.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd)\n ? breakdownSum.plus(value)\n : breakdownSum;\n }, new Decimal(0))\n );\n }\n\n const value = parseFiatNumber(asset.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd) ? sum.plus(value) : sum;\n }, new Decimal(0));\n\n const getTokenUsdRate = (token: SwapTokenOption) => {\n const tokenBalance = parseFiatNumber(token.balance) ?? new Decimal(0);\n const fiatBalance = parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n if (tokenBalance.gt(0) && fiatBalance.gt(0)) {\n return fiatBalance.div(tokenBalance);\n }\n\n const priceUsd = parseFiatNumber(token.priceUSD);\n if (priceUsd && priceUsd.gt(0)) {\n return priceUsd;\n }\n\n const fallbackRate = getFiatValue(1, token.symbol);\n if (Number.isFinite(fallbackRate) && fallbackRate > 0) {\n return new Decimal(fallbackRate);\n }\n\n return getCachedIntentUsdRate(token) ?? new Decimal(0);\n };\n const getUsdRateForSymbol = (symbol?: string) => {\n if (!symbol) return new Decimal(0);\n const fiat = getFiatValue(1, symbol);\n if (Number.isFinite(fiat) && fiat > 0) {\n return new Decimal(fiat);\n }\n\n const cached =\n intentSymbolUsdRateCacheRef.current[getSymbolUsdRateCacheKey(symbol)];\n const rate = parseFiatNumber(cached?.rate);\n return rate && rate.gt(0) ? rate : new Decimal(0);\n };\n\n const getFeeTokenLookupAddress = (value?: string | null) => {\n const lower = (value ?? \"\").toLowerCase();\n if (!lower || lower === \"0x\" || isNativeTokenAddress(lower)) {\n return zeroAddress;\n }\n return lower;\n };\n\n const getTokenUsdRateFromBalances = (\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId) return undefined;\n const lookupAddress = getFeeTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== chainId) continue;\n const breakdownAddress = getFeeTokenLookupAddress(\n breakdown.contractAddress\n );\n const addressMatches =\n lookupAddress && breakdownAddress === lookupAddress;\n const symbolMatches =\n lookupSymbol &&\n [breakdown.symbol, asset.symbol]\n .filter(Boolean)\n .some((candidate) => candidate.toUpperCase() === lookupSymbol);\n if (!addressMatches && !symbolMatches) continue;\n\n const balance = parseFiatNumber(breakdown.balance);\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n if (balance && fiatBalance && balance.gt(0) && fiatBalance.gt(0)) {\n return fiatBalance.div(balance);\n }\n }\n }\n\n return undefined;\n };\n\n const getTokenUsdRateFromSupportedChains = (\n chains: any[] | null | undefined,\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId) return undefined;\n const chain = chains?.find(\n (item: any) => Number(item?.id ?? item?.chainId) === chainId\n );\n const tokens = chain?.tokens ?? chain?.assets ?? [];\n const lookupAddress = getFeeTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n const matchedToken = tokens.find((token: any) => {\n const tokenAddress = getFeeTokenLookupAddress(\n token?.contractAddress ?? token?.address ?? token?.tokenAddress\n );\n const addressMatches = lookupAddress && tokenAddress === lookupAddress;\n const tokenSymbol = (token?.symbol ?? token?.tokenSymbol ?? \"\")\n .toString()\n .toUpperCase();\n const symbolMatches = lookupSymbol && tokenSymbol === lookupSymbol;\n return addressMatches || symbolMatches;\n });\n const priceUsd = parseFiatNumber(\n matchedToken?.priceUSD ??\n matchedToken?.priceUsd ??\n matchedToken?.usdPrice ??\n matchedToken?.price\n );\n return priceUsd && priceUsd.gt(0) ? priceUsd : undefined;\n };\n\n const getIntentDestinationGasUsdValue = (\n intent?: SwapIntentData | null\n ) => {\n const gas = intent?.destination?.gas;\n const explicitValue = parseFiatNumber(gas?.value);\n if (explicitValue && explicitValue.gt(0)) return explicitValue;\n\n const amount = parseFiatNumber(gas?.amount);\n if (!amount || amount.lte(0)) return undefined;\n\n const chainId = intent?.destination?.chain?.id;\n const contractAddress = gas?.token?.contractAddress;\n const symbol = gas?.token?.symbol;\n const rate =\n getTokenUsdRateFromBalances(chainId, contractAddress, symbol) ??\n getTokenUsdRateFromSupportedChains(\n swapSupportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol\n ) ??\n getTokenUsdRateFromSupportedChains(\n supportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol\n ) ??\n getUsdRateForSymbol(symbol);\n\n return rate.gt(0) ? amount.mul(rate) : undefined;\n };\n\n const getCachedDestinationUsdRate = (token?: SwapTokenOption) => {\n return getCachedIntentUsdRate(token);\n };\n\n const resolveUsdRateForSymbol = async (symbol?: string) => {\n if (!symbol) return new Decimal(0);\n\n const localRate = getUsdRateForSymbol(symbol);\n if (localRate.gt(0)) return localRate;\n\n try {\n const resolvedRate = await resolveTokenUsdRate(symbol);\n return resolvedRate && resolvedRate > 0\n ? new Decimal(resolvedRate)\n : new Decimal(0);\n } catch {\n return new Decimal(0);\n }\n };\n\n const getTokenUsdValue = (\n token: SwapTokenOption,\n fallbackAmount?: string\n ) => {\n const amountNumber =\n parseFiatNumber(token.userAmount || fallbackAmount) ?? new Decimal(0);\n if (amountNumber.lte(0)) return new Decimal(0);\n const quotedUsd = parseFiatNumber(token.userAmountUsd);\n if (quotedUsd && quotedUsd.gte(0)) return quotedUsd;\n if (token.userAmountMode === \"usd\") return amountNumber;\n\n const rate = getTokenUsdRate(token);\n return rate.gt(0) ? amountNumber.mul(rate) : new Decimal(0);\n };\n\n const getTokenBalanceAmount = (token: SwapTokenOption) =>\n parseFiatNumber(token.balance) ?? new Decimal(0);\n\n const getTokenBalanceUsd = (token: SwapTokenOption) =>\n parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n\n const getSdkExactOutSourcePriority = (token: SwapTokenOption) => {\n const isSameChain =\n Boolean(token.chainId && toToken?.chainId) &&\n token.chainId === toToken?.chainId;\n const isSameToken =\n Boolean(token.contractAddress && toToken?.contractAddress) &&\n ((isNativeTokenAddress(token.contractAddress) &&\n isNativeTokenAddress(toToken?.contractAddress)) ||\n token.contractAddress.toLowerCase() ===\n toToken?.contractAddress.toLowerCase());\n const isStable = isSdkExactOutStableSymbol(token.symbol);\n const isGas = isNativeTokenAddress(token.contractAddress);\n const isEthereum = token.chainId === ETHEREUM_MAINNET_CHAIN_ID;\n\n if (isSameChain) {\n if (isSameToken) return 1;\n if (isStable) return 2;\n if (isGas) return 3;\n return 4;\n }\n\n if (isEthereum) {\n if (isSameToken) return 8;\n if (isStable) return 9;\n if (isGas) return 10;\n return 11;\n }\n\n if (isSameToken) return 5;\n if (isStable) return 6;\n return 7;\n };\n\n const sortExactOutSourcesBySdkPriority = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const priorityDiff =\n getSdkExactOutSourcePriority(a) - getSdkExactOutSourcePriority(b);\n if (priorityDiff !== 0) return priorityDiff;\n\n const usdDiff = getTokenBalanceUsd(b).cmp(getTokenBalanceUsd(a));\n if (usdDiff !== 0) return usdDiff;\n\n return `${a.symbol} ${a.chainName ?? \"\"}`.localeCompare(\n `${b.symbol} ${b.chainName ?? \"\"}`\n );\n });\n\n const getTokenAmountForUsd = (token: SwapTokenOption, usdAmount: Decimal) => {\n const rate = getTokenUsdRate(token);\n if (rate.lte(0) || usdAmount.lte(0)) return new Decimal(0);\n return usdAmount.div(rate);\n };\n\n const getUsdForTokenAmount = (\n token: SwapTokenOption,\n tokenAmount: Decimal\n ) => {\n const rate = getTokenUsdRate(token);\n if (rate.lte(0) || tokenAmount.lte(0)) return new Decimal(0);\n return tokenAmount.mul(rate);\n };\n\n const getExactOutDestinationBalanceCoverage = ({\n requestedAmount,\n requestedUsd,\n producedAmount,\n producedUsd,\n token = toToken,\n }: {\n requestedAmount?: Decimal;\n requestedUsd?: Decimal;\n producedAmount?: Decimal;\n producedUsd?: Decimal;\n token?: SwapTokenOption;\n }) => {\n if (\n (activeMode !== \"deposit\" && activeMode !== \"send\") ||\n !token ||\n !requestedAmount ||\n requestedAmount.lte(0)\n ) {\n return null;\n }\n\n const balanceAmount =\n parseFiatNumber(destinationBalance) ??\n parseFiatNumber(token.balance) ??\n new Decimal(0);\n if (balanceAmount.lte(0)) return null;\n\n const externalAmount =\n producedAmount && producedAmount.gt(0) ? producedAmount : new Decimal(0);\n const uncoveredAmount = Decimal.max(\n requestedAmount.minus(externalAmount),\n new Decimal(0)\n );\n const coveredAmount = Decimal.min(balanceAmount, uncoveredAmount);\n if (coveredAmount.lte(0)) return null;\n\n const requestedRate =\n requestedUsd && requestedUsd.gt(0)\n ? requestedUsd.div(requestedAmount)\n : undefined;\n const producedRate =\n producedUsd && producedUsd.gt(0) && producedAmount && producedAmount.gt(0)\n ? producedUsd.div(producedAmount)\n : undefined;\n const fallbackRate = getTokenUsdRate(token);\n const usdRate =\n requestedRate && requestedRate.gt(0)\n ? requestedRate\n : producedRate && producedRate.gt(0)\n ? producedRate\n : fallbackRate.gt(0)\n ? fallbackRate\n : undefined;\n\n return {\n amount: coveredAmount,\n usd: usdRate ? coveredAmount.mul(usdRate) : undefined,\n };\n };\n\n const buildDestinationBalanceDisplayToken = (\n coverage: ReturnType,\n token?: SwapTokenOption\n ): SwapTokenOption | null => {\n if (!coverage || !token || coverage.amount.lte(0)) return null;\n\n const amount = coverage.amount\n .toDecimalPlaces(Math.max(0, token.decimals ?? 18), Decimal.ROUND_DOWN)\n .toFixed();\n const usd = coverage.usd?.toDecimalPlaces(6, Decimal.ROUND_DOWN).toFixed();\n const balanceUsd = coverage.usd\n ? `$${coverage.usd.toDecimalPlaces(2, Decimal.ROUND_DOWN).toFixed()}`\n : token.balanceInFiat || \"$0.00\";\n\n return {\n ...token,\n balance: `${amount} ${token.symbol}`,\n balanceInFiat: balanceUsd,\n userAmount: amount,\n userAmountMode: \"token\",\n userAmountUsd: usd,\n };\n };\n\n const cacheSymbolUsdRate = (symbol: string | undefined, rate: Decimal) => {\n const symbolKey = getSymbolUsdRateCacheKey(symbol);\n if (!symbolKey || rate.lte(0)) return;\n\n intentSymbolUsdRateCacheRef.current[symbolKey] = {\n amount: \"1\",\n rate: rate.toDecimalPlaces(18).toFixed(),\n updatedAt: Date.now(),\n value: rate.toFixed(),\n };\n };\n\n const getPredictiveDestinationKey = (token?: SwapTokenOption) => {\n const tokenKey = getTokenUsdRateCacheKey(token);\n return tokenKey ? `destination:${tokenKey}` : \"\";\n };\n\n const getPredictiveSourceKey = (token: SwapTokenOption) =>\n [\n token.chainId ?? \"unknown\",\n (token.contractAddress || zeroAddress).toLowerCase(),\n token.symbol.toUpperCase(),\n ].join(\":\");\n\n const getPredictiveQuoteCacheKey = (\n mode = activeMode,\n type = swapType,\n destination = toToken,\n sources = fromTokens\n ) => {\n const destinationKey = getPredictiveDestinationKey(destination);\n if (!destinationKey) return \"\";\n if (mode !== \"swap\" || type !== \"exactIn\") {\n return `exactOut:${destinationKey}`;\n }\n\n const sourceKey = getExpandedSourceTokens(sources)\n .map(getPredictiveSourceKey)\n .sort()\n .join(\"+\");\n return sourceKey ? `exactIn:${sourceKey}->${destinationKey}` : \"\";\n };\n\n const getPredictiveDisplayAmount = (\n amount: Decimal,\n token?: Pick\n ) => {\n const decimals = Math.min(\n PREDICTIVE_QUOTE_DISPLAY_DECIMALS,\n Math.max(0, token?.decimals ?? 18)\n );\n return amount.toDecimalPlaces(decimals, Decimal.ROUND_DOWN).toFixed();\n };\n\n const resolveUsdRateForToken = async (token?: SwapTokenOption) => {\n if (!token?.symbol) return new Decimal(0);\n\n const localRate = getTokenUsdRate(token);\n if (localRate.gt(0)) return localRate;\n\n const resolvedRate = await resolveUsdRateForSymbol(token.symbol);\n if (resolvedRate.gt(0)) {\n cacheSymbolUsdRate(token.symbol, resolvedRate);\n }\n return resolvedRate;\n };\n\n const getDestinationReceiveLimitUsd = (token?: SwapTokenOption) => {\n if (!token?.chainId) return undefined;\n const limit = DESTINATION_RECEIVE_LIMIT_USD_BY_CHAIN_ID[token.chainId];\n return limit ? new Decimal(limit) : undefined;\n };\n\n const getImmediateDestinationReceiveUsdRate = (token?: SwapTokenOption) => {\n const priceUsd = parseFiatNumber(token?.priceUSD);\n if (priceUsd && priceUsd.gt(0)) return priceUsd;\n\n const cachedRate = getCachedDestinationUsdRate(token);\n if (cachedRate && cachedRate.gt(0)) return cachedRate;\n\n if (!token) return undefined;\n const localRate = getTokenUsdRate(token);\n return localRate.gt(0) ? localRate : undefined;\n };\n\n const getExactInSourceUsdForReceiveLimit = (\n sourceTokens: SwapTokenOption[],\n inputAmount: string\n ) => {\n if (sourceTokens.length === 0) return undefined;\n let hasPositiveSourceAmount = false;\n let totalUsd = new Decimal(0);\n\n for (const token of sourceTokens) {\n const fallbackAmount =\n sourceTokens.length === 1 ? inputAmount : undefined;\n const sourceAmount = parseFiatNumber(token.userAmount || fallbackAmount);\n if (!sourceAmount || sourceAmount.lte(0)) continue;\n\n hasPositiveSourceAmount = true;\n const sourceUsd = getTokenUsdValue(token, fallbackAmount);\n if (sourceUsd.lte(0)) return undefined;\n totalUsd = totalUsd.plus(sourceUsd);\n }\n\n return hasPositiveSourceAmount ? totalUsd : undefined;\n };\n\n const buildReceiveAmountIssue = ({\n destinationRate,\n destinationToken = toToken,\n inputAmount = amount,\n mode = activeMode,\n sourceTokens = fromTokens,\n type = swapType,\n }: {\n destinationRate?: Decimal;\n destinationToken?: SwapTokenOption;\n inputAmount?: string;\n mode?: NexusWidgetMode;\n sourceTokens?: SwapTokenOption[];\n type?: SwapType;\n } = {}): ReceiveAmountIssue | null => {\n const limit = getDestinationReceiveLimitUsd(destinationToken);\n if (!limit || !destinationToken) return null;\n\n const parsedAmount = parseFiatNumber(inputAmount);\n if (!parsedAmount || parsedAmount.lte(0)) return null;\n if (mode === \"swap\" && type === \"exactIn\" && sourceTokens.length === 0) {\n return null;\n }\n\n const chainName = getShortChainName(\n destinationToken.chainId,\n destinationToken.chainName\n );\n const resolvedDestinationRate =\n destinationRate ??\n getImmediateDestinationReceiveUsdRate(destinationToken);\n\n if (!resolvedDestinationRate || resolvedDestinationRate.lte(0)) {\n return {\n ctaLabel: \"Price unavailable\",\n message: `Unable to price ${destinationToken.symbol} on ${chainName}. Select another receive token.`,\n type: \"unpricedReceiveToken\",\n };\n }\n\n let receiveUsd: Decimal | undefined;\n if (mode === \"swap\" && type === \"exactIn\") {\n receiveUsd = getExactInSourceUsdForReceiveLimit(\n sourceTokens,\n inputAmount\n );\n if (!receiveUsd || receiveUsd.lte(0)) {\n return {\n ctaLabel: \"Price unavailable\",\n message: `Unable to price selected assets for ${chainName}'s receive limit.`,\n type: \"unpricedReceiveToken\",\n };\n }\n } else if (mode === \"deposit\" && depositAmountMode === \"usd\") {\n receiveUsd = parsedAmount;\n } else {\n receiveUsd = parsedAmount.mul(resolvedDestinationRate);\n }\n\n if (receiveUsd.gt(limit)) {\n return {\n ctaLabel: \"Receive limit exceeded\",\n message: `Maximum receive amount on ${chainName} is ${formatUsdDisplay(limit)}.`,\n type: \"receiveLimitExceeded\",\n };\n }\n\n return null;\n };\n\n const buildConfiguredAmountIssue = (\n inputAmount = amount\n ): ReceiveAmountIssue | null => {\n if (!amountInputConfig || !inputAmount) return null;\n const parsedAmount = parseFiatNumber(inputAmount);\n if (!parsedAmount || parsedAmount.lte(0)) return null;\n\n const minAmount = parseFiatNumber(amountInputConfig.min);\n if (minAmount && parsedAmount.lt(minAmount)) {\n return {\n ctaLabel: \"Below minimum\",\n message: `Minimum amount is ${minAmount.toFixed()}.`,\n type: \"configuredAmountLimit\",\n };\n }\n\n const maxAmount = parseFiatNumber(amountInputConfig.max);\n if (maxAmount && parsedAmount.gt(maxAmount)) {\n return {\n ctaLabel: \"Above maximum\",\n message: `Maximum amount is ${maxAmount.toFixed()}.`,\n type: \"configuredAmountLimit\",\n };\n }\n\n return null;\n };\n\n const clearPreviewForBlockingAmountIssue = () => {\n clearPendingSwapIntent(true);\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setTxError(null);\n setSwapQuoteIssue(null);\n };\n\n const applyReceiveAmountIssue = (issue: ReceiveAmountIssue | null) => {\n const key = issue ? `${issue.type}:${issue.message}` : \"\";\n receiveAmountIssueRef.current = issue;\n if (receiveAmountIssueKeyRef.current !== key) {\n receiveAmountIssueKeyRef.current = key;\n setReceiveAmountIssue(issue);\n }\n if (!issue) return;\n\n clearPendingSwapIntent(true);\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setTxError(null);\n };\n\n const getPredictiveExactInSourceTokens = () => {\n const expanded = getExpandedSourceTokens(fromTokens);\n if (expanded.length === 0) return [];\n\n return expanded\n .map((token) => {\n const userAmount =\n token.userAmount ||\n (expanded.length === 1 && hasPositiveDecimalInput(amount)\n ? amount\n : \"\");\n return { ...token, userAmount };\n })\n .filter((token) => hasPositiveDecimalInput(token.userAmount));\n };\n\n const sortUnifiedSourceTokens = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const fiatDiff = getTokenBalanceUsd(b).cmp(getTokenBalanceUsd(a));\n if (fiatDiff !== 0) return fiatDiff;\n return getTokenBalanceAmount(b).cmp(getTokenBalanceAmount(a));\n });\n\n const allocateUnifiedExactInToken = (\n token: SwapTokenOption,\n fallbackAmount?: string\n ) => {\n if (!token.isUnified || !token.sourceTokens?.length) return [token];\n\n const rawAmount =\n parseFiatNumber(token.userAmount || fallbackAmount) ?? new Decimal(0);\n if (rawAmount.lte(0)) return [];\n\n const sortedSources = sortUnifiedSourceTokens(token.sourceTokens).filter(\n (source) =>\n source.chainId &&\n source.contractAddress &&\n getTokenBalanceAmount(source).gt(0) &&\n hasMinimumSourceUsdBalance(source)\n );\n const allocated: SwapTokenOption[] = [];\n\n if (token.userAmountMode === \"usd\") {\n let remainingUsd = rawAmount;\n\n for (const source of sortedSources) {\n if (remainingUsd.lte(0)) break;\n\n const availableUsd = getTokenBalanceUsd(source);\n if (availableUsd.lte(0)) continue;\n\n const targetUsd = Decimal.min(remainingUsd, availableUsd);\n const tokenAmount = getTokenAmountForUsd(\n source,\n targetUsd\n ).toDecimalPlaces(\n Math.max(0, source.decimals || 18),\n Decimal.ROUND_DOWN\n );\n if (tokenAmount.lte(0)) continue;\n\n const actualUsd = getUsdForTokenAmount(source, tokenAmount);\n allocated.push({\n ...source,\n userAmount: tokenAmount.toFixed(),\n userAmountMode: \"token\",\n userAmountUsd: actualUsd\n .toDecimalPlaces(6, Decimal.ROUND_DOWN)\n .toFixed(),\n });\n remainingUsd = remainingUsd.minus(targetUsd);\n }\n\n return allocated;\n }\n\n let remainingTokenAmount = rawAmount;\n\n for (const source of sortedSources) {\n if (remainingTokenAmount.lte(0)) break;\n\n const availableTokenAmount = getTokenBalanceAmount(source);\n if (availableTokenAmount.lte(0)) continue;\n\n const tokenAmount = Decimal.min(\n remainingTokenAmount,\n availableTokenAmount\n ).toDecimalPlaces(Math.max(0, source.decimals || 18), Decimal.ROUND_DOWN);\n if (tokenAmount.lte(0)) continue;\n\n const actualUsd = getUsdForTokenAmount(source, tokenAmount);\n allocated.push({\n ...source,\n userAmount: tokenAmount.toFixed(),\n userAmountMode: \"token\",\n userAmountUsd: actualUsd\n .toDecimalPlaces(6, Decimal.ROUND_DOWN)\n .toFixed(),\n });\n remainingTokenAmount = remainingTokenAmount.minus(tokenAmount);\n }\n\n return allocated;\n };\n\n const getExactInSourceTokens = (\n tokens: SwapTokenOption[],\n fallbackAmount?: string\n ) =>\n tokens\n .flatMap((token) =>\n token.isUnified\n ? allocateUnifiedExactInToken(token, fallbackAmount)\n : [token]\n )\n .filter(hasMinimumSourceUsdBalance);\n\n const hasPositiveDecimalInput = (value: unknown) =>\n Boolean(parseFiatNumber(value)?.gt(0));\n\n const hasPositiveSourceSpend = (token: SwapTokenOption) =>\n hasPositiveDecimalInput(token.userAmount) ||\n hasPositiveDecimalInput(token.userAmountUsd);\n\n const getReadyExactInSourceTokens = (tokens: SwapTokenOption[]) =>\n getExactInSourceTokens(tokens).filter(\n (token) =>\n Boolean(token.chainId && token.contractAddress) &&\n hasPositiveDecimalInput(token.userAmount)\n );\n\n const hasReadyExactInSwapInput = (\n tokens: SwapTokenOption[],\n destination?: SwapTokenOption\n ) =>\n Boolean(\n destination?.chainId &&\n destination.contractAddress &&\n getReadyExactInSourceTokens(tokens).length > 0\n );\n\n const getExpandedSourceTokens = (tokens: SwapTokenOption[]) => {\n const expanded = tokens.flatMap((token) =>\n token.isUnified && token.sourceTokens?.length\n ? token.sourceTokens\n : [token]\n );\n const seen = new Set();\n return expanded.filter((token) => {\n if (!token.chainId || !token.contractAddress) return false;\n if (\n !isSwapSupportedBySdkChainList(\n token.chainId,\n swapSupportedChainsAndTokens\n )\n ) {\n return false;\n }\n const key = `${token.chainId}-${token.contractAddress.toLowerCase()}`;\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n });\n };\n\n const getNativeGasBalanceForChain = (chainId: number) => {\n const nativeSymbol =\n CHAIN_METADATA[chainId]?.nativeCurrency?.symbol?.toUpperCase();\n let balance = new Decimal(0);\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== chainId) continue;\n const breakdownSymbol = (\n breakdown.symbol ??\n asset.symbol ??\n \"\"\n ).toUpperCase();\n const assetSymbol = (asset.symbol ?? \"\").toUpperCase();\n const isNativeBalance =\n isNativeTokenAddress(breakdown.contractAddress) ||\n Boolean(\n nativeSymbol &&\n (breakdownSymbol === nativeSymbol || assetSymbol === nativeSymbol)\n );\n\n if (!isNativeBalance) continue;\n balance = balance.plus(\n parseFiatNumber(breakdown.balance) ?? new Decimal(0)\n );\n }\n }\n\n return balance;\n };\n\n const hasGasForSource = (token: SwapTokenOption) => {\n if (!token.chainId || !token.contractAddress) return false;\n const tokenBalance = parseFiatNumber(token.balance) ?? new Decimal(0);\n if (tokenBalance.lte(0)) return false;\n if (isNativeTokenAddress(token.contractAddress)) return true;\n return getNativeGasBalanceForChain(token.chainId).gt(0);\n };\n\n const getGasCapableBalanceSourceTokens = () => {\n const tokens: SwapTokenOption[] = [];\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const chainId = breakdown.chain?.id;\n const contractAddress = breakdown.contractAddress;\n const balance = parseFiatNumber(breakdown.balance) ?? new Decimal(0);\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n if (\n !chainId ||\n !contractAddress ||\n balance.lte(0) ||\n !fiatBalance ||\n fiatBalance.lt(minimumSourceUsd)\n )\n continue;\n\n const chainMeta = CHAIN_METADATA[chainId];\n const symbol = breakdown.symbol ?? asset.symbol;\n tokens.push({\n chainId,\n chainLogo: chainMeta?.logo ?? breakdown.chain?.logo,\n chainName: getShortChainName(\n chainId,\n chainMeta?.name ?? breakdown.chain?.name\n ),\n contractAddress,\n decimals: breakdown.decimals ?? asset.decimals ?? 18,\n logo: asset.logo ?? \"\",\n name: symbol,\n symbol,\n balance: `${breakdown.balance} ${symbol}`,\n balanceInFiat:\n fiatBalance !== undefined\n ? `$${fiatBalance.toDecimalPlaces(2).toFixed()}`\n : \"$0.00\",\n });\n }\n }\n\n return getExpandedSourceTokens(tokens).filter(hasGasForSource);\n };\n\n const getMinimumBalanceSourceTokens = () =>\n filterMinimumSourceUsdTokens(\n getExpandedSourceTokens(\n swapBalance\n ? deriveTokenOptions(swapBalance, swapSupportedChainsAndTokens)\n : []\n )\n );\n const getHeldDestinationTokenOption = () => {\n if (!toToken?.chainId || !toToken.contractAddress) return undefined;\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const chainId = breakdown.chain?.id;\n if (chainId !== toToken.chainId) continue;\n\n const breakdownAddress = breakdown.contractAddress;\n const addressMatches =\n breakdownAddress &&\n (breakdownAddress.toLowerCase() ===\n toToken.contractAddress.toLowerCase() ||\n (isNativeTokenAddress(breakdownAddress) &&\n isNativeTokenAddress(toToken.contractAddress)));\n const symbolMatches =\n (breakdown.symbol ?? asset.symbol ?? \"\").toUpperCase() ===\n toToken.symbol.toUpperCase();\n\n if (!addressMatches && !symbolMatches) continue;\n\n const balanceAmount = parseFiatNumber(breakdown.balance);\n if (!balanceAmount || balanceAmount.lte(0)) continue;\n\n const chainMeta = CHAIN_METADATA[chainId];\n const symbol = breakdown.symbol ?? asset.symbol ?? toToken.symbol;\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n\n return {\n chainId,\n chainLogo:\n chainMeta?.logo ?? breakdown.chain?.logo ?? toToken.chainLogo,\n chainName: getShortChainName(\n chainId,\n chainMeta?.name ?? breakdown.chain?.name ?? toToken.chainName\n ),\n contractAddress: breakdown.contractAddress ?? toToken.contractAddress,\n decimals:\n breakdown.decimals ?? asset.decimals ?? toToken.decimals ?? 18,\n logo: asset.logo ?? toToken.logo,\n name: symbol,\n symbol,\n balance: `${breakdown.balance} ${symbol}`,\n balanceInFiat:\n fiatBalance !== undefined\n ? `$${fiatBalance.toDecimalPlaces(2).toFixed()}`\n : \"$0.00\",\n } satisfies SwapTokenOption;\n }\n }\n\n return undefined;\n };\n\n const getDepositDestinationForSourceSelection = () => {\n const destination =\n activeMode === \"deposit\" ? selectedOpportunity : toToken;\n const chainId = destination?.chainId;\n const tokenAddress =\n activeMode === \"deposit\"\n ? selectedOpportunity?.tokenAddress\n : toToken?.contractAddress;\n const tokenSymbol =\n activeMode === \"deposit\"\n ? selectedOpportunity?.tokenSymbol\n : toToken?.symbol;\n\n if (!chainId || !tokenAddress || !tokenSymbol) return undefined;\n\n return {\n chainId,\n tokenAddress: tokenAddress as `0x${string}`,\n tokenSymbol,\n };\n };\n const getDestinationSourceIdForDeposit = () => {\n const destination = getDepositDestinationForSourceSelection();\n return destination\n ? getDepositSourceId(destination.tokenAddress, destination.chainId)\n : undefined;\n };\n const getDepositSourceTargetUsd = () => {\n if (activeMode !== \"deposit\") return undefined;\n const requestedUsd = depositUsdDecimal;\n if (!requestedUsd || requestedUsd.lte(0)) return undefined;\n\n const coverage = getExactOutDestinationBalanceCoverage({\n requestedAmount: depositTokenAmountForQuote,\n requestedUsd,\n token: toToken,\n });\n return Decimal.max(\n requestedUsd.minus(coverage?.usd ?? new Decimal(0)),\n new Decimal(0)\n );\n };\n\n const getDepositSourceIdsFromTokens = (tokens: SwapTokenOption[]) =>\n getExpandedSourceTokens(tokens)\n .filter((token) => token.chainId && token.contractAddress)\n .map((token) =>\n getDepositSourceId(token.contractAddress, token.chainId!)\n );\n\n const getDepositTokenOptionsBySourceId = () => {\n const map = new Map();\n const sourceTokens = [\n ...(swapBalance\n ? deriveTokenOptions(swapBalance, swapSupportedChainsAndTokens)\n : []),\n ...fromTokens,\n ];\n\n for (const token of getExpandedSourceTokens(sourceTokens)) {\n if (!token.chainId || !token.contractAddress) continue;\n const id = getDepositSourceId(token.contractAddress, token.chainId);\n if (!map.has(id)) {\n map.set(id, {\n ...token,\n userAmount: \"\",\n });\n }\n }\n\n return map;\n };\n\n const getDepositSourceTokensForIds = (sourceIds: string[]) => {\n const tokenBySourceId = getDepositTokenOptionsBySourceId();\n return sourceIds\n .map((sourceId) => tokenBySourceId.get(sourceId))\n .filter((token): token is SwapTokenOption => Boolean(token))\n .map((token) => ({ ...token, userAmount: \"\" }));\n };\n\n const getResolvedDepositSourceSelection = (options?: {\n filter?: DepositSourceFilter;\n selectedTokens?: SwapTokenOption[];\n isManualSelection?: boolean;\n targetAmountUsd?: Decimal;\n }) => {\n const destination = getDepositDestinationForSourceSelection();\n if (!destination) {\n return { sourcePoolIds: [], selectedSourceIds: [], fromSources: [] };\n }\n\n const manualSelection =\n options?.isManualSelection ?? sourceSelectionTouched;\n const selectedTokensForResolution = options?.selectedTokens ?? fromTokens;\n const selectedSourceIds = getDepositSourceIdsFromTokens(\n selectedTokensForResolution\n );\n const destinationSourceId = getDestinationSourceIdForDeposit();\n const targetAmountUsd =\n options?.targetAmountUsd ??\n (activeMode === \"deposit\"\n ? getDepositSourceTargetUsd()\n : activeMode === \"send\"\n ? new Decimal(sendAmountUsd || 0)\n : undefined);\n\n return resolveDepositSourceSelection({\n swapBalance,\n destination,\n filter: manualSelection\n ? \"custom\"\n : (options?.filter ?? depositSourceFilter),\n selectedSourceIds,\n isManualSelection: manualSelection,\n minimumBalanceUsd: minimumSourceUsd.toNumber(),\n targetAmountUsd: targetAmountUsd?.toNumber(),\n excludedSourceIds: destinationSourceId ? [destinationSourceId] : [],\n });\n };\n\n const getExactOutSourceTokens = (\n mode: \"all\" | \"selected\" = exactOutQuoteSourceModeRef.current,\n targetAmountUsd?: Decimal\n ) => {\n if (activeMode === \"deposit\") {\n const selection = getResolvedDepositSourceSelection({ targetAmountUsd });\n return getDepositSourceTokensForIds(\n mode === \"all\" ? selection.sourcePoolIds : selection.selectedSourceIds\n );\n }\n\n if (activeMode === \"send\" && mode === \"selected\" && fromTokens.length > 0) {\n return filterMinimumSourceUsdTokens(\n getExpandedSourceTokens(fromTokens)\n ).filter(hasGasForSource);\n }\n\n return getGasCapableBalanceSourceTokens();\n };\n\n const buildExplicitSourcesPayload = (tokens: SwapTokenOption[]) => {\n const eligibleTokens = filterMinimumSourceUsdTokens(tokens).filter(\n (token) => token.chainId && token.contractAddress\n );\n return {\n sources: eligibleTokens.map((token) => ({\n chainId: token.chainId!,\n tokenAddress: token.contractAddress as `0x${string}`,\n })),\n };\n };\n\n const getSdkSourceKey = (source: {\n chainId: number;\n tokenAddress: `0x${string}`;\n }) => {\n const normalizedAddress = isNativeTokenAddress(source.tokenAddress)\n ? zeroAddress\n : source.tokenAddress.toLowerCase();\n return `${source.chainId}:${normalizedAddress}`;\n };\n\n const dedupeSdkSources = (\n sources: Array<\n | {\n chainId: number;\n tokenAddress: `0x${string}`;\n }\n | undefined\n >\n ) => {\n const seen = new Set();\n return sources.filter(\n (\n source\n ): source is {\n chainId: number;\n tokenAddress: `0x${string}`;\n } => {\n if (!source) return false;\n const key = getSdkSourceKey(source);\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n }\n );\n };\n\n const getHeldDestinationTokenSource = () => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n if (\n !heldDestinationToken?.chainId ||\n !heldDestinationToken.contractAddress\n ) {\n return undefined;\n }\n\n return {\n chainId: heldDestinationToken.chainId,\n tokenAddress: heldDestinationToken.contractAddress as `0x${string}`,\n };\n };\n\n const shouldSendExactOutSourceAllowlist = () => {\n if (activeMode === \"deposit\") {\n return sourceSelectionTouched || depositSourceFilter !== \"all\";\n }\n\n if (activeMode === \"send\") {\n return exactOutQuoteSourceModeRef.current === \"selected\";\n }\n\n return true;\n };\n\n const buildExactOutSourcesPayload = (tokens: SwapTokenOption[]) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n return buildExplicitSourcesPayload(tokens);\n }\n\n const sourceTokens = shouldSendExactOutSourceAllowlist()\n ? tokens\n : getExactOutSourceTokens(\"all\");\n const explicitSources = buildExplicitSourcesPayload(sourceTokens).sources;\n const sources = dedupeSdkSources([\n ...explicitSources,\n getHeldDestinationTokenSource(),\n ]);\n\n return sources.length > 0 ? { sources } : {};\n };\n\n const buildPredictiveExactOutSources = async (requiredSourceUsd: Decimal) => {\n if (requiredSourceUsd.lte(0)) return [];\n\n const destinationKey = getTokenSelectionKey(toToken);\n const candidates = sortExactOutSourcesBySdkPriority(\n getExactOutSourceTokens(\n exactOutQuoteSourceModeRef.current,\n requiredSourceUsd\n )\n .filter((token) => getTokenSelectionKey(token) !== destinationKey)\n .filter(hasMinimumSourceUsdBalance)\n );\n const sources: SwapTokenOption[] = [];\n let remainingUsd = requiredSourceUsd;\n\n for (const token of candidates) {\n if (remainingUsd.lte(0)) break;\n\n const availableUsd = getTokenBalanceUsd(token);\n if (availableUsd.lt(minimumSourceUsd)) continue;\n\n const rate = await resolveUsdRateForToken(token);\n if (rate.lte(0)) continue;\n\n const targetUsd = Decimal.min(remainingUsd, availableUsd);\n const tokenAmount = targetUsd\n .div(rate)\n .toDecimalPlaces(Math.max(0, token.decimals || 18), Decimal.ROUND_DOWN);\n if (tokenAmount.lte(0)) continue;\n\n sources.push({\n ...token,\n userAmount: tokenAmount.toFixed(),\n userAmountMode: \"token\",\n userAmountUsd: targetUsd\n .toDecimalPlaces(6, Decimal.ROUND_DOWN)\n .toFixed(),\n });\n remainingUsd = remainingUsd.minus(targetUsd);\n }\n\n return remainingUsd.gt(0.01) ? [] : sources;\n };\n\n const getErrorText = (error: unknown) => {\n const err = error as any;\n const parts = [\n err?.message,\n typeof error === \"string\" ? error : undefined,\n err?.code,\n ];\n\n try {\n if (err?.data) parts.push(JSON.stringify(err.data));\n } catch {\n // Ignore non-serializable SDK error metadata.\n }\n\n return parts.filter(Boolean).join(\" \");\n };\n\n const isInsufficientSourcesError = (error: unknown) => {\n const err = error as any;\n const message = getErrorText(error).toLowerCase();\n\n return (\n err?.code === ERROR_CODES.INSUFFICIENT_BALANCE ||\n message.includes(\"insufficient balance\") ||\n message.includes(\"sources are not enough\") ||\n (message.includes(\"source\") && message.includes(\"not enough\"))\n );\n };\n\n const isViemInvalidDecimalError = (error: unknown) =>\n getErrorText(error).toLowerCase().includes(\"not a valid decimal number\");\n\n const parseLabeledErrorDecimal = (text: string, label: string) => {\n const match = text.match(\n new RegExp(`${label}\\\\s*:\\\\s*\\\\$?\\\\s*([0-9][0-9,]*(?:\\\\.[0-9]+)?)`, \"i\")\n );\n return match ? parseFiatNumber(match[1]) : undefined;\n };\n\n const getExactOutRequestedUsd = () => {\n const amountNumber = parseFiatNumber(amount);\n if (!amountNumber || amountNumber.lte(0) || !toToken?.symbol) {\n return undefined;\n }\n\n if (activeMode === \"deposit\" && depositAmountMode === \"usd\") {\n return amountNumber;\n }\n\n const fiatValue = getFiatValue(amountNumber.toNumber(), toToken.symbol);\n return Number.isFinite(fiatValue) && fiatValue > 0\n ? new Decimal(fiatValue)\n : undefined;\n };\n\n const getExactOutAvailableSourceUsd = (\n sourceTokensOverride?: SwapTokenOption[]\n ): Decimal => {\n const sumTokensWithDestinationCredit = (\n tokens: SwapTokenOption[]\n ): Decimal => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n const heldDestinationKey = getTokenSelectionKey(heldDestinationToken);\n let hasHeldDestinationToken = false;\n\n const sourceTotal = tokens.reduce((sum, token) => {\n const value = parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n const isHeldDestinationToken =\n heldDestinationKey &&\n getTokenSelectionKey(token) === heldDestinationKey;\n if (isHeldDestinationToken) {\n hasHeldDestinationToken = true;\n }\n\n return value.gt(0) &&\n (isHeldDestinationToken || value.gte(minimumSourceUsd))\n ? sum.plus(value)\n : sum;\n }, new Decimal(0));\n\n if (!heldDestinationToken || hasHeldDestinationToken) {\n return sourceTotal;\n }\n\n const heldDestinationUsd =\n parseFiatNumber(heldDestinationToken.balanceInFiat) ?? new Decimal(0);\n return heldDestinationUsd.gt(0)\n ? sourceTotal.plus(heldDestinationUsd)\n : sourceTotal;\n };\n\n if (\n sourceTokensOverride ||\n exactOutQuoteSourceModeRef.current === \"selected\"\n ) {\n const selectedSourceTokens = sourceTokensOverride ?? fromTokens;\n return sumTokensWithDestinationCredit(selectedSourceTokens);\n }\n\n return getExactOutTotalSourceBalanceUsd();\n };\n const getExactOutTotalSourceBalanceUsd = (): Decimal => {\n const allSourceTotal: Decimal = getExactOutAvailableSourceUsd(\n getMinimumBalanceSourceTokens()\n );\n\n return allSourceTotal.gt(0) ? allSourceTotal : getSwapBalanceTotalUsd();\n };\n\n const getExactOutIntentSourceUsd = () => {\n const intentSourceUsd = (intentData?.sources ?? []).reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0)\n );\n return intentSourceUsd.gt(0) ? intentSourceUsd : undefined;\n };\n\n const getExactOutHeldDestinationUsd = () => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n const value = parseFiatNumber(heldDestinationToken?.balanceInFiat);\n return value && value.gt(0) ? value : new Decimal(0);\n };\n\n const exactOutIntentIncludesHeldDestination = () => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n const heldDestinationKey = getTokenSelectionKey(heldDestinationToken);\n if (!heldDestinationKey) return false;\n\n return (intentData?.sources ?? []).some((source) => {\n const chainId = source.chain?.id;\n const tokenAddress = source.token?.contractAddress;\n if (!chainId || !tokenAddress) return false;\n\n return (\n getTokenSelectionKey({\n chainId,\n contractAddress: tokenAddress,\n } as SwapTokenOption) === heldDestinationKey\n );\n });\n };\n\n const getExactOutRequiredSourceUsd = () => {\n const intentSourceUsd = getExactOutIntentSourceUsd();\n if (intentSourceUsd) return intentSourceUsd;\n return undefined;\n };\n\n const getExactOutRequiredFundingUsd = () => {\n const sourceRequiredUsd = getExactOutRequiredSourceUsd();\n if (sourceRequiredUsd) {\n const destinationCreditUsd = exactOutIntentIncludesHeldDestination()\n ? new Decimal(0)\n : getExactOutHeldDestinationUsd();\n return sourceRequiredUsd.plus(destinationCreditUsd);\n }\n\n return getExactOutRequestedUsd();\n };\n\n const getExactInSourceDeficitUsd = () => {\n if (swapType !== \"exactIn\" || fromTokens.length === 0) return undefined;\n\n return fromTokens.reduce((sum, token) => {\n const requestedAmount = parseFiatNumber(token.userAmount);\n if (!requestedAmount || requestedAmount.lte(0)) return sum;\n\n if (token.userAmountMode === \"usd\") {\n const availableUsd = parseFiatNumber(token.balanceInFiat);\n if (!availableUsd || requestedAmount.lte(availableUsd)) return sum;\n return sum.plus(requestedAmount.minus(availableUsd));\n }\n\n const availableTokenAmount = parseFiatNumber(token.balance);\n if (!availableTokenAmount || requestedAmount.lte(availableTokenAmount)) {\n return sum;\n }\n\n const missingTokenAmount = requestedAmount.minus(availableTokenAmount);\n const fiatBalance = parseFiatNumber(token.balanceInFiat);\n if (fiatBalance && availableTokenAmount.gt(0)) {\n return sum.plus(\n missingTokenAmount.mul(fiatBalance.div(availableTokenAmount))\n );\n }\n\n return sum;\n }, new Decimal(0));\n };\n\n const buildInsufficientSourcesIssue = (error: unknown): SwapQuoteIssue => {\n const errorText = getErrorText(error);\n const details =\n (error as any)?.data?.details ?? (error as any)?.details ?? {};\n const requiredFromError =\n parseFiatNumber(\n details.requiredUsd ??\n details.requiredUSD ??\n details.requiredAmountUsd ??\n details.requiredAmount ??\n details.required\n ) ?? parseLabeledErrorDecimal(errorText, \"required\");\n const availableFromError =\n parseFiatNumber(\n details.availableUsd ??\n details.availableUSD ??\n details.availableAmountUsd ??\n details.availableAmount ??\n details.available\n ) ?? parseLabeledErrorDecimal(errorText, \"available\");\n const requestedUsd = getExactOutRequiredFundingUsd();\n const availableUsd = getExactOutAvailableSourceUsd();\n const exactInSourceDeficitUsd = getExactInSourceDeficitUsd();\n\n let missingUsd =\n exactInSourceDeficitUsd && exactInSourceDeficitUsd.gt(0)\n ? exactInSourceDeficitUsd\n : requiredFromError && availableFromError\n ? requiredFromError.minus(availableFromError)\n : undefined;\n\n if (\n requestedUsd &&\n (!missingUsd || missingUsd.lte(0) || missingUsd.gt(requestedUsd.mul(5)))\n ) {\n missingUsd = requestedUsd.minus(availableUsd);\n }\n\n if (missingUsd && missingUsd.gt(0)) {\n const formattedMissing =\n missingUsd.gt(0) && missingUsd.lt(0.01)\n ? \"<$0.01\"\n : formatUsdDisplay(missingUsd);\n\n return {\n type: \"insufficientSources\",\n missingUsd: missingUsd.toDecimalPlaces(2).toFixed(),\n message: `Need ${formattedMissing} more across your assets`,\n };\n }\n\n return {\n type: \"insufficientSources\",\n message: \"Add more source balance across your assets\",\n };\n };\n\n const buildExactOutSourceBalanceIssue = (\n sourceTokensOverride?: SwapTokenOption[]\n ): SwapQuoteIssue | null => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return null;\n const requestedUsd = getExactOutRequiredFundingUsd();\n const availableUsd = getExactOutAvailableSourceUsd(sourceTokensOverride);\n if (!requestedUsd || requestedUsd.lte(0) || !availableUsd) return null;\n\n const missingUsd = requestedUsd.minus(availableUsd);\n if (missingUsd.lte(0.01)) return null;\n\n const formattedMissing =\n missingUsd.gt(0) && missingUsd.lt(0.01)\n ? \"<$0.01\"\n : formatUsdDisplay(missingUsd);\n\n return {\n type: \"insufficientSources\",\n missingUsd: missingUsd.toDecimalPlaces(2).toFixed(),\n message: `Need ${formattedMissing} more across your assets`,\n };\n };\n\n const isNativeTokenAddress = (address?: string) =>\n !address ||\n address.toLowerCase() === \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\" ||\n address.toLowerCase() === \"0x0000000000000000000000000000000000000000\";\n\n const formatReadableTokenAmount = (rawAmount: bigint, decimals: number) =>\n new Decimal(rawAmount.toString())\n .div(new Decimal(10).pow(decimals))\n .toFixed();\n\n const formatReadableTokenBalanceAmount = (\n rawAmount: bigint,\n decimals: number\n ) =>\n new Decimal(rawAmount.toString())\n .div(new Decimal(10).pow(decimals))\n .toDecimalPlaces(6)\n .toFixed();\n\n const trimDecimalString = (value: string) =>\n value.replace(/(\\.\\d*?)0+$/, \"$1\").replace(/\\.$/, \"\");\n\n const buildSourceTokenSnapshotMap = useCallback(\n (balances: UserAsset[] | null | undefined) => {\n const snapshots = new Map();\n\n for (const asset of balances ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const chainId = breakdown.chain?.id;\n const contractAddress = breakdown.contractAddress;\n const symbol = breakdown.symbol ?? asset.symbol;\n\n if (\n !chainId ||\n !contractAddress ||\n !symbol ||\n !isSwapSupportedBySdkChainList(\n chainId,\n swapSupportedChainsAndTokens\n )\n ) {\n continue;\n }\n\n const chainMeta = CHAIN_METADATA[chainId];\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n const snapshot: SwapTokenOption = {\n balance: `${breakdown.balance ?? \"0\"} ${symbol}`,\n balanceInFiat: fiatBalance\n ? formatUsdDisplay(fiatBalance)\n : \"$0.00\",\n chainId,\n chainLogo: chainMeta?.logo ?? breakdown.chain?.logo,\n chainName: getShortChainName(\n chainId,\n chainMeta?.name ?? breakdown.chain?.name\n ),\n contractAddress,\n decimals: breakdown.decimals ?? asset.decimals ?? 18,\n logo: asset.logo ?? \"\",\n name: symbol,\n symbol,\n };\n snapshots.set(getTokenSelectionKey(snapshot), snapshot);\n }\n }\n\n return snapshots;\n },\n [swapSupportedChainsAndTokens]\n );\n\n const patchSourceTokensWithBalances = useCallback(\n (tokens: SwapTokenOption[], balances: UserAsset[]) => {\n const snapshots = buildSourceTokenSnapshotMap(balances);\n\n const updateToken = (token: SwapTokenOption): SwapTokenOption => {\n const preservedAmounts = {\n userAmount: token.userAmount,\n userAmountMode: token.userAmountMode,\n userAmountUsd: token.userAmountUsd,\n };\n\n if (token.isUnified) {\n const sourceTokens = (token.sourceTokens ?? []).map(updateToken);\n const totalBalance = sourceTokens.reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber(source.balance) ?? new Decimal(0)),\n new Decimal(0)\n );\n const totalFiat = sourceTokens.reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber(source.balanceInFiat) ?? new Decimal(0)),\n new Decimal(0)\n );\n\n return {\n ...token,\n ...preservedAmounts,\n balance: totalBalance.toDecimalPlaces(8).toFixed(),\n balanceInFiat: formatUsdDisplay(totalFiat),\n sourceTokens,\n };\n }\n\n const snapshot = snapshots.get(getTokenSelectionKey(token));\n if (!snapshot) {\n return {\n ...token,\n ...preservedAmounts,\n balance: `0 ${token.symbol}`,\n balanceInFiat: \"$0.00\",\n chainLogo:\n token.chainLogo ??\n (token.chainId ? CHAIN_METADATA[token.chainId]?.logo : undefined),\n chainName: getShortChainName(token.chainId, token.chainName),\n };\n }\n\n return {\n ...token,\n ...snapshot,\n ...preservedAmounts,\n };\n };\n\n return tokens.map(updateToken);\n },\n [buildSourceTokenSnapshotMap]\n );\n\n const refreshSelectedSourceBalances = useCallback(async () => {\n const refreshedBalance = await fetchSwapBalance();\n const balances = refreshedBalance ?? swapBalance;\n if (!balances) return;\n\n setFromTokens((current) =>\n current.length === 0\n ? current\n : patchSourceTokensWithBalances(current, balances)\n );\n setSourceSelectionRevision((current) => current + 1);\n }, [fetchSwapBalance, patchSourceTokensWithBalances, swapBalance]);\n\n const receiveMaxSafetyMultiplier = new Decimal(\"0.9\");\n const currentSwapEntry =\n currentSwapId !== null\n ? swapHistory.find((entry) => entry.id === currentSwapId)\n : undefined;\n\n const scheduleTerminalBalanceRefresh = () => {\n if (terminalBalanceRefreshTimerRef.current) {\n clearTimeout(terminalBalanceRefreshTimerRef.current);\n }\n\n terminalBalanceRefreshTimerRef.current = setTimeout(() => {\n terminalBalanceRefreshTimerRef.current = null;\n void fetchSwapBalance();\n }, BALANCE_REFRESH_AFTER_TERMINAL_MS);\n };\n\n const patchSwapHistoryEntry = (\n id: string | null | undefined,\n patch: Partial\n ) => {\n if (!id) return;\n setSwapHistory((prev) =>\n sortSwapHistoryEntries(\n prev.map((entry) => (entry.id === id ? { ...entry, ...patch } : entry))\n )\n );\n };\n\n const patchCurrentSwapHistoryEntry = (patch: Partial) => {\n patchSwapHistoryEntry(currentSwapIdRef.current, patch);\n };\n\n const patchCurrentIntentExplorerUrl = (url?: string | null) => {\n if (!isHttpUrl(url)) return;\n if (intentUrlRef.current === url) return;\n\n intentUrlRef.current = url;\n const intentId = extractIntentIdFromUrl(url);\n patchCurrentSwapHistoryEntry({\n intentExplorerUrl: url,\n ...(intentId ? { intentId } : {}),\n });\n };\n\n const resetExplorerUrls = () => {\n const next = { sourceExplorerUrl: null, destinationExplorerUrl: null };\n explorerUrlsRef.current = next;\n setExplorerUrls(next);\n };\n\n const mergeExplorerUrls = (\n patch: Partial<{\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n }>\n ) => {\n const next = { ...explorerUrlsRef.current, ...patch };\n explorerUrlsRef.current = next;\n setExplorerUrls(next);\n patchCurrentSwapHistoryEntry({\n sourceExplorerUrl: next.sourceExplorerUrl,\n finalExplorerUrl: next.destinationExplorerUrl ?? next.sourceExplorerUrl,\n });\n };\n\n const resetProgressEvents = () => {\n progressEventsRef.current = [];\n setProgressEvents((current) => (current.length === 0 ? current : []));\n setFailedProgressStep((current) => (current === null ? current : null));\n };\n\n const appendProgressEvent = (\n name: string,\n step: SwapStepType | BridgeStepType | undefined,\n defaultCompleted: boolean,\n event?: unknown\n ) => {\n if (!step) return;\n const completed =\n typeof (step as any).completed === \"boolean\"\n ? Boolean((step as any).completed)\n : defaultCompleted;\n\n setProgressEvents((prev) => {\n const next = [\n ...prev,\n {\n id: `${Date.now()}-${prev.length}-${(step as any).typeID ?? (step as any).type ?? name}`,\n name,\n completed,\n event,\n step,\n },\n ];\n progressEventsRef.current = next;\n return next;\n });\n };\n\n const appendProgressListEvent = (\n name: string,\n stepList: Array\n ) => {\n if (stepList.length === 0) return;\n\n setProgressEvents((prev) => {\n const next = [\n ...prev,\n {\n id: `${Date.now()}-${prev.length}-${name}`,\n name,\n completed: false,\n step: stepList[0],\n steps: stepList,\n },\n ];\n progressEventsRef.current = next;\n return next;\n });\n };\n\n const startSwapHistoryEntry = () => {\n const id = `${Date.now()}-${swapRunIdRef.current}`;\n const now = Date.now();\n const resolvedToToken =\n toToken && destinationBalance\n ? { ...toToken, balance: destinationBalance }\n : toToken;\n const entry: SwapHistoryEntry = {\n id,\n mode: activeMode,\n status: \"pending\",\n createdAt: now,\n startedAt: now,\n intentData,\n fromTokens,\n toToken: resolvedToToken,\n requestedToAmount:\n activeMode === \"deposit\" || activeMode === \"send\"\n ? previewDestinationAmount\n : undefined,\n requestedToValue:\n activeMode === \"deposit\" || activeMode === \"send\"\n ? previewToAmountUsd\n : undefined,\n recipientAddress: transferRecipientAddress,\n opportunity: selectedOpportunity,\n feeUsd: intentFeeUsd,\n sourceExplorerUrl: null,\n finalExplorerUrl: null,\n intentExplorerUrl: isHttpUrl(intentUrlRef.current)\n ? intentUrlRef.current\n : null,\n intentId: extractIntentIdFromUrl(intentUrlRef.current),\n autoRefundAvailable: false,\n };\n\n currentSwapStartedAtRef.current = 0;\n currentSwapIdRef.current = id;\n setCurrentSwapId(id);\n setSwapHistory((prev) => sortSwapHistoryEntries([entry, ...prev]));\n return id;\n };\n\n const finishCurrentSwapHistoryEntry = (\n status: \"fulfilled\" | \"failed\" | \"timeout\",\n patch: Partial = {}\n ) => {\n const now = Date.now();\n const startedAt = currentSwapStartedAtRef.current || now;\n patchSwapHistoryEntry(currentSwapIdRef.current, {\n status,\n endedAt: now,\n durationSeconds: Math.max(1, Math.round((now - startedAt) / 1000)),\n sourceExplorerUrl: explorerUrlsRef.current.sourceExplorerUrl,\n finalExplorerUrl:\n explorerUrlsRef.current.destinationExplorerUrl ??\n explorerUrlsRef.current.sourceExplorerUrl,\n ...patch,\n });\n scheduleTerminalBalanceRefresh();\n };\n\n const markSwapExecutionStarted = () => {\n if (currentSwapStartedAtRef.current > 0) return;\n const now = Date.now();\n currentSwapStartedAtRef.current = now;\n patchCurrentSwapHistoryEntry({ startedAt: now });\n };\n\n const enterSkippedSwapProgress = () => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n\n const shouldInitializeProgress = swapStepRef.current !== \"progress\";\n if (!currentSwapIdRef.current) {\n onStart?.();\n startSwapHistoryEntry();\n }\n\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setPreviewQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setSwapQuoteIssue(null);\n\n if (shouldInitializeProgress) {\n resetProgressEvents();\n swapStepsListRef.current = [];\n resetSteps();\n swapStepRef.current = \"progress\";\n setSwapStep(\"progress\");\n }\n };\n\n const cachePredictiveBaselineFromIntent = (intent: SwapIntentData) => {\n const destinationAmount = parseFiatNumber(intent.destination?.amount);\n const destinationValue = parseFiatNumber(intent.destination?.value);\n const sourceUsd = (intent.sources ?? []).reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0)\n );\n\n if (!destinationAmount || destinationAmount.lte(0)) return;\n\n const destinationUsdRate =\n destinationValue && destinationValue.gt(0)\n ? destinationValue.div(destinationAmount)\n : getUsdRateForSymbol(intent.destination?.token?.symbol);\n if (destinationUsdRate.lte(0)) return;\n\n cacheSymbolUsdRate(intent.destination?.token?.symbol, destinationUsdRate);\n\n const key = getPredictiveQuoteCacheKey();\n if (!key) return;\n\n const baseline: PredictiveQuoteBaseline = {\n destinationUsdRate: destinationUsdRate.toDecimalPlaces(18).toFixed(),\n updatedAt: Date.now(),\n };\n\n if (activeMode === \"swap\" && swapType === \"exactIn\" && sourceUsd.gt(0)) {\n baseline.exactInDestinationAmountPerSourceUsd = destinationAmount\n .div(sourceUsd)\n .toDecimalPlaces(18)\n .toFixed();\n }\n\n const resolvedDestinationValue =\n destinationValue && destinationValue.gt(0)\n ? destinationValue\n : destinationAmount.mul(destinationUsdRate);\n if (\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n resolvedDestinationValue.gt(0) &&\n sourceUsd.gt(0)\n ) {\n baseline.exactOutSourceUsdPerDestinationUsd = sourceUsd\n .div(resolvedDestinationValue)\n .toDecimalPlaces(18)\n .toFixed();\n }\n\n predictiveQuoteCacheRef.current[key] = baseline;\n };\n\n const applySwapIntent = useCallback(\n (intent: SwapIntentData) => {\n const sortedIntent = {\n ...intent,\n sources: sortIntentSourcesByUsdDesc(intent.sources ?? []),\n };\n const enrichedIntent = enrichSwapIntentTokenMetadata(sortedIntent);\n const sortedIntentSourceTokens = sortSwapTokensByUsdDesc(\n (enrichedIntent.sources ?? []).map(buildIntentSourceToken)\n );\n\n lastSwapIntentRefreshAtRef.current = Date.now();\n lastIntentSourceTokensRef.current = sortedIntentSourceTokens;\n if (\n !sourceSelectionTouched &&\n (activeMode === \"send\" ||\n (activeMode === \"deposit\" && swapType === \"exactOut\"))\n ) {\n lastAutoIntentSourceTokensRef.current = sortedIntentSourceTokens;\n }\n cacheDestinationUsdRateFromIntent(enrichedIntent);\n cachePredictiveBaselineFromIntent(enrichedIntent);\n setIntentData(enrichedIntent);\n setIntentToAmount(enrichedIntent.destination?.amount || undefined);\n setSwapQuoteIssue(null);\n\n if (\n !sourceSelectionTouched &&\n swapStepRef.current !== \"choose-swap-asset\" &&\n (activeMode === \"send\" ||\n (activeMode === \"deposit\" && swapType === \"exactOut\"))\n ) {\n syncingIntentSourcesRef.current = true;\n setFromTokens(sortedIntentSourceTokens);\n }\n\n try {\n const bridgeFees = enrichedIntent.feesAndBuffer?.bridge;\n const bridgeFeeData =\n bridgeFees && typeof bridgeFees === \"object\" ? bridgeFees : undefined;\n const collectionFee = parseFiatNumber(bridgeFeeData?.collection);\n const fulfilmentFee = parseFiatNumber(bridgeFeeData?.fulfilment);\n const executionGasFee =\n parseFiatNumber(bridgeFeeData?.caGas) ??\n (collectionFee !== undefined || fulfilmentFee !== undefined\n ? (collectionFee ?? new Decimal(0)).plus(\n fulfilmentFee ?? new Decimal(0)\n )\n : undefined);\n const bridgeGasSuppliedFee = parseFiatNumber(\n bridgeFeeData?.gasSupplied\n );\n const destinationGasSuppliedFee =\n getIntentDestinationGasUsdValue(enrichedIntent);\n const gasSuppliedFee =\n bridgeGasSuppliedFee ?? destinationGasSuppliedFee;\n const bridgeComponentsTotal = bridgeFeeData\n ? [\n executionGasFee,\n parseFiatNumber(bridgeFeeData.protocol),\n parseFiatNumber(bridgeFeeData.solver),\n gasSuppliedFee,\n ].reduce(\n (sum, value) => sum.plus(value ?? new Decimal(0)),\n new Decimal(0)\n )\n : undefined;\n const rawBridgeTotal =\n typeof bridgeFees === \"string\"\n ? parseFiatNumber(bridgeFees)\n : parseFiatNumber(bridgeFeeData?.total);\n const bridgeTotal =\n rawBridgeTotal &&\n !bridgeGasSuppliedFee &&\n destinationGasSuppliedFee &&\n destinationGasSuppliedFee.gt(0)\n ? rawBridgeTotal.plus(destinationGasSuppliedFee)\n : (rawBridgeTotal ??\n (bridgeComponentsTotal && bridgeComponentsTotal.gt(0)\n ? bridgeComponentsTotal\n : destinationGasSuppliedFee));\n\n if (bridgeTotal !== undefined) {\n setIntentFeeUsd(\n bridgeTotal.gt(0) ? bridgeTotal.toDecimalPlaces(6).toFixed() : \"0\"\n );\n } else {\n setIntentFeeUsd(undefined);\n }\n } catch (err) {\n console.warn(\"Could not resolve bridge fee total\", err);\n setIntentFeeUsd(undefined);\n }\n },\n [\n activeMode,\n fromTokens,\n getFiatValue,\n sourceSelectionTouched,\n swapType,\n swapBalance,\n swapSupportedChainsAndTokens,\n supportedChainsAndTokens,\n toToken,\n ]\n );\n\n const handleSwapIntentCallback = useCallback(\n (data: any, runId: number, quoteInputKey: string) => {\n const { intent, allow, deny, refresh } = data;\n const bridgeProvider = normalizeBridgeProvider(\n data?.bridgeProvider ??\n intent?.bridgeProvider ??\n intent?.normalizedIntent?.bridgeProvider ??\n intent?.swap?.bridgeProvider\n );\n const intentWithBridgeProvider = normalizeRenderableSwapIntentData(\n intent,\n bridgeProvider\n );\n logSdkIntentEvent(\"onIntent\", data, {\n bridgeProvider,\n currentRunId: swapRunIdRef.current,\n normalizedIntent: intentWithBridgeProvider,\n isCurrentRun: swapRunIdRef.current === runId,\n quoteInputKey,\n runId,\n });\n if (swapRunIdRef.current !== runId) {\n logSdkIntentEvent(\"ignored stale onIntent\", data, {\n currentRunId: swapRunIdRef.current,\n quoteInputKey,\n runId,\n });\n deny();\n return;\n }\n if (quoteInputKey && activeQuoteInputKeyRef.current !== quoteInputKey) {\n logSdkIntentEvent(\"ignored stale quote input onIntent\", data, {\n currentQuoteInputKey: activeQuoteInputKeyRef.current,\n quoteInputKey,\n runId,\n });\n deny();\n return;\n }\n if (!intentWithBridgeProvider) {\n console.warn(\"[NexusWidget SDK][intent] Unsupported intent shape\", {\n intent,\n raw: data,\n });\n finishIntentFetchTiming(runId, \"failed\");\n deny();\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setTxError(\"Quote unavailable\");\n return;\n }\n const resolvedQuoteInputKey = quoteInputKey;\n const normalizedRefresh =\n typeof refresh === \"function\"\n ? async (...args: unknown[]) => {\n const refreshed = await refresh(...args);\n const refreshedBridgeProvider = normalizeBridgeProvider(\n refreshed?.bridgeProvider ??\n refreshed?.normalizedIntent?.bridgeProvider ??\n refreshed?.swap?.bridgeProvider ??\n bridgeProvider\n );\n return (\n normalizeRenderableSwapIntentData(\n refreshed,\n refreshedBridgeProvider\n ) ?? refreshed\n );\n }\n : refresh;\n finishIntentFetchTiming(runId);\n providerSwapIntent.current = {\n intent: intentWithBridgeProvider as any,\n allow,\n deny,\n refresh: normalizedRefresh,\n };\n swapIntentRef.current = {\n intent: intentWithBridgeProvider,\n allow,\n deny,\n refresh: normalizedRefresh,\n runId,\n quoteInputKey: resolvedQuoteInputKey,\n };\n flushSync(() => {\n applySwapIntent(intentWithBridgeProvider);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n });\n },\n [applySwapIntent, finishIntentFetchTiming, providerSwapIntent]\n );\n\n // Deposit-specific\n const [selectedOpportunity, setSelectedOpportunity] = useState<\n NexusWidgetDepositOpportunityConfig | undefined\n >(() => (activeMode === \"deposit\" ? configuredDeposit : undefined));\n const selectedOpportunityIdentity =\n getDepositConfigIdentity(selectedOpportunity);\n const [depositAmountMode, setDepositAmountMode] = useState<\"token\" | \"usd\">(\n \"token\"\n );\n const [depositSourceFilter, setDepositSourceFilter] =\n useState(\"all\");\n\n useEffect(() => {\n const immediateIssue = buildReceiveAmountIssue();\n applyReceiveAmountIssue(immediateIssue);\n\n if (!toToken || !getDestinationReceiveLimitUsd(toToken)) return;\n if (!parseFiatNumber(amount)?.gt(0)) return;\n if (getImmediateDestinationReceiveUsdRate(toToken)?.gt(0)) return;\n\n let cancelled = false;\n void resolveUsdRateForToken(toToken)\n .then((resolvedRate) => {\n if (cancelled) return;\n const issue = buildReceiveAmountIssue({\n destinationRate: resolvedRate.gt(0) ? resolvedRate : undefined,\n });\n applyReceiveAmountIssue(issue);\n })\n .catch(() => {\n if (!cancelled) {\n applyReceiveAmountIssue(buildReceiveAmountIssue());\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [\n activeMode,\n amount,\n depositAmountMode,\n fromTokens,\n swapType,\n toToken?.chainId,\n toToken?.chainName,\n toToken?.contractAddress,\n toToken?.priceUSD,\n toToken?.symbol,\n ]);\n\n const trackDeposit = useCallback(\n (event: string, props?: Record) => {\n const analytics = nexusSDK?.analytics;\n if (!analytics) return;\n analytics.track(event, {\n widgetSessionId: widgetSessionIdRef.current,\n widgetAttemptId: widgetAttemptIdRef.current,\n opportunityProtocol: selectedOpportunity?.protocol ?? null,\n destinationChainId: selectedOpportunity?.chainId ?? null,\n destinationToken: selectedOpportunity?.tokenSymbol ?? null,\n ...props,\n });\n },\n [nexusSDK, selectedOpportunity]\n );\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (!nexusSDK?.analytics) return;\n if (widgetOpenedFiredRef.current) return;\n widgetOpenedFiredRef.current = true;\n widgetOpenedTsRef.current = Date.now();\n rotateAttempt();\n trackDeposit(\"deposit_widget_opened\", {\n embed: Boolean(embed),\n depositConfigured: Boolean(configuredDeposit),\n prefillAmountPresent: Boolean(config.prefill?.amount),\n });\n }, [\n activeMode,\n nexusSDK,\n embed,\n configuredDeposit,\n config.prefill,\n rotateAttempt,\n trackDeposit,\n ]);\n\n useEffect(() => {\n analyticsRef.current = nexusSDK?.analytics ?? null;\n }, [nexusSDK]);\n\n useEffect(() => {\n selectedOpportunityRef.current = selectedOpportunity;\n }, [selectedOpportunity]);\n\n useEffect(() => {\n return () => {\n if (!widgetOpenedFiredRef.current) return;\n const analytics = analyticsRef.current;\n if (!analytics) return;\n const opp = selectedOpportunityRef.current;\n analytics.track(\"deposit_widget_closed\", {\n widgetSessionId: widgetSessionIdRef.current,\n widgetAttemptId: widgetAttemptIdRef.current,\n opportunityProtocol: opp?.protocol ?? null,\n lastStep: swapStepRef.current,\n reachedTerminal: reachedTerminalRef.current,\n hadSimulationSuccess: hadSimulationSuccessRef.current,\n hadPreviewViewed: hadPreviewViewedRef.current,\n timeInWidgetMs: Date.now() - widgetOpenedTsRef.current,\n });\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const toTokenFromOpportunity = (\n opp: NexusWidgetDepositOpportunityMetadata\n ): SwapTokenOption => {\n const citreaToken = findCitreaReceiveToken({\n address: opp.tokenAddress,\n chainId: opp.chainId,\n symbol: opp.tokenSymbol,\n });\n const chainTokens = supportedChainsAndTokens?.find(\n (chain) => chain.id === opp.chainId\n )?.tokens;\n const matchedToken = chainTokens?.find(\n (token) =>\n token.contractAddress.toLowerCase() ===\n opp.tokenAddress.toLowerCase() || token.symbol === opp.tokenSymbol\n );\n const tokenSymbol =\n citreaToken?.symbol ?? matchedToken?.symbol ?? opp.tokenSymbol;\n const tokenMeta =\n TOKEN_METADATA[tokenSymbol as keyof typeof TOKEN_METADATA];\n\n return {\n chainId: opp.chainId,\n contractAddress: citreaToken?.contractAddress ?? opp.tokenAddress,\n symbol: tokenSymbol,\n name: matchedToken?.name || citreaToken?.name || tokenSymbol,\n balance: \"0\",\n balanceInFiat: \"$0.00\",\n decimals:\n matchedToken?.decimals ??\n citreaToken?.decimals ??\n opp.tokenDecimals ??\n tokenMeta?.decimals ??\n 18,\n logo:\n opp.tokenLogo ||\n matchedToken?.logo ||\n citreaToken?.logo ||\n tokenMeta?.logo,\n chainName: getShortChainName(\n opp.chainId,\n CHAIN_METADATA[opp.chainId]?.name ?? citreaToken?.chainName\n ),\n chainLogo: CHAIN_METADATA[opp.chainId]?.logo ?? citreaToken?.chainLogo,\n };\n };\n\n const getDestinationBalanceFromSwapBalances = (token?: SwapTokenOption) => {\n if (!token?.chainId || !token.contractAddress) return null;\n\n const targetAddress = token.contractAddress.toLowerCase();\n const targetSymbol = token.symbol.toUpperCase();\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== token.chainId) continue;\n\n const breakdownAddress = breakdown.contractAddress?.toLowerCase();\n const addressMatches =\n (breakdownAddress && breakdownAddress === targetAddress) ||\n (isNativeTokenAddress(breakdownAddress) &&\n isNativeTokenAddress(targetAddress));\n const symbolMatches =\n (breakdown.symbol ?? asset.symbol ?? \"\").toUpperCase() ===\n targetSymbol;\n\n if (!addressMatches && !symbolMatches) continue;\n\n const balance = parseFiatNumber(breakdown.balance);\n if (!balance) return null;\n\n return `${balance.toDecimalPlaces(6).toFixed()} ${token.symbol}`;\n }\n }\n\n return null;\n };\n\n const resolvePrefillToken = useCallback(\n (pair?: RuntimeDestinationPair) => {\n if (!pair?.token || !pair.chain) return undefined;\n\n const normalizeAddress = (address?: string) => {\n if (!address) return \"\";\n return isNativeTokenAddress(address)\n ? zeroAddress\n : address.toLowerCase();\n };\n const targetAddress = normalizeAddress(pair.token);\n\n const balanceToken = deriveTokenOptions(\n swapBalance ?? [],\n swapSupportedChainsAndTokens\n ).find(\n (token) =>\n token.chainId === pair.chain &&\n normalizeAddress(token.contractAddress) === targetAddress\n );\n if (balanceToken) return balanceToken;\n\n const chain = supportedChainsAndTokens?.find(\n (item) => item.id === pair.chain\n );\n const matchedToken = chain?.tokens?.find(\n (token) => normalizeAddress(token.contractAddress) === targetAddress\n );\n const citreaToken = findCitreaReceiveToken({\n address: pair.token,\n chainId: pair.chain,\n });\n const tokenAddressSymbol = Object.entries(\n TOKEN_CONTRACT_ADDRESSES as Record>\n ).find(\n ([, addresses]) =>\n normalizeAddress(addresses[pair.chain]) === targetAddress\n )?.[0];\n const chainMeta = CHAIN_METADATA[pair.chain];\n const isNativePrefill = isNativeTokenAddress(pair.token);\n const tokenSymbol =\n matchedToken?.symbol ??\n citreaToken?.symbol ??\n pair.symbol ??\n tokenAddressSymbol ??\n (isNativePrefill ? chainMeta?.nativeCurrency?.symbol : undefined) ??\n \"Token\";\n const tokenMeta =\n TOKEN_METADATA[tokenSymbol as keyof typeof TOKEN_METADATA];\n\n if (\n !chain &&\n !matchedToken &&\n !citreaToken &&\n !tokenAddressSymbol &&\n !pair.symbol &&\n !isNativePrefill\n ) {\n return undefined;\n }\n\n return {\n chainId: pair.chain,\n contractAddress: citreaToken?.contractAddress ?? pair.token,\n symbol: tokenSymbol,\n name: matchedToken?.name || citreaToken?.name || tokenSymbol,\n balance: `0 ${tokenSymbol}`,\n balanceInFiat: \"$0.00\",\n decimals:\n matchedToken?.decimals ??\n citreaToken?.decimals ??\n pair.decimals ??\n tokenMeta?.decimals ??\n (isNativePrefill ? chainMeta?.nativeCurrency?.decimals : undefined) ??\n 18,\n logo:\n matchedToken?.logo ||\n citreaToken?.logo ||\n pair.logo ||\n tokenMeta?.logo,\n chainName: getShortChainName(\n pair.chain,\n chain?.name ?? chainMeta?.name ?? citreaToken?.chainName\n ),\n chainLogo: chainMeta?.logo ?? chain?.logo ?? citreaToken?.chainLogo,\n } satisfies SwapTokenOption;\n },\n [supportedChainsAndTokens, swapBalance]\n );\n\n useEffect(() => {\n if (activeMode !== \"swap\") return;\n\n const sourcePrefill = config.prefill?.source;\n const destinationPrefill = config.prefill?.destination;\n if (!sourcePrefill && !destinationPrefill) return;\n\n const prefillKey = [\n sourcePrefill\n ? `source:${sourcePrefill.chain}:${sourcePrefill.token.toLowerCase()}`\n : \"\",\n destinationPrefill\n ? `destination:${destinationPrefill.chain}:${destinationPrefill.token.toLowerCase()}`\n : \"\",\n config.prefill?.amount ? `amount:${config.prefill.amount}` : \"\",\n ].join(\"|\");\n\n if (appliedTokenPrefillRef.current === prefillKey) return;\n\n const sourceToken = resolvePrefillToken(sourcePrefill);\n const destinationToken = resolvePrefillToken(destinationPrefill);\n\n if (sourcePrefill && !sourceToken) return;\n if (destinationPrefill && !destinationToken) return;\n\n if (sourceToken) {\n setFromTokens((current) => {\n const nextSourceToken = {\n ...sourceToken,\n userAmount: config.prefill?.amount ?? \"\",\n };\n const currentSourceToken = current[0];\n if (\n current.length === 1 &&\n isSameTokenSelection(currentSourceToken, nextSourceToken) &&\n currentSourceToken.userAmount === nextSourceToken.userAmount\n ) {\n return current;\n }\n return [nextSourceToken];\n });\n setSourceSelectionTouched(true);\n }\n if (destinationToken) {\n setToToken((current) =>\n isSameTokenSelection(current, destinationToken)\n ? current\n : destinationToken\n );\n }\n setSwapType(\"exactIn\");\n appliedTokenPrefillRef.current = prefillKey;\n }, [\n activeMode,\n config.prefill?.amount,\n config.prefill?.destination?.chain,\n config.prefill?.destination?.decimals,\n config.prefill?.destination?.logo,\n config.prefill?.destination?.symbol,\n config.prefill?.destination?.token,\n config.prefill?.source?.chain,\n config.prefill?.source?.decimals,\n config.prefill?.source?.logo,\n config.prefill?.source?.symbol,\n config.prefill?.source?.token,\n resolvePrefillToken,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"send\") return;\n\n const sendPrefill =\n config.prefill?.token && config.prefill?.chain\n ? {\n token: config.prefill.token,\n chain: config.prefill.chain,\n }\n : config.prefill?.destination;\n if (!sendPrefill) return;\n\n const prefillKey = `send:${sendPrefill.chain}:${sendPrefill.token.toLowerCase()}`;\n if (appliedTokenPrefillRef.current === prefillKey) return;\n\n const token = resolvePrefillToken(sendPrefill);\n if (!token) return;\n\n setToToken(token);\n setSwapType(\"exactOut\");\n appliedTokenPrefillRef.current = prefillKey;\n }, [\n activeMode,\n config.prefill?.chain,\n config.prefill?.destination?.chain,\n config.prefill?.destination?.decimals,\n config.prefill?.destination?.logo,\n config.prefill?.destination?.symbol,\n config.prefill?.destination?.token,\n config.prefill?.token,\n resolvePrefillToken,\n ]);\n\n useEffect(() => {\n if (config.prefill?.amount) setAmount(config.prefill.amount);\n if (config.prefill?.recipient)\n setRecipientAddress(config.prefill.recipient);\n }, [config.prefill?.amount, config.prefill?.recipient]);\n\n useEffect(() => {\n setDestinationBalance(null);\n\n const balanceToken =\n toToken ??\n (activeMode === \"deposit\" && selectedOpportunity\n ? toTokenFromOpportunity(selectedOpportunity)\n : undefined);\n\n if (!balanceToken?.chainId || !ownerAddress) return;\n\n const swapBalanceValue =\n getDestinationBalanceFromSwapBalances(balanceToken);\n if (swapBalanceValue) {\n setDestinationBalance(swapBalanceValue);\n }\n }, [\n activeMode,\n ownerAddress,\n selectedOpportunity?.chainId,\n selectedOpportunity?.tokenAddress,\n selectedOpportunity?.tokenLogo,\n selectedOpportunity?.tokenSymbol,\n swapBalance,\n toToken?.chainId,\n toToken?.chainName,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || !configuredDeposit) return;\n setSelectedOpportunity((current) =>\n isSameDepositConfig(current, configuredDeposit)\n ? current\n : configuredDeposit\n );\n setSwapType(\"exactOut\");\n setToToken((current) => {\n const next = {\n ...toTokenFromOpportunity(configuredDeposit),\n balance: current?.balance ?? \"0\",\n balanceInFiat: current?.balanceInFiat ?? \"$0.00\",\n };\n if (\n current &&\n current.chainId === next.chainId &&\n current.contractAddress.toLowerCase() ===\n next.contractAddress.toLowerCase() &&\n current.symbol === next.symbol &&\n current.decimals === next.decimals &&\n current.logo === next.logo &&\n current.chainLogo === next.chainLogo &&\n current.chainName === next.chainName &&\n current.balance === next.balance &&\n current.balanceInFiat === next.balanceInFiat\n ) {\n return current;\n }\n return next;\n });\n }, [\n activeMode,\n configuredDepositIdentity,\n configuredDeposit?.chainId,\n configuredDeposit?.tokenAddress,\n configuredDeposit?.tokenLogo,\n configuredDeposit?.tokenSymbol,\n supportedChainsAndTokens,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || !selectedOpportunity) return;\n setToToken((current) => {\n const next = {\n ...toTokenFromOpportunity(selectedOpportunity),\n balance: current?.balance ?? \"0\",\n balanceInFiat: current?.balanceInFiat ?? \"$0.00\",\n };\n if (\n current &&\n current.chainId === next.chainId &&\n current.contractAddress.toLowerCase() ===\n next.contractAddress.toLowerCase() &&\n current.symbol === next.symbol &&\n current.decimals === next.decimals &&\n current.logo === next.logo &&\n current.chainLogo === next.chainLogo &&\n current.chainName === next.chainName &&\n current.balance === next.balance &&\n current.balanceInFiat === next.balanceInFiat\n ) {\n return current;\n }\n return next;\n });\n }, [activeMode, selectedOpportunity, supportedChainsAndTokens]);\n\n useEffect(() => {\n if (activeMode !== \"send\") return;\n setSwapType(\"exactOut\");\n }, [activeMode]);\n\n useEffect(() => {\n if (activeMode === \"swap\" && swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n }, [activeMode, swapType]);\n\n useEffect(() => {\n if (!toToken?.symbol) return;\n if (getFiatValue(1, toToken.symbol) > 0) return;\n\n let cancelled = false;\n void resolveTokenUsdRate(toToken.symbol).catch((error) => {\n if (!cancelled) {\n console.warn(\"Unable to resolve Nexus Widget token USD rate\", {\n symbol: toToken.symbol,\n error,\n });\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [activeMode, getFiatValue, resolveTokenUsdRate, toToken?.symbol]);\n\n // Balance helpers\n const activeBalanceArray = swapBalance;\n const selectedToken = config.prefill?.token ?? \"USDC\";\n const currentAsset =\n activeBalanceArray?.find((a) => a.symbol === selectedToken) ||\n activeBalanceArray?.[0];\n const maxBalance = currentAsset?.balance\n ? String(currentAsset.balance)\n : undefined;\n const usdValue = getFiatValue(\n Number(amount) || 0,\n currentAsset?.symbol || \"USDC\"\n );\n const getDepositTokenUsdRate = () => {\n if (selectedOpportunity?.tokenSymbol) {\n const fiat = getFiatValue(1, selectedOpportunity.tokenSymbol);\n if (Number.isFinite(fiat) && fiat > 0) {\n return new Decimal(fiat);\n }\n }\n\n if (toToken) {\n const tokenRate = getTokenUsdRate(toToken);\n if (tokenRate.gt(0)) return tokenRate;\n }\n\n return getCachedDestinationUsdRate(toToken) ?? new Decimal(0);\n };\n const getDepositTokenAmountForQuote = () => {\n const parsedAmount = parseFiatNumber(amount) ?? new Decimal(0);\n if (parsedAmount.lte(0)) return undefined;\n if (depositAmountMode === \"token\") return parsedAmount;\n\n const rate = getDepositTokenUsdRate();\n if (rate.lte(0)) return undefined;\n return parsedAmount.div(rate);\n };\n const depositTokenAmountForQuote = getDepositTokenAmountForQuote();\n const depositQuoteAmountKey = depositTokenAmountForQuote?.toFixed() ?? \"\";\n const depositUsdDecimal =\n depositAmountMode === \"usd\"\n ? (parseFiatNumber(amount) ?? new Decimal(0))\n : depositTokenAmountForQuote\n ? depositTokenAmountForQuote.mul(getDepositTokenUsdRate())\n : new Decimal(0);\n const depositUsdDisplay = depositUsdDecimal.toDecimalPlaces(2).toFixed();\n const depositTokenDisplay =\n depositTokenAmountForQuote\n ?.toDecimalPlaces(\n getCappedTokenDisplayDecimals(toToken?.decimals),\n Decimal.ROUND_DOWN\n )\n .toFixed() ?? \"0\";\n const isExactOutMode = activeMode === \"deposit\" || activeMode === \"send\";\n const getActiveTotalBalanceUsd = () =>\n isExactOutMode\n ? getExactOutTotalSourceBalanceUsd()\n : getSwapBalanceTotalUsd();\n const getTotalBalancePercentUsdAmount = (pct: number) => {\n const ratio =\n isExactOutMode && pct === 100\n ? receiveMaxSafetyMultiplier\n : new Decimal(pct).div(100);\n return getActiveTotalBalanceUsd().mul(ratio);\n };\n const getExactOutDestinationUsdRate = (token: SwapTokenOption) => {\n if (activeMode === \"deposit\") {\n return getDepositTokenUsdRate();\n }\n return getTokenUsdRate(token);\n };\n const getExactOutPercentAmountFromBalance = (\n token: SwapTokenOption,\n pct: number,\n preferUsd: boolean\n ) => {\n const usdAmount = getTotalBalancePercentUsdAmount(pct);\n if (usdAmount.lte(0)) return undefined;\n\n if (preferUsd) {\n return {\n amount: usdAmount.toDecimalPlaces(2, Decimal.ROUND_DOWN).toFixed(),\n mode: \"usd\" as const,\n };\n }\n\n const rate = getExactOutDestinationUsdRate(token);\n if (rate.lte(0)) return undefined;\n\n return {\n amount: usdAmount\n .div(rate)\n .toDecimalPlaces(\n getCappedTokenDisplayDecimals(token.decimals),\n Decimal.ROUND_DOWN\n )\n .toFixed(),\n mode: \"token\" as const,\n };\n };\n const depositSourceTargetUsdKey =\n activeMode === \"deposit\"\n ? (getDepositSourceTargetUsd()?.toFixed() ?? \"\")\n : \"\";\n const normalizedQuoteAmountKey = parseFiatNumber(amount)?.toFixed() ?? \"\";\n const quoteRecipientKey =\n activeMode === \"swap\"\n ? effectiveRecipientAddress\n : activeMode === \"send\"\n ? recipientAddress\n : \"\";\n const activeQuoteInputKey = [\n activeMode,\n swapType,\n normalizedQuoteAmountKey,\n toTokenQuoteKey,\n quoteRecipientKey.toLowerCase(),\n activeMode === \"swap\" ? fromTokensQuoteKey : \"\",\n activeMode === \"deposit\"\n ? [\n depositAmountMode,\n depositQuoteAmountKey,\n selectedOpportunityIdentity,\n depositSourceTargetUsdKey,\n depositSourceFilter,\n sourceSelectionTouched ? \"manual\" : \"auto\",\n sourceSelectionRevision,\n exactOutQuoteSourceMode,\n ].join(\":\")\n : \"\",\n activeMode === \"send\"\n ? [\n sourceSelectionTouched ? \"manual\" : \"auto\",\n sourceSelectionRevision,\n exactOutQuoteSourceMode,\n ].join(\":\")\n : \"\",\n ].join(\"|\");\n\n const exactOutAutoSourceInputKey = [\n activeMode,\n normalizedQuoteAmountKey,\n toTokenQuoteKey,\n quoteRecipientKey.toLowerCase(),\n activeMode === \"deposit\"\n ? [\n depositAmountMode,\n depositQuoteAmountKey,\n selectedOpportunityIdentity,\n depositSourceTargetUsdKey,\n ].join(\":\")\n : \"\",\n ].join(\"|\");\n\n activeQuoteInputKeyRef.current = activeQuoteInputKey;\n\n useEffect(() => {\n lastAutoIntentSourceTokensRef.current = [];\n }, [exactOutAutoSourceInputKey]);\n\n useEffect(() => {\n setTxError(null);\n }, [activeQuoteInputKey]);\n const hasCurrentQuoteIntent = Boolean(\n intentData &&\n swapIntentRef.current &&\n swapIntentRef.current.runId === swapRunIdRef.current &&\n swapIntentRef.current.quoteInputKey === activeQuoteInputKey\n );\n\n useEffect(() => {\n if (swapStep !== \"idle\") return;\n if (!hasCurrentQuoteIntent) return;\n if (sourceSelectionTouched) return;\n if (\n activeMode !== \"send\" &&\n !(activeMode === \"deposit\" && swapType === \"exactOut\")\n ) {\n return;\n }\n\n const nextTokens = lastIntentSourceTokensRef.current;\n if (nextTokens.length === 0) return;\n\n setFromTokens((current) => {\n if (\n getSourceTokenListSyncKey(current) ===\n getSourceTokenListSyncKey(nextTokens)\n ) {\n return current;\n }\n\n syncingIntentSourcesRef.current = true;\n return nextTokens;\n });\n }, [\n activeMode,\n hasCurrentQuoteIntent,\n sourceSelectionTouched,\n swapStep,\n swapType,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (!nexusSDK?.analytics) return;\n const parsed = parseFiatNumber(amount);\n if (!parsed || parsed.lte(0)) return;\n if (amount === amountEnteredLastValueRef.current) return;\n const timeout = setTimeout(() => {\n amountEnteredLastValueRef.current = amount;\n trackDeposit(\"deposit_amount_entered\", {\n amountToken: depositTokenDisplay,\n amountUsd: Number(depositUsdDisplay) || 0,\n inputMethod: lastInputMethodRef.current,\n });\n lastInputMethodRef.current = \"typed\";\n }, 500);\n return () => clearTimeout(timeout);\n }, [\n amount,\n activeMode,\n nexusSDK,\n depositTokenDisplay,\n depositUsdDisplay,\n trackDeposit,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (intentData) hadSimulationSuccessRef.current = true;\n }, [intentData, activeMode]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (sourceSelectionTouched) return;\n previousAutoSourceCountRef.current = (intentData?.sources ?? []).length;\n }, [intentData, activeMode, sourceSelectionTouched]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n const prev = prevSourceTouchedRef.current;\n const curr = sourceSelectionTouched;\n if (prev === curr) return;\n prevSourceTouchedRef.current = curr;\n if (!prev && curr) {\n trackDeposit(\"deposit_source_selection_changed\", {\n sourceCount: fromTokens.length,\n sourceChainIds: fromTokens.map((t) => t.chainId).filter(Boolean),\n sourceTokenSymbols: fromTokens.map((t) => t.symbol).filter(Boolean),\n previousSourceCount: previousAutoSourceCountRef.current,\n });\n } else if (prev && !curr) {\n trackDeposit(\"deposit_source_selection_reverted_to_auto\", {\n previousSourceCount: fromTokens.length,\n });\n }\n }, [sourceSelectionTouched, activeMode, fromTokens, trackDeposit]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (swapStep !== \"preview-intent\") return;\n if (intentLoading) return;\n if (!intentData) return;\n if (hadPreviewViewedRef.current) return;\n hadPreviewViewedRef.current = true;\n previewViewedTsRef.current = Date.now();\n trackDeposit(\"deposit_preview_viewed\", {\n totalFeeUsd: Number(intentFeeUsd) || 0,\n toAmountUsd: Number(depositUsdDisplay) || 0,\n sourceCount: (intentData?.sources ?? []).length,\n });\n }, [\n swapStep,\n intentLoading,\n intentData,\n activeMode,\n intentFeeUsd,\n depositUsdDisplay,\n trackDeposit,\n ]);\n const requiredDestinationTokenAmount =\n activeMode === \"deposit\"\n ? depositTokenAmountForQuote\n : activeMode === \"send\"\n ? parseFiatNumber(amount)\n : undefined;\n const canRefreshExactOutQuote = () =>\n activeMode === \"deposit\"\n ? Boolean(\n hasPositiveDecimalInput(amount) &&\n toToken &&\n selectedOpportunity &&\n depositTokenAmountForQuote &&\n depositTokenAmountForQuote.gt(0)\n )\n : activeMode === \"send\"\n ? Boolean(hasPositiveDecimalInput(amount) && toToken)\n : false;\n const invalidateExactOutQuoteForRefresh = (options?: {\n sourceTokens?: SwapTokenOption[];\n }) => {\n immediateQuoteAfterSourceEditRef.current = true;\n const receiveIssue = buildReceiveAmountIssue({\n sourceTokens: options?.sourceTokens,\n });\n applyReceiveAmountIssue(receiveIssue);\n const configuredIssue = buildConfiguredAmountIssue();\n if (configuredIssue) {\n clearPreviewForBlockingAmountIssue();\n }\n const sourceBalanceIssue = buildExactOutSourceBalanceIssue(\n options?.sourceTokens\n );\n if (sourceBalanceIssue) {\n clearPreviewForBlockingAmountIssue();\n setSwapQuoteIssue(sourceBalanceIssue);\n }\n const shouldLoadQuote = Boolean(\n !receiveIssue &&\n !configuredIssue &&\n !sourceBalanceIssue &&\n nexusSDK &&\n canRefreshExactOutQuote()\n );\n if (!receiveIssue && !configuredIssue && !sourceBalanceIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n setTxError(null);\n setSwapQuoteIssue(null);\n }\n return shouldLoadQuote;\n };\n\n useEffect(() => {\n if (\n activeMode !== \"swap\" ||\n swapStep !== \"idle\" ||\n swapType !== \"exactIn\"\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n\n const sources = getPredictiveExactInSourceTokens();\n const key = getPredictiveQuoteCacheKey();\n if (!toToken || sources.length === 0 || !key) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n\n const runId = ++predictiveQuoteRunRef.current;\n let cancelled = false;\n\n void (async () => {\n const baseline = predictiveQuoteCacheRef.current[key];\n const cachedDestinationRate = parseFiatNumber(\n baseline?.destinationUsdRate\n );\n const destinationRate =\n cachedDestinationRate && cachedDestinationRate.gt(0)\n ? cachedDestinationRate\n : await resolveUsdRateForToken(toToken);\n\n if (cancelled || runId !== predictiveQuoteRunRef.current) return;\n if (destinationRate.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n\n let sourceUsd = new Decimal(0);\n for (const source of sources) {\n const sourceAmount =\n parseFiatNumber(source.userAmount) ?? new Decimal(0);\n if (sourceAmount.lte(0)) continue;\n\n if (source.userAmountMode === \"usd\") {\n sourceUsd = sourceUsd.plus(sourceAmount);\n continue;\n }\n\n const sourceRate = await resolveUsdRateForToken(source);\n if (cancelled || runId !== predictiveQuoteRunRef.current) return;\n if (sourceRate.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n sourceUsd = sourceUsd.plus(sourceAmount.mul(sourceRate));\n }\n\n if (sourceUsd.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n\n const cachedAmountPerSourceUsd = parseFiatNumber(\n baseline?.exactInDestinationAmountPerSourceUsd\n );\n const predictedDestinationAmount =\n cachedAmountPerSourceUsd && cachedAmountPerSourceUsd.gt(0)\n ? sourceUsd.mul(cachedAmountPerSourceUsd)\n : sourceUsd\n .mul(BASIS_POINTS - PREDICTIVE_EXACT_IN_DISCOUNT_BPS)\n .div(BASIS_POINTS)\n .div(destinationRate);\n const predictedDestinationUsd =\n cachedAmountPerSourceUsd && cachedAmountPerSourceUsd.gt(0)\n ? predictedDestinationAmount.mul(destinationRate)\n : sourceUsd\n .mul(BASIS_POINTS - PREDICTIVE_EXACT_IN_DISCOUNT_BPS)\n .div(BASIS_POINTS);\n\n if (\n cancelled ||\n runId !== predictiveQuoteRunRef.current ||\n predictedDestinationAmount.lte(0)\n ) {\n return;\n }\n\n setPredictiveQuote({\n key,\n mode: \"exactIn\",\n toAmount: getPredictiveDisplayAmount(\n predictedDestinationAmount,\n toToken\n ),\n toUsd: predictedDestinationUsd.toDecimalPlaces(6).toFixed(),\n });\n })();\n\n return () => {\n cancelled = true;\n };\n }, [\n activeMode,\n amount,\n fromTokens,\n swapStep,\n swapType,\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n useEffect(() => {\n if (\n (activeMode !== \"deposit\" && activeMode !== \"send\") ||\n swapStep !== \"idle\" ||\n swapType !== \"exactOut\" ||\n !nexusSDK\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current\n );\n return;\n }\n\n const parsedAmount = parseFiatNumber(amount);\n const key = getPredictiveQuoteCacheKey();\n if (\n !toToken ||\n !parsedAmount ||\n parsedAmount.lte(0) ||\n !key ||\n (activeMode === \"deposit\" && !selectedOpportunity)\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current\n );\n return;\n }\n\n const runId = ++predictiveQuoteRunRef.current;\n let cancelled = false;\n\n void (async () => {\n const baseline = predictiveQuoteCacheRef.current[key];\n const cachedDestinationRate = parseFiatNumber(\n baseline?.destinationUsdRate\n );\n const destinationRate =\n cachedDestinationRate && cachedDestinationRate.gt(0)\n ? cachedDestinationRate\n : await resolveUsdRateForToken(toToken);\n\n if (cancelled || runId !== predictiveQuoteRunRef.current) return;\n if (destinationRate.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current\n );\n return;\n }\n\n const destinationAmount =\n activeMode === \"deposit\" && depositAmountMode === \"usd\"\n ? parsedAmount.div(destinationRate)\n : parsedAmount;\n const destinationUsd =\n activeMode === \"deposit\" && depositAmountMode === \"usd\"\n ? parsedAmount\n : destinationAmount.mul(destinationRate);\n const destinationCoverage = getExactOutDestinationBalanceCoverage({\n requestedAmount: destinationAmount,\n requestedUsd: destinationUsd,\n token: toToken,\n });\n const destinationUsdNeedingSources = Decimal.max(\n destinationUsd.minus(destinationCoverage?.usd ?? new Decimal(0)),\n new Decimal(0)\n );\n const cachedSourceUsdRatio = parseFiatNumber(\n baseline?.exactOutSourceUsdPerDestinationUsd\n );\n const requiredSourceUsd = getPredictiveExactOutSourceTargetUsd(\n destinationUsdNeedingSources,\n cachedSourceUsdRatio\n );\n const sources = requiredSourceUsd.gt(0)\n ? await buildPredictiveExactOutSources(requiredSourceUsd)\n : [];\n\n if (\n cancelled ||\n runId !== predictiveQuoteRunRef.current ||\n (requiredSourceUsd.gt(0) && sources.length === 0)\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current\n );\n return;\n }\n\n setPredictiveQuote({\n key,\n mode: \"exactOut\",\n sources,\n toAmount: getPredictiveDisplayAmount(destinationAmount, toToken),\n toUsd: destinationUsd.toDecimalPlaces(6).toFixed(),\n });\n })();\n\n return () => {\n cancelled = true;\n };\n }, [\n activeMode,\n amount,\n depositAmountMode,\n destinationBalance,\n fromTokensQuoteKey,\n nexusSDK,\n selectedOpportunityIdentity,\n sourceSelectionRevision,\n swapBalance,\n swapStep,\n swapType,\n toToken?.balance,\n toToken?.balanceInFiat,\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n const resolvedDepositSourceTokens = useMemo(() => {\n if (activeMode !== \"deposit\" || !swapBalance) return [];\n const selection = getResolvedDepositSourceSelection();\n return getDepositSourceTokensForIds(selection.selectedSourceIds);\n }, [\n activeMode,\n depositSourceFilter,\n depositQuoteAmountKey,\n depositSourceTargetUsdKey,\n depositUsdDecimal.toFixed(),\n fromTokensQuoteKey,\n selectedOpportunity?.chainId,\n selectedOpportunity?.tokenAddress,\n selectedOpportunity?.tokenSymbol,\n sourceSelectionRevision,\n sourceSelectionTouched,\n swapBalance,\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.symbol,\n ]);\n const lockedDestinationSourceTokens = useMemo(() => {\n if (\n (activeMode !== \"deposit\" && activeMode !== \"send\") ||\n !toToken?.chainId ||\n !requiredDestinationTokenAmount ||\n requiredDestinationTokenAmount.lte(0)\n ) {\n return [];\n }\n\n const heldDestinationToken = getHeldDestinationTokenOption();\n return heldDestinationToken ? [heldDestinationToken] : [];\n }, [\n activeMode,\n requiredDestinationTokenAmount?.toFixed(),\n swapBalance,\n toToken?.chainId,\n toToken?.chainLogo,\n toToken?.chainName,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.logo,\n toToken?.symbol,\n ]);\n\n const setSourcePickerDraftSelection = useCallback(\n (tokens: SwapTokenOption[]) => {\n const nextTokens = tokens.map((token) => ({\n ...token,\n userAmount: \"\",\n }));\n sourcePickerDraftTokensRef.current = nextTokens;\n setSourcePickerDraftTokens(nextTokens);\n },\n []\n );\n\n const getAutoExactOutSourceTokensForPicker = useCallback(() => {\n const autoIntentTokens = lastAutoIntentSourceTokensRef.current;\n if (autoIntentTokens.length > 0) {\n return autoIntentTokens;\n }\n\n return sourceSelectionTouched ? [] : fromTokens;\n }, [fromTokens, sourceSelectionTouched]);\n\n const resetSourcePickerDraft = useCallback(() => {\n sourcePickerDraftTokensRef.current = null;\n setSourcePickerDraftTokens(null);\n }, []);\n\n const beginSourcePickerEdit = useCallback(() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n resetSourcePickerDraft();\n return;\n }\n\n sourcePickerDraftDepositFilterRef.current = depositSourceFilter;\n sourcePickerDraftTouchedRef.current = sourceSelectionTouched;\n sourcePickerDraftModeRef.current = exactOutQuoteSourceModeRef.current;\n setSourcePickerDraftSelection(fromTokens);\n }, [\n activeMode,\n depositSourceFilter,\n fromTokens,\n resetSourcePickerDraft,\n setSourcePickerDraftSelection,\n sourceSelectionTouched,\n ]);\n\n const handleSourcePickerCancel = useCallback(() => {\n resetSourcePickerDraft();\n closeDrawerToIdle();\n }, [closeDrawerToIdle, resetSourcePickerDraft]);\n\n const handleSourcePickerDraftSelectionChange = useCallback(\n (tokens: SwapTokenOption[]) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n\n setSourcePickerDraftSelection(tokens);\n sourcePickerDraftTouchedRef.current = true;\n sourcePickerDraftModeRef.current = \"selected\";\n if (activeMode === \"deposit\") {\n sourcePickerDraftDepositFilterRef.current = \"custom\";\n }\n },\n [activeMode, setSourcePickerDraftSelection]\n );\n\n const handleSourcePickerFilterTabSelect = useCallback(\n (tab: Exclude) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n\n const nextFilter: DepositSourceFilter =\n tab === \"stables\" ? \"stablecoins\" : tab;\n\n if (tab === \"all\") {\n sourcePickerDraftDepositFilterRef.current = nextFilter;\n sourcePickerDraftTouchedRef.current = false;\n sourcePickerDraftModeRef.current = \"all\";\n setSourcePickerDraftSelection(getAutoExactOutSourceTokensForPicker());\n return;\n }\n\n if (activeMode !== \"deposit\") return;\n\n const selection = getResolvedDepositSourceSelection({\n filter: nextFilter,\n isManualSelection: false,\n });\n const sourcePoolTokens = getDepositSourceTokensForIds(\n selection.sourcePoolIds\n );\n\n sourcePickerDraftDepositFilterRef.current = nextFilter;\n sourcePickerDraftTouchedRef.current = false;\n sourcePickerDraftModeRef.current = \"all\";\n setSourcePickerDraftSelection(sourcePoolTokens);\n },\n [\n activeMode,\n getAutoExactOutSourceTokensForPicker,\n getDepositSourceTokensForIds,\n getResolvedDepositSourceSelection,\n setSourcePickerDraftSelection,\n ]\n );\n\n const commitSourcePickerDraft = useCallback(\n (tokens?: SwapTokenOption[]) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n resetSourcePickerDraft();\n closeDrawerToIdle();\n return;\n }\n\n const nextTokens =\n tokens ?? sourcePickerDraftTokensRef.current ?? fromTokens;\n const normalizedTokens = nextTokens.map((token) => ({\n ...token,\n userAmount: \"\",\n }));\n\n setSourceSelectionTouched(sourcePickerDraftTouchedRef.current);\n setExactOutQuoteSourceModeValue(sourcePickerDraftModeRef.current);\n if (activeMode === \"deposit\") {\n setDepositSourceFilter(sourcePickerDraftDepositFilterRef.current);\n }\n invalidateExactOutQuoteForRefresh({\n sourceTokens: normalizedTokens,\n });\n setSourceSelectionRevision((current) => current + 1);\n setFromTokens(normalizedTokens);\n resetSourcePickerDraft();\n closeDrawerToIdle();\n },\n [\n activeMode,\n closeDrawerToIdle,\n fromTokens,\n invalidateExactOutQuoteForRefresh,\n resetSourcePickerDraft,\n setExactOutQuoteSourceModeValue,\n ]\n );\n\n const sourcePickerSelectedTokens =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n swapStep === \"choose-swap-asset\"\n ? (sourcePickerDraftTokens ?? fromTokens)\n : fromTokens;\n\n useEffect(() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n if (lockedDestinationSourceTokens.length === 0) return;\n if (activeMode === \"deposit\" && !sourceSelectionTouched) return;\n\n setFromTokens((current) => {\n const missing = lockedDestinationSourceTokens.filter(\n (locked) =>\n !current.some(\n (token) =>\n getTokenSelectionKey(token) === getTokenSelectionKey(locked)\n )\n );\n if (missing.length === 0) return current;\n return [\n ...current,\n ...missing.map((token) => ({ ...token, userAmount: \"\" })),\n ];\n });\n }, [activeMode, lockedDestinationSourceTokens, sourceSelectionTouched]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (sourceSelectionTouched) return;\n if (\n !toToken ||\n !depositTokenAmountForQuote ||\n depositTokenAmountForQuote.lte(0)\n ) {\n return;\n }\n if (resolvedDepositSourceTokens.length === 0) {\n return;\n }\n\n setFromTokens((current) => {\n const canInitialize = current.length === 0;\n if (!canInitialize) return current;\n\n const next: SwapTokenOption[] = [];\n const seen = new Set();\n for (const token of resolvedDepositSourceTokens) {\n const key = getTokenSelectionKey(token);\n if (!key || seen.has(key)) continue;\n seen.add(key);\n next.push({ ...token, userAmount: \"\" });\n }\n\n const currentKeys = current.map(getTokenSelectionKey).sort().join(\"|\");\n const nextKeys = next.map(getTokenSelectionKey).sort().join(\"|\");\n if (currentKeys === nextKeys) return current;\n return next;\n });\n }, [\n activeMode,\n depositQuoteAmountKey,\n resolvedDepositSourceTokens,\n sourceSelectionTouched,\n toTokenQuoteKey,\n ]);\n\n // ---------------------------------------------------------------------------\n // Handlers\n // ---------------------------------------------------------------------------\n\n const applyConfiguredPrefillsAfterReset = () => {\n const prefillAmount = config.prefill?.amount ?? \"\";\n const prefillRecipient = config.prefill?.recipient ?? \"\";\n\n appliedTokenPrefillRef.current = null;\n syncingIntentSourcesRef.current = false;\n maxPercentRunRef.current += 1;\n setAmount(prefillAmount);\n setRecipientAddress(prefillRecipient);\n setTxError(null);\n setSwapQuoteIssue(null);\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n setPredictiveQuote(null);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setMaxCalculationPercent(null);\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setExactOutQuoteSourceModeValue(\"all\");\n setSourceSelectionRevision((current) => current + 1);\n setDepositAmountMode(\"token\");\n\n if (activeMode === \"swap\") {\n const sourcePrefill = config.prefill?.source;\n const destinationPrefill = config.prefill?.destination;\n const sourceToken = resolvePrefillToken(sourcePrefill);\n const destinationToken = resolvePrefillToken(destinationPrefill);\n\n setFromTokens(\n sourceToken\n ? [\n {\n ...sourceToken,\n userAmount: prefillAmount,\n },\n ]\n : []\n );\n setSourceSelectionTouched(Boolean(sourceToken));\n setToToken(destinationToken);\n setSelectedOpportunity(undefined);\n setSwapType(\"exactIn\");\n\n if (\n (!sourcePrefill || sourceToken) &&\n (!destinationPrefill || destinationToken) &&\n (sourcePrefill || destinationPrefill)\n ) {\n appliedTokenPrefillRef.current = [\n sourcePrefill\n ? `source:${sourcePrefill.chain}:${sourcePrefill.token.toLowerCase()}`\n : \"\",\n destinationPrefill\n ? `destination:${destinationPrefill.chain}:${destinationPrefill.token.toLowerCase()}`\n : \"\",\n config.prefill?.amount ? `amount:${config.prefill.amount}` : \"\",\n ].join(\"|\");\n }\n return;\n }\n\n if (activeMode === \"send\") {\n const sendPrefill =\n config.prefill?.token && config.prefill?.chain\n ? {\n token: config.prefill.token,\n chain: config.prefill.chain,\n }\n : config.prefill?.destination;\n const token = resolvePrefillToken(sendPrefill);\n\n setFromTokens([]);\n setSelectedOpportunity(undefined);\n setToToken(token);\n setSwapType(\"exactOut\");\n\n if (sendPrefill && token) {\n appliedTokenPrefillRef.current = `send:${sendPrefill.chain}:${sendPrefill.token.toLowerCase()}`;\n }\n return;\n }\n\n const destinationPrefill = config.prefill?.destination;\n const configuredPrefillDeposit = destinationPrefill\n ? configuredDepositOptions.find(\n (deposit) =>\n deposit.chainId === destinationPrefill.chain &&\n deposit.tokenAddress.toLowerCase() ===\n destinationPrefill.token.toLowerCase()\n )\n : undefined;\n const nextDeposit = configuredPrefillDeposit ?? configuredDeposit;\n\n setFromTokens([]);\n setSelectedOpportunity(nextDeposit);\n setToToken(nextDeposit ? toTokenFromOpportunity(nextDeposit) : undefined);\n setSwapType(\"exactOut\");\n };\n\n const handleReset = () => {\n clearPendingSwapIntent();\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n amountEnteredLastValueRef.current = config.prefill?.amount ?? \"\";\n applyConfiguredPrefillsAfterReset();\n rotateAttempt();\n };\n\n const resetSourceSelectionAfterFailure = () => {\n lastAutoIntentSourceTokensRef.current = [];\n lastIntentSourceTokensRef.current = [];\n resetSourcePickerDraft();\n resetExactOutSourcesToAuto();\n };\n\n const handleFailureBack = () => {\n clearPendingSwapIntent();\n resetSourceSelectionAfterFailure();\n setTxError(null);\n void refreshSelectedSourceBalances();\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n rotateAttempt();\n };\n\n const clearTerminalInputState = () => {\n setAmount(\"\");\n setRecipientAddress(\"\");\n setTxError(null);\n setSwapQuoteIssue(null);\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n setFromTokens((current) => (current.length === 0 ? current : []));\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setDepositAmountMode(\"token\");\n };\n\n const resetInputsAfterSuccessfulExecution = () => {\n clearTerminalInputState();\n if (activeMode === \"deposit\") {\n setSelectedOpportunity(configuredDeposit);\n setToToken(\n configuredDeposit\n ? toTokenFromOpportunity(configuredDeposit)\n : undefined\n );\n } else {\n setToToken(undefined);\n }\n };\n\n const handleTerminalReceiptDone = () => {\n clearPendingSwapIntent();\n clearTerminalInputState();\n void refreshSelectedSourceBalances();\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n amountEnteredLastValueRef.current = \"\";\n rotateAttempt();\n };\n\n const handleModalOpenChange = useCallback(\n (open: boolean) => {\n if (!open && swapStepRef.current === \"progress\") return;\n if (!isControlledOpen) {\n setInternalOpen(open);\n }\n onOpenChange?.(open);\n if (!open) {\n clearPendingSwapIntent();\n onClose?.();\n }\n },\n [clearPendingSwapIntent, isControlledOpen, onClose, onOpenChange]\n );\n\n const handleClose = () => {\n if (!embed) {\n handleModalOpenChange(false);\n return;\n }\n clearPendingSwapIntent();\n onClose?.();\n };\n\n const handleConnectWallet = async () => {\n if (walletActionPending || nexusLoading) return;\n\n const clickHandler = onConnectClick || onConnectWallet;\n if (clickHandler) {\n setWalletActionPending(true);\n setTxError(null);\n try {\n await clickHandler();\n } catch (error: any) {\n setTxError(error?.message || \"Unable to connect wallet.\");\n } finally {\n setWalletActionPending(false);\n }\n return;\n }\n\n if (isWalletConnectPending) return;\n\n setWalletActionPending(true);\n setTxError(null);\n try {\n let activeConnector = connector;\n\n if (walletStatus !== \"connected\") {\n const nextConnector = connectors[0];\n if (!nextConnector) {\n throw new Error(\"No wallet connector available.\");\n }\n await connectAsync({ connector: nextConnector });\n return;\n }\n\n const effectiveProvider = await getEffectiveWalletProvider(\n activeConnector\n );\n\n if (\n !effectiveProvider ||\n typeof effectiveProvider.request !== \"function\"\n ) {\n throw new Error(\"Wallet provider is not ready yet.\");\n }\n\n await handleInit(effectiveProvider as EthereumProvider, ownerAddress);\n } catch (error: any) {\n setTxError(error?.message || \"Unable to connect wallet.\");\n } finally {\n setWalletActionPending(false);\n }\n };\n\n const handleOpenRecipientEditor = () => {\n if (activeMode === \"swap\" && !recipientAddress && defaultRecipientAddress) {\n setRecipientAddress(defaultRecipientAddress);\n }\n setTxError(null);\n openDrawerStep(\"enter-recipient\");\n };\n\n const handleResetRecipientToDefault = () => {\n setRecipientAddress(defaultRecipientAddress);\n setTxError(null);\n };\n\n const handleSaveRecipient = () => {\n const next = recipientAddress.trim();\n if (!next) {\n setTxError(\"Recipient address is required\");\n return;\n }\n if (!next.endsWith(\".eth\") && !isAddress(next)) {\n setTxError(\"Incorrect address\");\n return;\n }\n if (\n activeMode === \"send\" &&\n ownerAddress &&\n isAddress(next) &&\n next.toLowerCase() === ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\n setRecipientAddress(next);\n setTxError(null);\n closeDrawerToIdle();\n };\n\n /** Start swap flow — v2 SDK per-operation onIntent hooks populate preview. */\n const handleEnterPreview = async (options: { background?: boolean } = {}) => {\n const { background = false } = options;\n const isExactOutFlow = activeMode === \"deposit\" || activeMode === \"send\";\n const quoteInputKey = activeQuoteInputKeyRef.current;\n const isCurrentQuoteInput = () =>\n activeQuoteInputKeyRef.current === quoteInputKey;\n\n if (!toToken) {\n return;\n }\n\n if (isExactOutFlow) {\n if (!hasPositiveDecimalInput(amount)) {\n return;\n }\n } else if (!hasReadyExactInSwapInput(fromTokens, toToken)) {\n if (!background) {\n setTxError(null);\n setSwapQuoteIssue(null);\n }\n return;\n }\n\n const receiveIssue = buildReceiveAmountIssue();\n if (receiveIssue) {\n applyReceiveAmountIssue(receiveIssue);\n if (!background && swapStepRef.current !== \"idle\") {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n }\n return;\n }\n\n const configuredIssue = buildConfiguredAmountIssue();\n if (configuredIssue) {\n clearPreviewForBlockingAmountIssue();\n if (!background && swapStepRef.current !== \"idle\") {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n }\n return;\n }\n\n const sourceBalanceIssue = isExactOutFlow\n ? buildExactOutSourceBalanceIssue()\n : null;\n if (sourceBalanceIssue) {\n clearPreviewForBlockingAmountIssue();\n setSwapQuoteIssue(sourceBalanceIssue);\n if (!background && swapStepRef.current !== \"idle\") {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n }\n return;\n }\n\n if (!background && activeMode === \"deposit\") {\n trackDeposit(\"deposit_confirm_clicked\", {\n amountToken: depositTokenDisplay,\n amountUsd: Number(depositUsdDisplay) || 0,\n selectionMode: sourceSelectionTouched ? \"manual\" : \"auto\",\n sourceCount: (intentData?.sources ?? []).length,\n });\n }\n\n setTxError(null);\n setSwapQuoteIssue(null);\n\n if (\n !background &&\n swapIntentRef.current?.runId === swapRunIdRef.current &&\n swapIntentRef.current?.quoteInputKey === quoteInputKey &&\n intentData &&\n (activeMode !== \"send\" || Boolean(recipientAddress)) &&\n ((activeMode !== \"deposit\" && activeMode !== \"send\") ||\n (intentData.sources ?? []).length > 0 ||\n Boolean(intentData.destination))\n ) {\n swapStepRef.current = \"preview-intent\";\n setSwapStep(\"preview-intent\");\n return;\n }\n\n let resolvedRecipientAddress =\n activeMode === \"swap\" ? effectiveRecipientAddress : recipientAddress;\n\n if (!background && activeMode === \"send\" && !resolvedRecipientAddress) {\n setTxError(\"Recipient address is required\");\n return;\n }\n\n if ((!background && activeMode === \"send\") || hasCustomSwapRecipient) {\n if (!resolvedRecipientAddress) {\n setTxError(\"Recipient address is required\");\n return;\n }\n\n if (\n activeMode === \"send\" &&\n ownerAddress &&\n isAddress(resolvedRecipientAddress) &&\n resolvedRecipientAddress.toLowerCase() === ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\n\n if (resolvedRecipientAddress.endsWith(\".eth\")) {\n try {\n const mainnetClient =\n publicClient?.chain?.id === 1\n ? publicClient\n : createPublicClient({\n chain: mainnet,\n transport: http(),\n });\n const ensAddr = await mainnetClient.getEnsAddress({\n name: normalize(resolvedRecipientAddress),\n });\n if (!ensAddr) {\n setTxError(\"Could not resolve ENS name to an address.\");\n return;\n }\n resolvedRecipientAddress = ensAddr;\n } catch (e: any) {\n setTxError(e.message || \"Failed to resolve ENS name.\");\n return;\n }\n } else {\n if (!isAddress(resolvedRecipientAddress)) {\n setTxError(\"Invalid recipient address.\");\n return;\n }\n }\n\n if (\n activeMode === \"send\" &&\n ownerAddress &&\n isAddress(resolvedRecipientAddress) &&\n resolvedRecipientAddress.toLowerCase() === ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\n }\n\n if (!isCurrentQuoteInput()) {\n return;\n }\n\n if (!background) {\n swapStepRef.current = \"preview-intent\";\n setSwapStep(\"preview-intent\");\n }\n setIntentLoading(true);\n setQuoteRefreshing(background);\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n swapIntentRef.current?.deny();\n swapIntentRef.current = null;\n if (!background) {\n resetProgressEvents();\n swapStepsListRef.current = [];\n resetSteps();\n }\n\n if (!nexusSDK) {\n setTxError(\"SDK not initialized\");\n if (!background) {\n setSwapStep(\"idle\");\n }\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n swapRunIdRef.current += 1;\n const runId = swapRunIdRef.current;\n\n const isActionPlanStep = (step: SwapStepType | BridgeStepType) => {\n const type = getProgressStepType(step);\n return (\n type === \"APPROVAL\" ||\n type === \"TRANSACTION_SENT\" ||\n type === \"TRANSACTION_CONFIRMED\"\n );\n };\n\n const hasSwapPlanSteps = (stepList: Array) =>\n stepList.some((step) => !isActionPlanStep(step));\n\n const handleProgressStepSideEffects = (\n event: any,\n step: SwapStepType | BridgeStepType,\n completed: boolean\n ) => {\n const type = getProgressStepType(step);\n const rawStepType = String(\n event?.stepType ?? (step as any)?.type ?? (step as any)?.typeID ?? \"\"\n ).toLowerCase();\n const rawState = String(event?.state ?? \"\").toLowerCase();\n const explorerUrl = getPlanStepExplorerUrl(event, step);\n const sourceMovementStep = isSourceMovementProgressStep(type, rawStepType);\n const hasSourceMovementTransaction = Boolean(\n explorerUrl || getPlanStepTransactionHash(event, step)\n );\n const intentExplorerUrl = getEventIntentExplorerUrl(\n appConfig.nexusNetwork,\n event,\n step\n );\n\n patchCurrentIntentExplorerUrl(intentExplorerUrl);\n\n if (\n type === \"TRANSACTION_SENT\" ||\n type === \"TRANSACTION_CONFIRMED\" ||\n type === \"SOURCE_SWAP\" ||\n type === \"BRIDGE_DEPOSIT\" ||\n type === \"BRIDGE_INTENT_SUBMISSION\" ||\n type === \"BRIDGE_FILL\" ||\n type === \"DESTINATION_SWAP\" ||\n type === \"SWAP_COMPLETE\" ||\n type === \"SWAP_SKIPPED\"\n ) {\n markSwapExecutionStarted();\n }\n\n if (\n sourceMovementStep &&\n hasSourceMovementTransaction &&\n PLAN_STEP_FUNDS_MAY_HAVE_MOVED_STATES.has(rawState)\n ) {\n fundsMovedRef.current = true;\n }\n\n if (explorerUrl) {\n if (\n rawStepType === \"destination_swap\" ||\n rawStepType === \"execute_transaction\" ||\n type.includes(\"DESTINATION_SWAP\") ||\n type === \"TRANSACTION_SENT\" ||\n type === \"TRANSACTION_CONFIRMED\"\n ) {\n mergeExplorerUrls({ destinationExplorerUrl: explorerUrl });\n } else if (\n sourceMovementStep\n ) {\n mergeExplorerUrls({ sourceExplorerUrl: explorerUrl });\n }\n\n if (\n !intentUrlRef.current &&\n (rawStepType === \"bridge_intent_submission\" ||\n rawStepType === \"request_submission\" ||\n type === \"BRIDGE_INTENT_SUBMISSION\")\n ) {\n patchCurrentIntentExplorerUrl(explorerUrl);\n }\n }\n\n if (completed) {\n onStepComplete(step as SwapStepType);\n }\n };\n\n const handlePlanEvent = (event: any) => {\n if (event.type === \"plan_preview\" || event.type === \"plan_confirmed\") {\n const stepList = Array.isArray(event.plan?.steps)\n ? event.plan.steps.map((step: any) =>\n normalizePlanStep(step, step?.type, undefined, false)\n )\n : [];\n logSwapPlanSteps(event.type, stepList, event.plan?.steps);\n if (stepList.length === 0) return;\n\n if (hasSwapPlanSteps(stepList)) {\n swapStepsListRef.current = stepList as SwapStepType[];\n appendProgressListEvent(\n PROGRESS_EVENT_NAMES.SWAP_PLAN_LIST,\n stepList\n );\n } else {\n appendProgressListEvent(\n PROGRESS_EVENT_NAMES.BRIDGE_PLAN_LIST,\n stepList\n );\n }\n onStepsList(stepList as SwapStepType[]);\n return;\n }\n\n if (event.type !== \"plan_progress\") {\n logSdkSwapEvent(\"unhandled typed event\", event);\n return;\n }\n\n const state = String(event.state ?? \"\").toLowerCase();\n const completed = PLAN_FINAL_STATES.has(state);\n const step = normalizePlanStep(\n event.step,\n event.stepType,\n event.state,\n completed\n );\n const eventName = isActionPlanStep(step)\n ? PROGRESS_EVENT_NAMES.BRIDGE_PLAN_PROGRESS\n : PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS;\n\n logSwapPlanProgress(event, step, eventName, completed);\n appendProgressEvent(eventName, step, completed, event);\n handleProgressStepSideEffects(event, step, completed);\n };\n\n const appendSkippedSwapProgress = () => {\n const step = {\n completed: true,\n type: \"SWAP_SKIPPED\",\n typeID: \"SWAP_SKIPPED\",\n } as SwapStepType;\n enterSkippedSwapProgress();\n appendProgressEvent(PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS, step, true);\n onStepComplete(step);\n };\n\n const handleSwapEvent = (event: any) => {\n if (!event || typeof event !== \"object\") return;\n if (typeof event.type === \"string\") {\n handlePlanEvent(event);\n return;\n }\n logSdkSwapEvent(\"ignored event without string type\", event);\n };\n\n const onEvent = (event: any) => {\n const isCurrentRun = swapRunIdRef.current === runId;\n const isCurrentQuote = isCurrentQuoteInput();\n logSdkSwapEvent(\"onEvent\", event, {\n currentRunId: swapRunIdRef.current,\n isCurrentQuote,\n isCurrentRun,\n quoteInputKey,\n runId,\n });\n if (!isCurrentRun || !isCurrentQuote) {\n logSdkSwapEvent(\"ignored stale onEvent\", event, {\n currentRunId: swapRunIdRef.current,\n isCurrentQuote,\n quoteInputKey,\n runId,\n });\n return;\n }\n patchCurrentIntentExplorerUrl(\n getEventIntentExplorerUrl(appConfig.nexusNetwork, event)\n );\n handleSwapEvent(event);\n };\n\n const buildRecipientTransferExecuteConfig = (transferAmount: bigint) => {\n if (!resolvedRecipientAddress) {\n throw new Error(\"Recipient address is required\");\n }\n\n const isNative =\n !toToken.contractAddress ||\n toToken.contractAddress.toLowerCase() ===\n \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\" ||\n toToken.contractAddress ===\n \"0x0000000000000000000000000000000000000000\";\n\n if (isNative) {\n return {\n to: resolvedRecipientAddress as `0x${string}`,\n value: transferAmount,\n gas: BigInt(100000),\n };\n }\n\n return {\n to: toToken.contractAddress as `0x${string}`,\n data: encodeFunctionData({\n abi: erc20Abi,\n functionName: \"transfer\",\n args: [resolvedRecipientAddress as `0x${string}`, transferAmount],\n }),\n gas: BigInt(100000),\n };\n };\n\n const executeRecipientTransfer = async (transferAmount: bigint) => {\n const result = await nexusSDK.execute(\n {\n toChainId: toToken.chainId!,\n ...buildRecipientTransferExecuteConfig(transferAmount),\n },\n { onEvent }\n );\n const finalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result\n );\n if (finalExplorerUrl) {\n setTransferExplorerUrl(finalExplorerUrl);\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n return finalExplorerUrl;\n };\n\n try {\n if (!isExactOutFlow) {\n const fromPayload: {\n chainId: number;\n tokenAddress: `0x${string}`;\n amountRaw: bigint;\n }[] = [];\n\n const exactInSourceTokens = getReadyExactInSourceTokens(fromTokens);\n\n for (const token of exactInSourceTokens) {\n // Determine the amount to use for this specific token\n let rawAmountStr = token.userAmount;\n if (!rawAmountStr && exactInSourceTokens.length === 1) {\n rawAmountStr = amount; // fallback for single-token case\n }\n\n let cleanAmount = parseFiatNumber(rawAmountStr) ?? new Decimal(0);\n if (cleanAmount.lte(0)) continue;\n\n if (token.userAmountMode === \"usd\") {\n const tokenBalance =\n parseFiatNumber(token.balance) ?? new Decimal(0);\n const fiatBalance =\n parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n const price = tokenBalance.gt(0)\n ? fiatBalance.div(tokenBalance)\n : new Decimal(0);\n if (price.gt(0)) {\n cleanAmount = cleanAmount.div(price);\n } else {\n cleanAmount = new Decimal(0);\n }\n }\n\n if (cleanAmount.lte(0)) continue;\n\n const safeTokenAmountStr = toViemDecimalString(\n cleanAmount,\n token.decimals || 18\n );\n\n fromPayload.push({\n chainId: token.chainId!,\n tokenAddress: token.contractAddress as `0x${string}`,\n amountRaw: parseUnits(safeTokenAmountStr, token.decimals || 18),\n });\n }\n\n if (fromPayload.length === 0) {\n throw new Error(\"No source amount available for swap.\");\n }\n\n resetExplorerUrls();\n const exactInSwapPayload = {\n sources: fromPayload,\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n };\n let intentExplorerUrl: string | null = null;\n let intentId = currentSwapEntry?.intentId;\n let finalExplorerUrl: string | null =\n explorerUrlsRef.current.destinationExplorerUrl ||\n explorerUrlsRef.current.sourceExplorerUrl;\n\n if (hasCustomSwapRecipient && resolvedRecipientAddress) {\n const sdkWithOptionalTransfer = nexusSDK as any;\n\n if (typeof sdkWithOptionalTransfer.swapAndTransfer === \"function\") {\n const swapAndTransferExactInInput = {\n mode: \"exactIn\",\n recipient: resolvedRecipientAddress as `0x${string}`,\n ...exactInSwapPayload,\n };\n logSdkIntentInput(\"swapAndTransfer exactIn\", swapAndTransferExactInInput, {\n activeMode,\n quoteInputKey,\n runId,\n });\n const result = await sdkWithOptionalTransfer.swapAndTransfer(\n swapAndTransferExactInInput,\n { onEvent }\n );\n if (result?.success === false) {\n throw new Error(result?.error || \"Swap and transfer failed\");\n }\n\n const swapResult = getSdkSwapResult(result);\n intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result,\n swapResult\n );\n intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const resultFinalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result,\n swapResult\n );\n finalExplorerUrl = resultFinalExplorerUrl || finalExplorerUrl;\n if (resultFinalExplorerUrl) {\n setTransferExplorerUrl(resultFinalExplorerUrl);\n mergeExplorerUrls({\n destinationExplorerUrl: resultFinalExplorerUrl,\n });\n }\n } else {\n logSdkIntentInput(\"swapWithExactIn\", exactInSwapPayload, {\n activeMode,\n quoteInputKey,\n runId,\n });\n startIntentFetchTiming({\n background,\n operation: \"swapWithExactIn\",\n quoteInputKey,\n runId,\n });\n const result = await nexusSDK.swapWithExactIn(exactInSwapPayload, {\n hooks: {\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n },\n onEvent,\n });\n\n intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result\n );\n intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n\n const latestSwapIntent = (\n swapIntentRef.current as unknown as {\n intent?: SwapIntentData;\n } | null\n )?.intent;\n const transferAmount = latestSwapIntent?.destination?.amount;\n if (!transferAmount) {\n throw new Error(\n \"Unable to determine received amount to transfer.\"\n );\n }\n\n const transferAmountBigInt = parseUnits(\n toViemDecimalString(transferAmount, toToken.decimals || 18),\n toToken.decimals || 18\n );\n finalExplorerUrl =\n (await executeRecipientTransfer(transferAmountBigInt)) ||\n finalExplorerUrl;\n }\n } else {\n // Start exact-in swap — the intent hook will fire and populate preview\n logSdkIntentInput(\"swapWithExactIn\", exactInSwapPayload, {\n activeMode,\n quoteInputKey,\n runId,\n });\n startIntentFetchTiming({\n background,\n operation: \"swapWithExactIn\",\n quoteInputKey,\n runId,\n });\n const result = await nexusSDK.swapWithExactIn(exactInSwapPayload, {\n hooks: {\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n },\n onEvent,\n });\n intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result\n );\n intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const swapResult = getSdkSwapResult(result);\n const resultFinalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result,\n swapResult\n );\n finalExplorerUrl = resultFinalExplorerUrl || finalExplorerUrl;\n if (resultFinalExplorerUrl) {\n setTransferExplorerUrl(resultFinalExplorerUrl);\n mergeExplorerUrls({\n destinationExplorerUrl: resultFinalExplorerUrl,\n });\n }\n }\n\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n const resolvedFinalExplorerUrl =\n finalExplorerUrl ||\n explorerUrlsRef.current.destinationExplorerUrl ||\n explorerUrlsRef.current.sourceExplorerUrl;\n const resolvedIntentExplorerUrl =\n intentExplorerUrl || intentUrlRef.current;\n finishCurrentSwapHistoryEntry(\"fulfilled\", {\n finalExplorerUrl: resolvedFinalExplorerUrl,\n ...(resolvedIntentExplorerUrl\n ? { intentExplorerUrl: resolvedIntentExplorerUrl }\n : {}),\n ...(intentId ? { intentId } : {}),\n });\n resetInputsAfterSuccessfulExecution();\n onComplete?.();\n setSwapStep(\"success\");\n }\n } else {\n const exactOutAmountString =\n activeMode === \"deposit\"\n ? depositTokenAmountForQuote\n ?.toDecimalPlaces(toToken.decimals || 18, Decimal.ROUND_DOWN)\n .toFixed()\n : amount;\n if (!exactOutAmountString || new Decimal(exactOutAmountString).lte(0)) {\n setTxError(\n depositAmountMode === \"usd\"\n ? \"Unable to convert USD amount into the destination token amount.\"\n : \"Enter a valid amount.\"\n );\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n const amountBigInt = parseUnits(\n toViemDecimalString(exactOutAmountString, toToken.decimals || 18),\n toToken.decimals || 18\n );\n\n resetExplorerUrls();\n\n const fromSourcesPayload = buildExactOutSourcesPayload(\n getExactOutSourceTokens()\n );\n\n let executeConfig: any;\n if (activeMode === \"deposit\" && !selectedOpportunity?.executeDeposit) {\n throw new Error(\"Deposit config is missing executeDeposit.\");\n }\n\n if (activeMode === \"deposit\" && selectedOpportunity) {\n const user = (ownerAddress ?? connectedAddress) as `0x${string}`;\n\n const executeParams = selectedOpportunity.executeDeposit(\n selectedOpportunity.tokenSymbol,\n selectedOpportunity.tokenAddress,\n amountBigInt,\n selectedOpportunity.chainId,\n user\n );\n if (!isPositiveGasLimit(executeParams.gas)) {\n throw new Error(\n \"Deposit config executeDeposit must return a positive gas limit.\"\n );\n }\n executeConfig = {\n to: executeParams.to,\n value: executeParams.value,\n data: executeParams.data,\n tokenApproval: executeParams.tokenApproval,\n gas: executeParams.gas,\n };\n } else if (\n (activeMode === \"send\" || hasCustomSwapRecipient) &&\n resolvedRecipientAddress\n ) {\n executeConfig = buildRecipientTransferExecuteConfig(amountBigInt);\n }\n\n if (executeConfig?.tokenApproval) {\n executeConfig = {\n ...executeConfig,\n tokenApproval: {\n toTokenAddress:\n executeConfig.tokenApproval.toTokenAddress ||\n toToken.contractAddress,\n amount: executeConfig.tokenApproval.amount,\n spender: executeConfig.tokenApproval.spender,\n },\n };\n }\n\n if (executeConfig) {\n const sdkWithOptionalTransfer = nexusSDK as any;\n const isTransferExactOut =\n (activeMode === \"send\" || hasCustomSwapRecipient) &&\n typeof sdkWithOptionalTransfer.swapAndTransfer === \"function\";\n const exactOutOperationInput = isTransferExactOut\n ? {\n mode: \"exactOut\",\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmountRaw: amountBigInt,\n recipient: resolvedRecipientAddress as `0x${string}`,\n ...fromSourcesPayload,\n }\n : {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmountRaw: amountBigInt,\n execute: executeConfig,\n ...fromSourcesPayload,\n };\n logSdkIntentInput(\n isTransferExactOut\n ? \"swapAndTransfer exactOut\"\n : \"swapAndExecute exactOut\",\n exactOutOperationInput,\n {\n activeMode,\n quoteInputKey,\n runId,\n }\n );\n startIntentFetchTiming({\n background,\n operation: isTransferExactOut\n ? \"swapAndTransfer exactOut\"\n : \"swapAndExecute exactOut\",\n quoteInputKey,\n runId,\n });\n const result =\n isTransferExactOut\n ? await sdkWithOptionalTransfer.swapAndTransfer(\n exactOutOperationInput,\n {\n onEvent,\n onIntent: (data: any) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n }\n )\n : await nexusSDK.swapAndExecute(\n exactOutOperationInput as any,\n {\n onEvent,\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n }\n );\n\n const swapResult = result?.swapResult ?? result?.result ?? null;\n const swapSkipped = Boolean((result as any)?.swapSkipped);\n if (swapSkipped) {\n appendSkippedSwapProgress();\n }\n if (\n !swapResult &&\n !swapSkipped &&\n activeMode !== \"send\" &&\n !hasCustomSwapRecipient\n ) {\n throw new Error(\"Swap failed\");\n }\n const executeTxHash = getSdkTransactionHash(result);\n const intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result,\n swapResult\n );\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const finalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n executeTxHash,\n result,\n swapResult\n );\n if (finalExplorerUrl) {\n if (activeMode === \"send\" || hasCustomSwapRecipient) {\n setTransferExplorerUrl(finalExplorerUrl);\n }\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n patchCurrentSwapHistoryEntry({\n ...(finalExplorerUrl ? { finalExplorerUrl } : {}),\n ...(intentExplorerUrl ? { intentExplorerUrl } : {}),\n ...(intentId ? { intentId } : {}),\n });\n } else {\n const exactOutSwapInput = {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmountRaw: amountBigInt,\n ...fromSourcesPayload,\n };\n logSdkIntentInput(\"swapWithExactOut\", exactOutSwapInput, {\n activeMode,\n quoteInputKey,\n runId,\n });\n startIntentFetchTiming({\n background,\n operation: \"swapWithExactOut\",\n quoteInputKey,\n runId,\n });\n const result = await nexusSDK.swapWithExactOut(\n exactOutSwapInput,\n {\n hooks: {\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n },\n onEvent,\n }\n );\n const intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result\n );\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const swapResult = getSdkSwapResult(result);\n const finalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result,\n swapResult\n );\n if (finalExplorerUrl) {\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n patchCurrentSwapHistoryEntry({\n ...(finalExplorerUrl ? { finalExplorerUrl } : {}),\n ...(intentExplorerUrl ? { intentExplorerUrl } : {}),\n ...(intentId ? { intentId } : {}),\n });\n }\n\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n finishCurrentSwapHistoryEntry(\"fulfilled\");\n resetInputsAfterSuccessfulExecution();\n onComplete?.();\n if (activeMode === \"deposit\") {\n reachedTerminalRef.current = true;\n const now = Date.now();\n trackDeposit(\"deposit_completed\", {\n postConfirmDurationMs: previewConfirmedTsRef.current\n ? now - previewConfirmedTsRef.current\n : 0,\n totalDurationMs: now - widgetOpenedTsRef.current,\n attemptCount: attemptCountRef.current,\n amountToken: depositTokenDisplay,\n amountUsd: Number(depositUsdDisplay) || 0,\n });\n }\n setSwapStep(\"success\");\n }\n }\n } catch (err: any) {\n const caughtTimeout = isTimeoutLikeError(err);\n if (caughtTimeout) {\n console.warn(\"Timeout in handleEnterPreview:\", err);\n } else {\n console.error(\"Error in handleEnterPreview:\", err);\n }\n if (swapRunIdRef.current !== runId || !isCurrentQuoteInput()) {\n return;\n }\n finishIntentFetchTiming(runId, \"failed\");\n if (activeMode === \"deposit\" && err?.code !== \"USER_DENIED_INTENT\") {\n const hasActiveExecution =\n swapStepRef.current === \"progress\" &&\n Boolean(currentSwapIdRef.current);\n const isInsufficient = isInsufficientSourcesError(err);\n const errMessage =\n (typeof err?.message === \"string\" ? err.message : \"\") ||\n (typeof err === \"string\" ? err : \"\");\n const errName = typeof err?.name === \"string\" ? err.name : \"\";\n const isTimeout = isTimeoutLikeError(err);\n const isUserRejected =\n err?.code === 4001 ||\n err?.code === \"ACTION_REJECTED\" ||\n errName === \"UserRejectedRequestError\" ||\n /user rejected|user denied/i.test(errMessage);\n const failedAtStep:\n | \"simulation\"\n | \"nexus_operation\"\n | \"execute_leg\"\n | \"unknown\" = !hasActiveExecution ? \"simulation\" : \"nexus_operation\";\n const errorCategory: string = isUserRejected\n ? \"user_rejected\"\n : isTimeout\n ? \"timeout\"\n : isInsufficient\n ? \"no_eligible_sources\"\n : !hasActiveExecution\n ? \"quote_failed\"\n : fundsMovedRef.current\n ? \"pending_after_funds_moved\"\n : \"execution_failed\";\n reachedTerminalRef.current = true;\n if (fundsMovedRef.current) {\n trackDeposit(\"deposit_pending_after_funds_moved\", {\n errorCode: err?.code ?? \"UNKNOWN\",\n errorMessage: errMessage || \"Transaction pending.\",\n intentUrl: intentUrlRef.current,\n failedAtStep,\n });\n } else {\n trackDeposit(\"deposit_failed\", {\n errorCode: err?.code ?? \"UNKNOWN\",\n errorCategory,\n errorMessage: errMessage || \"Transaction failed.\",\n failedAtStep,\n });\n }\n }\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n const hasActiveExecution =\n swapStepRef.current === \"progress\" && Boolean(currentSwapIdRef.current);\n const isTimeout = caughtTimeout;\n const showFailedProgressThenReceipt = (\n error: string,\n patch: Partial = {}\n ) => {\n const failedProgressEvent = progressEventsRef.current.at(-1);\n const isTransferExecution =\n activeMode === \"send\" || hasCustomSwapRecipient;\n const fallbackFailedStep =\n activeMode === \"deposit\" || isTransferExecution\n ? ({ type: \"APPROVAL\", typeID: \"AP\" } as BridgeStepType)\n : ({\n type: \"DETERMINING_SWAP\",\n typeID: \"DETERMINING_SWAP\",\n } as unknown as SwapStepType);\n const failedStep = failedProgressEvent?.step ?? fallbackFailedStep;\n const autoRefundAvailable =\n isAutoRefundAvailableProgressEvent(failedProgressEvent);\n setFailedProgressStep(failedStep);\n finishCurrentSwapHistoryEntry(\"failed\", {\n error,\n autoRefundAvailable,\n failureDescription: getFailureDescriptionForProgressStep(\n failedStep,\n autoRefundAvailable\n ),\n failureMessage: getFailureMessageForProgressStep(\n failedStep,\n hasCustomSwapRecipient ? \"send\" : activeMode,\n autoRefundAvailable\n ),\n failedStepType: getProgressStepType(failedStep),\n ...patch,\n });\n window.setTimeout(() => {\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n setSwapStep(\"failed\");\n }\n }, 700);\n };\n const showTimeoutReceipt = (\n message = \"Transaction timed out\",\n patch: Partial = {}\n ) => {\n finishCurrentSwapHistoryEntry(\"timeout\", {\n error: message,\n failureDescription:\n \"This transaction is still pending. Check the intent explorer for the latest status.\",\n failureMessage: TIMEOUT_LABEL,\n ...patch,\n });\n window.setTimeout(() => {\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n setSwapStep(\"failed\");\n }\n }, 700);\n };\n if (err?.code === \"USER_DENIED_INTENT\") {\n if (hasActiveExecution) {\n showFailedProgressThenReceipt(\"Transaction cancelled by user\");\n } else if (!background && swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n return;\n }\n if (isInsufficientSourcesError(err) && !hasActiveExecution) {\n const issue = buildInsufficientSourcesIssue(err);\n if (!background || swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n setTxError(null);\n setSwapQuoteIssue(issue);\n onError?.(issue.message);\n return;\n }\n if (isExactOutFlow && isViemInvalidDecimalError(err) && !hasActiveExecution) {\n const issue = buildExactOutSourceBalanceIssue();\n if (issue) {\n if (!background || swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n setTxError(null);\n setSwapQuoteIssue(issue);\n onError?.(issue.message);\n return;\n }\n }\n const errorMessage =\n err?.message ||\n (typeof err === \"string\"\n ? err\n : \"Transaction failed. Please try again or check console.\");\n if (isTimeout && hasActiveExecution) {\n showTimeoutReceipt(errorMessage);\n setTxError(null);\n return;\n }\n if (hasActiveExecution && fundsMovedRef.current) {\n showTimeoutReceipt(errorMessage, {\n failureDescription:\n \"Funds have already moved and the transaction is still pending. Check the intent explorer for the latest status.\",\n });\n setTxError(null);\n return;\n }\n if (hasActiveExecution) {\n showFailedProgressThenReceipt(errorMessage);\n } else if (!background || swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n setTxError(errorMessage);\n onError?.(errorMessage);\n }\n };\n\n const hasInsufficientSourcesQuoteIssue =\n swapQuoteIssue?.type === \"insufficientSources\";\n const hasReceiveAmountQuoteIssue = Boolean(receiveAmountIssue);\n\n useEffect(() => {\n if (activeMode !== \"swap\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = false;\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n }\n\n if (hasReceiveAmountQuoteIssue) {\n clearPendingSwapIntent(true);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n if (hasInsufficientSourcesQuoteIssue) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n const hasEnoughForQuote = hasReadyExactInSwapInput(fromTokens, toToken);\n\n if (!hasEnoughForQuote) {\n clearPendingSwapIntent();\n setSwapQuoteIssue(null);\n setTxError(null);\n return;\n }\n\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n return;\n }\n\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n let quoteStarted = false;\n const quoteDelay = getQuoteRequestDelay();\n const timer = window.setTimeout(() => {\n quoteStarted = true;\n void handleEnterPreview({ background: true });\n }, quoteDelay);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (!quoteStarted && swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n activeQuoteInputKey,\n amount,\n defaultRecipientAddress,\n fromTokensQuoteKey,\n hasCurrentQuoteIntent,\n hasInsufficientSourcesQuoteIssue,\n hasReceiveAmountQuoteIssue,\n getQuoteRequestDelay,\n nexusSDK,\n recipientAddress,\n swapStep,\n toTokenQuoteKey,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = false;\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n }\n\n if (hasReceiveAmountQuoteIssue) {\n clearPendingSwapIntent(true);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n if (hasInsufficientSourcesQuoteIssue) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n const parsedAmount = parseFiatNumber(amount);\n const hasEnoughForQuote = Boolean(\n parsedAmount?.gt(0) &&\n toToken &&\n selectedOpportunity &&\n depositTokenAmountForQuote\n );\n\n if (!hasEnoughForQuote) {\n clearPendingSwapIntent();\n clearSelectedSources();\n return;\n }\n\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n return;\n }\n\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n let quoteStarted = false;\n const quoteDelay = getQuoteRequestDelay();\n const timer = window.setTimeout(() => {\n quoteStarted = true;\n void handleEnterPreview({ background: true });\n }, quoteDelay);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (!quoteStarted && swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n amount,\n activeQuoteInputKey,\n depositAmountMode,\n depositQuoteAmountKey,\n hasCurrentQuoteIntent,\n hasInsufficientSourcesQuoteIssue,\n hasReceiveAmountQuoteIssue,\n getQuoteRequestDelay,\n nexusSDK,\n sourceSelectionRevision,\n selectedOpportunityIdentity,\n swapStep,\n toTokenQuoteKey,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"send\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = false;\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n }\n\n if (hasReceiveAmountQuoteIssue) {\n clearPendingSwapIntent(true);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n if (hasInsufficientSourcesQuoteIssue) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n const parsedAmount = parseFiatNumber(amount);\n const hasEnoughForQuote = Boolean(parsedAmount?.gt(0) && toToken);\n\n if (!hasEnoughForQuote) {\n clearPendingSwapIntent();\n clearSelectedSources();\n return;\n }\n\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n return;\n }\n\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n let quoteStarted = false;\n const quoteDelay = getQuoteRequestDelay();\n const timer = window.setTimeout(() => {\n quoteStarted = true;\n void handleEnterPreview({ background: true });\n }, quoteDelay);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (!quoteStarted && swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n amount,\n activeQuoteInputKey,\n hasCurrentQuoteIntent,\n hasInsufficientSourcesQuoteIssue,\n hasReceiveAmountQuoteIssue,\n getQuoteRequestDelay,\n nexusSDK,\n sourceSelectionRevision,\n swapStep,\n toTokenQuoteKey,\n ]);\n\n const refreshActiveSwapIntent = useCallback(async () => {\n if (receiveAmountIssueRef.current) return;\n\n const activeIntent = swapIntentRef.current;\n if (\n !activeIntent ||\n intentLoading ||\n quoteRefreshing ||\n receiveMaxCalculating ||\n previewQuoteRefreshing\n ) {\n return;\n }\n\n const runId = activeIntent.runId;\n const quoteInputKey = activeIntent.quoteInputKey;\n if (!quoteInputKey || activeQuoteInputKeyRef.current !== quoteInputKey) {\n return;\n }\n const isPreviewRefresh = swapStepRef.current === \"preview-intent\";\n if (isPreviewRefresh) {\n setPreviewQuoteRefreshing(true);\n } else {\n setQuoteRefreshing(true);\n }\n try {\n const updatedRaw = await activeIntent.refresh();\n const updatedBridgeProvider = normalizeBridgeProvider(\n (updatedRaw as any)?.bridgeProvider ??\n (updatedRaw as any)?.normalizedIntent?.bridgeProvider ??\n (updatedRaw as any)?.swap?.bridgeProvider ??\n activeIntent.intent?.bridgeProvider\n );\n const updated = normalizeRenderableSwapIntentData(\n updatedRaw,\n updatedBridgeProvider\n );\n if (\n !updated ||\n swapRunIdRef.current !== runId ||\n activeQuoteInputKeyRef.current !== quoteInputKey\n ) {\n return;\n }\n\n if (swapIntentRef.current) {\n swapIntentRef.current.intent = updated;\n }\n applySwapIntent(updated);\n } catch (err) {\n console.error(\"Unable to refresh swap intent\", err);\n } finally {\n if (\n swapRunIdRef.current === runId &&\n activeQuoteInputKeyRef.current === quoteInputKey\n ) {\n if (isPreviewRefresh) {\n setPreviewQuoteRefreshing(false);\n } else {\n setQuoteRefreshing(false);\n }\n }\n }\n }, [\n applySwapIntent,\n intentLoading,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n ]);\n\n useEffect(() => {\n const hasRefreshableIntent =\n (activeMode === \"swap\" ||\n activeMode === \"deposit\" ||\n activeMode === \"send\") &&\n Boolean(\n intentData &&\n swapIntentRef.current &&\n swapIntentRef.current.quoteInputKey === activeQuoteInputKey\n ) &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n\n if (!hasRefreshableIntent || receiveAmountIssue) {\n setQuoteRefreshProgress(0);\n setQuoteRefreshSecondsRemaining(0);\n return;\n }\n\n let cancelled = false;\n let timeout: number | undefined;\n\n const scheduleRefresh = () => {\n const quoteAge = Date.now() - lastSwapIntentRefreshAtRef.current;\n const delay = Math.max(0, QUOTE_REFRESH_INTERVAL_MS - quoteAge);\n timeout = window.setTimeout(() => {\n if (receiveAmountIssueRef.current) {\n clearPendingSwapIntent(true);\n setQuoteRefreshProgress(0);\n setQuoteRefreshSecondsRemaining(0);\n return;\n }\n\n if (\n intentLoading ||\n quoteRefreshing ||\n receiveMaxCalculating ||\n previewQuoteRefreshing\n ) {\n if (!cancelled) {\n timeout = window.setTimeout(scheduleRefresh, 1000);\n }\n return;\n }\n\n void refreshActiveSwapIntent().finally(() => {\n if (!cancelled) {\n scheduleRefresh();\n }\n });\n }, delay);\n };\n\n scheduleRefresh();\n\n return () => {\n cancelled = true;\n if (timeout !== undefined) {\n window.clearTimeout(timeout);\n }\n };\n }, [\n activeMode,\n activeQuoteInputKey,\n intentData,\n intentLoading,\n receiveAmountIssue,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n refreshActiveSwapIntent,\n swapStep,\n ]);\n\n useEffect(() => {\n const hasRefreshableIntent =\n (activeMode === \"swap\" ||\n activeMode === \"deposit\" ||\n activeMode === \"send\") &&\n Boolean(\n intentData &&\n swapIntentRef.current &&\n swapIntentRef.current.quoteInputKey === activeQuoteInputKey\n ) &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n\n if (!hasRefreshableIntent || receiveAmountIssue) {\n setQuoteRefreshProgress(0);\n setQuoteRefreshSecondsRemaining(0);\n return;\n }\n\n const updateProgress = () => {\n const quoteAge = Date.now() - lastSwapIntentRefreshAtRef.current;\n const remaining = Math.max(0, QUOTE_REFRESH_INTERVAL_MS - quoteAge);\n setQuoteRefreshProgress(remaining / QUOTE_REFRESH_INTERVAL_MS);\n setQuoteRefreshSecondsRemaining(Math.ceil(remaining / 1000));\n };\n\n updateProgress();\n const interval = window.setInterval(updateProgress, 250);\n\n return () => window.clearInterval(interval);\n }, [\n activeMode,\n activeQuoteInputKey,\n intentData,\n receiveAmountIssue,\n swapStep,\n ]);\n\n /** User accepted swap from the preview — call allow() from the intent hook */\n const handleSwapAccept = () => {\n const activeIntent = swapIntentRef.current;\n if (activeIntent) {\n if (\n activeIntent.quoteInputKey &&\n activeQuoteInputKeyRef.current !== activeIntent.quoteInputKey\n ) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n setSwapStep(\"idle\");\n return;\n }\n if (activeMode === \"deposit\") {\n previewConfirmedTsRef.current = Date.now();\n attemptCountRef.current += 1;\n const timeInPreviewMs = previewViewedTsRef.current\n ? previewConfirmedTsRef.current - previewViewedTsRef.current\n : 0;\n trackDeposit(\"deposit_preview_confirmed\", {\n timeInPreviewMs,\n totalFeeUsd: Number(intentFeeUsd) || 0,\n sourceCount: (intentData?.sources ?? []).length,\n });\n }\n onStart?.();\n startSwapHistoryEntry();\n setSwapStep(\"progress\");\n setQuoteRefreshing(false);\n resetProgressEvents();\n if (swapStepsListRef.current.length > 0) {\n seed(swapStepsListRef.current);\n } else {\n resetSteps();\n }\n activeIntent.allow();\n }\n };\n\n // ---------------------------------------------------------------------------\n // Header title\n // ---------------------------------------------------------------------------\n const getTitle = () => {\n const configuredWidgetHeading = normalizeConfiguredString(\n appearanceConfig?.widgetHeading\n );\n if (swapStep === \"history\") return \"Transaction History\";\n // Drawer panels overlay the main page,\n // so the header should still show the main page title.\n\n if (swapStep === \"preview-intent\") {\n return activeMode === \"deposit\"\n ? \"Confirm Deposit\"\n : activeMode === \"send\"\n ? \"Confirm Send\"\n : \"Confirm Swap\";\n }\n\n if (activeMode === \"swap\") {\n if (swapStep === \"progress\") return \"Swapping…\";\n if (swapStep === \"success\") return \"Swap Complete\";\n if (swapStep === \"failed\" && currentSwapEntry?.status === \"timeout\") {\n return TIMEOUT_LABEL;\n }\n if (swapStep === \"failed\") return \"Swap Failed\";\n return configuredWidgetHeading ?? \"Swap and Bridge\";\n }\n if (activeMode === \"deposit\") {\n if (swapStep === \"progress\") return \"Depositing…\";\n if (swapStep === \"success\") return \"Deposit Complete\";\n if (swapStep === \"failed\" && currentSwapEntry?.status === \"timeout\") {\n return TIMEOUT_LABEL;\n }\n if (swapStep === \"failed\") return \"Deposit Failed\";\n return configuredWidgetHeading ?? \"Deposit\";\n }\n if (activeMode === \"send\") {\n if (swapStep === \"progress\") return \"Sending…\";\n if (swapStep === \"success\") return \"Send Complete\";\n if (swapStep === \"failed\" && currentSwapEntry?.status === \"timeout\") {\n return TIMEOUT_LABEL;\n }\n if (swapStep === \"failed\") return \"Send Failed\";\n return configuredWidgetHeading ?? \"Send\";\n }\n return configuredWidgetHeading ?? \"Nexus Widget\";\n };\n\n // Titles that should be center-aligned (main screens / confirm screens)\n // Left-aligned: choose-swap-asset, choose-receive-asset (sub-screens with subtitles)\n const isTitleCentered = () => {\n if (swapStep === \"history\") return false;\n return true; // idle, drawer panels, preview-intent, progress, etc.\n };\n\n const canGoBack = swapStep === \"preview-intent\" || swapStep === \"history\";\n const handleBack = () => {\n if (swapStep === \"history\") {\n setSwapStep(\"idle\");\n return;\n }\n if (swapStep === \"choose-swap-asset\") {\n handleSourcePickerCancel();\n return;\n }\n if (swapStep === \"choose-receive-asset\") {\n closeDrawerToIdle();\n return;\n }\n if (swapStep === \"enter-recipient\") {\n closeDrawerToIdle();\n return;\n }\n if (swapStep === \"preview-intent\") {\n const canRequoteAfterPreviewBack =\n activeMode === \"swap\"\n ? hasReadyExactInSwapInput(fromTokens, toToken)\n : canRefreshExactOutQuote();\n\n if (\n canRequoteAfterPreviewBack &&\n (activeMode === \"deposit\" || activeMode === \"send\")\n ) {\n setExactOutQuoteSourceModeValue(\"all\");\n }\n if (activeMode === \"deposit\" || activeMode === \"send\") {\n invalidateExactOutQuoteForRefresh();\n } else {\n clearPendingSwapIntent(true, {\n keepQuoteRefreshing: canRequoteAfterPreviewBack,\n });\n }\n if (canRequoteAfterPreviewBack && activeMode === \"swap\") {\n setQuoteRefreshing(true);\n setTxError(null);\n setSwapQuoteIssue(null);\n }\n setSwapStep(\"idle\");\n return;\n }\n if (swapStep === \"progress\") {\n return;\n } // can't go back during tx\n setSwapStep(\"idle\");\n };\n\n const handleSwapAmountChange = (val: string, panel: \"send\" | \"receive\") => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n setSwapQuoteIssue(null);\n setTxError(null);\n const nextAmount = parseFiatNumber(val);\n const receiveIssue = buildReceiveAmountIssue({ inputAmount: val });\n applyReceiveAmountIssue(receiveIssue);\n const hasSelectedSourceToken = fromTokens.some(\n (token) => token.chainId && token.contractAddress\n );\n const shouldLoadQuote = Boolean(\n !receiveIssue &&\n nexusSDK &&\n nextAmount?.gt(0) &&\n toToken &&\n hasSelectedSourceToken\n );\n if (!receiveIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n }\n setAmount(val);\n if (panel === \"receive\") {\n setFromTokens((prev) =>\n prev.map((token) => ({ ...token, userAmount: \"\" }))\n );\n }\n // Nexus Widget swaps are exact-in only. Exact-out is reserved for Deposit and Send.\n if (swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n };\n\n const handleSwapTokensUpdate = (tokens: SwapTokenOption[]) => {\n setSwapQuoteIssue(null);\n setTxError(null);\n applyReceiveAmountIssue(buildReceiveAmountIssue({ sourceTokens: tokens }));\n setFromTokens(tokens);\n };\n\n const handleDepositAmountChange = (val: string) => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n resetExactOutSourcesToAuto();\n maxPercentRunRef.current += 1;\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setSwapQuoteIssue(null);\n const nextAmount = parseFiatNumber(val);\n const receiveIssue = buildReceiveAmountIssue({ inputAmount: val });\n applyReceiveAmountIssue(receiveIssue);\n const shouldLoadQuote = Boolean(\n !receiveIssue &&\n nexusSDK &&\n nextAmount?.gt(0) &&\n toToken &&\n selectedOpportunity\n );\n if (!receiveIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n } else {\n clearSelectedSources();\n }\n setAmount(val);\n };\n\n const handleSendAmountChange = (val: string) => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n resetExactOutSourcesToAuto();\n maxPercentRunRef.current += 1;\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setSwapQuoteIssue(null);\n setSwapType(\"exactOut\");\n const nextAmount = parseFiatNumber(val);\n const receiveIssue = buildReceiveAmountIssue({\n inputAmount: val,\n type: \"exactOut\",\n });\n applyReceiveAmountIssue(receiveIssue);\n const shouldLoadQuote = Boolean(\n !receiveIssue && nexusSDK && nextAmount?.gt(0) && toToken\n );\n if (!receiveIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n } else {\n clearSelectedSources();\n }\n setAmount(val);\n };\n\n const handleDepositAmountModeToggle = () => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n resetExactOutSourcesToAuto();\n const rate = getDepositTokenUsdRate();\n const parsedAmount = parseFiatNumber(amount) ?? new Decimal(0);\n if (parsedAmount.gt(0) && rate.gt(0)) {\n const converted =\n depositAmountMode === \"token\"\n ? parsedAmount.mul(rate).toDecimalPlaces(2)\n : parsedAmount\n .div(rate)\n .toDecimalPlaces(\n getCappedTokenDisplayDecimals(toToken?.decimals),\n Decimal.ROUND_DOWN\n );\n setAmount(converted.toFixed());\n }\n clearPendingSwapIntent();\n setDepositAmountMode((current) => (current === \"token\" ? \"usd\" : \"token\"));\n };\n\n const handleDepositPercentSelect = async (pct: number) => {\n if (isConfiguredAmountFixed) return;\n if (!toToken) return;\n\n syncingIntentSourcesRef.current = false;\n setTxError(null);\n setSwapQuoteIssue(null);\n const runId = ++maxPercentRunRef.current;\n lastInputMethodRef.current =\n pct === 20\n ? \"percent_20\"\n : pct === 25\n ? \"percent_25\"\n : pct === 50\n ? \"percent_50\"\n : pct === 75\n ? \"percent_75\"\n : \"percent_max\";\n\n if (pct !== 100) {\n const nextAmount = getExactOutPercentAmountFromBalance(\n toToken,\n pct,\n depositAmountMode === \"usd\"\n );\n\n if (nextAmount) {\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setDepositAmountMode(nextAmount.mode);\n handleDepositAmountChange(nextAmount.amount);\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setTxError(\"Unable to calculate this percentage for the deposit asset.\");\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n const maxAmount = getExactOutPercentAmountFromBalance(\n toToken,\n 100,\n depositAmountMode === \"usd\"\n );\n if (runId !== maxPercentRunRef.current) return;\n if (!maxAmount) {\n setTxError(\"No depositable amount is available for this deposit.\");\n return;\n }\n\n setDepositAmountMode(maxAmount.mode);\n handleDepositAmountChange(maxAmount.amount);\n };\n\n const handleSendPercentSelect = async (pct: number) => {\n if (isConfiguredAmountFixed) return;\n if (!toToken) return;\n\n syncingIntentSourcesRef.current = false;\n setTxError(null);\n setSwapQuoteIssue(null);\n const runId = ++maxPercentRunRef.current;\n lastInputMethodRef.current =\n pct === 20\n ? \"percent_20\"\n : pct === 25\n ? \"percent_25\"\n : pct === 50\n ? \"percent_50\"\n : pct === 75\n ? \"percent_75\"\n : \"percent_max\";\n\n if (pct !== 100) {\n const nextAmount = getExactOutPercentAmountFromBalance(\n toToken,\n pct,\n false\n );\n\n if (nextAmount) {\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleSendAmountChange(nextAmount.amount);\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setTxError(\"Unable to calculate this percentage for the send asset.\");\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n const maxAmount = getExactOutPercentAmountFromBalance(toToken, 100, false);\n if (runId !== maxPercentRunRef.current) return;\n if (!maxAmount) {\n setTxError(\"No transferable amount is available for this asset.\");\n return;\n }\n\n handleSendAmountChange(maxAmount.amount);\n };\n\n // ---------------------------------------------------------------------------\n // Render\n // ---------------------------------------------------------------------------\n const insufficientSourceIssue =\n swapQuoteIssue?.type === \"insufficientSources\" ? swapQuoteIssue : null;\n const configuredAmountIssue = buildConfiguredAmountIssue();\n const blockingQuoteIssue =\n insufficientSourceIssue ?? receiveAmountIssue ?? configuredAmountIssue;\n const isExactOutPaymentFlow =\n activeMode === \"deposit\" || activeMode === \"send\";\n const hasCurrentRunnableIntent = hasCurrentQuoteIntent;\n const hasIntentSources = Boolean((intentData?.sources ?? []).length > 0);\n const hasCurrentExactOutPaymentIntent =\n hasCurrentRunnableIntent &&\n (hasIntentSources ||\n (isExactOutPaymentFlow && Boolean(intentData?.destination)));\n const isExactOutRouteLoading =\n isExactOutPaymentFlow &&\n swapStep === \"idle\" &&\n swapType === \"exactOut\" &&\n Boolean(\n toToken && (receiveMaxCalculating || (amount && Number(amount) > 0))\n ) &&\n !blockingQuoteIssue &&\n !hasCurrentExactOutPaymentIntent &&\n (quoteRefreshing || intentLoading || receiveMaxCalculating);\n const isQuoteUnavailableForAutoSourceFlow =\n isExactOutPaymentFlow &&\n Boolean(hasPositiveDecimalInput(amount) && toToken) &&\n !quoteRefreshing &&\n !receiveMaxCalculating &&\n !intentLoading &&\n !blockingQuoteIssue &&\n !hasCurrentExactOutPaymentIntent;\n const hasPositiveRootAmount = hasPositiveDecimalInput(amount);\n const hasReadySwapQuoteInput = hasReadyExactInSwapInput(fromTokens, toToken);\n const needsWalletConnection = !ownerAddress || !nexusSDK;\n const isExactOutPaymentQuotePending =\n isExactOutPaymentFlow && (quoteRefreshing || intentLoading);\n const walletConnectBusy =\n walletActionPending ||\n nexusLoading ||\n isWalletConnectPending ||\n walletStatus === \"connecting\";\n const hasConnectWalletHandler = Boolean(\n onConnectClick || onConnectWallet || connectors.length > 0\n );\n const walletCtaLabel = hasConnectWalletHandler\n ? walletConnectBusy\n ? \"Connecting...\"\n : \"Connect Wallet\"\n : \"Connect your wallet to proceed\";\n const isSwapCtaDisabled = needsWalletConnection\n ? !hasConnectWalletHandler || walletConnectBusy\n : !hasReadySwapQuoteInput ||\n receiveMaxCalculating ||\n quoteRefreshing ||\n Boolean(blockingQuoteIssue);\n const isDepositCtaDisabled = needsWalletConnection\n ? !hasConnectWalletHandler || walletConnectBusy\n : !hasPositiveRootAmount ||\n !toToken ||\n receiveMaxCalculating ||\n isExactOutPaymentQuotePending ||\n (!hasCurrentExactOutPaymentIntent &&\n isQuoteUnavailableForAutoSourceFlow) ||\n Boolean(blockingQuoteIssue);\n const sendNeedsRecipient = activeMode === \"send\" && !recipientAddress;\n const isSendCtaDisabled = needsWalletConnection\n ? !hasConnectWalletHandler || walletConnectBusy\n : !hasPositiveRootAmount ||\n !toToken ||\n hasSameOwnerSendRecipient ||\n receiveMaxCalculating ||\n (!sendNeedsRecipient &&\n (isExactOutPaymentQuotePending ||\n (!hasCurrentExactOutPaymentIntent &&\n isQuoteUnavailableForAutoSourceFlow))) ||\n Boolean(blockingQuoteIssue);\n const quoteCtaLabel = (fallback: string) => {\n if (needsWalletConnection) return walletCtaLabel;\n if (insufficientSourceIssue) return \"Insufficient balance\";\n if (receiveAmountIssue) return receiveAmountIssue.ctaLabel;\n if (configuredAmountIssue) return configuredAmountIssue.ctaLabel;\n if (receiveMaxCalculating) return \"Calculating...\";\n if (\n isExactOutPaymentQuotePending ||\n (!hasCurrentExactOutPaymentIntent && (quoteRefreshing || intentLoading))\n ) {\n return \"Fetching quotes...\";\n }\n if (isQuoteUnavailableForAutoSourceFlow) return \"Quote unavailable\";\n if (!hasPositiveRootAmount) return \"Enter amount\";\n return fallback;\n };\n const sendCtaLabel = (() => {\n if (needsWalletConnection) return walletCtaLabel;\n if (insufficientSourceIssue) return \"Insufficient balance\";\n if (receiveAmountIssue) return receiveAmountIssue.ctaLabel;\n if (configuredAmountIssue) return configuredAmountIssue.ctaLabel;\n if (!hasPositiveRootAmount) return \"Enter amount\";\n if (!toToken) return \"Select token\";\n if (hasSameOwnerSendRecipient) return \"Change recipient\";\n if (sendNeedsRecipient) return \"Add recipient\";\n return quoteCtaLabel(\"Review send\");\n })();\n const previewIntentSourceUsdNumber = (intentData?.sources ?? []).reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0)\n );\n const previewSourceUsdNumber = previewIntentSourceUsdNumber.gt(0)\n ? previewIntentSourceUsdNumber\n : fromTokens.length > 0\n ? fromTokens.reduce(\n (sum, token) =>\n sum.plus(\n getTokenUsdValue(\n token,\n swapType === \"exactIn\" && fromTokens.length === 1\n ? amount\n : undefined\n )\n ),\n new Decimal(0)\n )\n : undefined;\n const previewExactOutDestinationAmount =\n activeMode === \"deposit\"\n ? depositTokenAmountForQuote\n : activeMode === \"send\"\n ? parseFiatNumber(amount)\n : undefined;\n const previewExactOutDestinationUsdNumber =\n activeMode === \"deposit\"\n ? depositUsdDecimal\n : activeMode === \"send\" && amount && toToken\n ? getTokenUsdValue(\n {\n ...toToken,\n userAmount: amount,\n userAmountMode: \"token\",\n },\n amount\n )\n : undefined;\n const previewDestinationUsdNumber =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n previewExactOutDestinationUsdNumber?.gt(0)\n ? previewExactOutDestinationUsdNumber\n : parseFiatNumber((intentData?.destination as any)?.value);\n const previewDestinationAmount =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n previewExactOutDestinationAmount?.gt(0)\n ? previewExactOutDestinationAmount\n .toDecimalPlaces(toToken?.decimals ?? 18, Decimal.ROUND_DOWN)\n .toFixed()\n : intentToAmount;\n const previewFromAmountUsd =\n previewSourceUsdNumber && previewSourceUsdNumber.gt(0)\n ? previewSourceUsdNumber.toDecimalPlaces(6).toFixed()\n : undefined;\n const previewToAmountUsd =\n previewDestinationUsdNumber && previewDestinationUsdNumber.gt(0)\n ? previewDestinationUsdNumber.toDecimalPlaces(6).toFixed()\n : undefined;\n const previewDestinationGasFeeUsd = (() => {\n const value = getIntentDestinationGasUsdValue(intentData);\n return value && value.gt(0)\n ? value.toDecimalPlaces(6, Decimal.ROUND_DOWN).toFixed()\n : undefined;\n })();\n const predictiveExactInQuote =\n predictiveQuote?.mode === \"exactIn\" &&\n predictiveQuote.key === getPredictiveQuoteCacheKey(\"swap\", \"exactIn\")\n ? predictiveQuote\n : null;\n const predictiveExactOutQuote =\n predictiveQuote?.mode === \"exactOut\" &&\n predictiveQuote.key === getPredictiveQuoteCacheKey(activeMode, \"exactOut\")\n ? predictiveQuote\n : null;\n const shouldUseCurrentExactOutIntentSources =\n isExactOutPaymentFlow && hasCurrentQuoteIntent && hasIntentSources;\n const currentExactOutIntentSourceTokens =\n shouldUseCurrentExactOutIntentSources\n ? sortSwapTokensByUsdDesc(\n (intentData?.sources ?? [])\n .map(buildIntentSourceToken)\n .filter(hasPositiveSourceSpend)\n )\n : [];\n const resolvedToToken =\n toToken ??\n (activeMode === \"deposit\" && selectedOpportunity\n ? toTokenFromOpportunity(selectedOpportunity)\n : undefined);\n const enrichedResolvedToToken = enrichTokenOptionLogo(resolvedToToken);\n const toTokenWithFetchedBalance =\n enrichedResolvedToToken && destinationBalance\n ? { ...enrichedResolvedToToken, balance: destinationBalance }\n : enrichedResolvedToToken;\n const idleReceiveQuoteAmount =\n activeMode === \"swap\" && swapType === \"exactIn\"\n ? (intentToAmount ?? predictiveExactInQuote?.toAmount)\n : undefined;\n const idleReceiveQuoteUsd =\n activeMode === \"swap\" && swapType === \"exactIn\"\n ? (previewToAmountUsd ?? predictiveExactInQuote?.toUsd)\n : previewToAmountUsd;\n const exactOutDestinationCoverage = getExactOutDestinationBalanceCoverage({\n requestedAmount: previewExactOutDestinationAmount,\n requestedUsd: previewExactOutDestinationUsdNumber,\n producedAmount: hasIntentSources\n ? parseFiatNumber(intentData?.destination?.amount)\n : undefined,\n producedUsd: hasIntentSources\n ? parseFiatNumber(intentData?.destination?.value)\n : undefined,\n token: toTokenWithFetchedBalance,\n });\n const destinationBalanceDisplayToken = buildDestinationBalanceDisplayToken(\n exactOutDestinationCoverage,\n toTokenWithFetchedBalance\n );\n const shouldShowPredictiveExactOutDisplay =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (quoteRefreshing || intentLoading) &&\n !hasIntentSources &&\n Boolean(\n predictiveExactOutQuote &&\n ((predictiveExactOutQuote.sources?.length ?? 0) > 0 ||\n destinationBalanceDisplayToken)\n );\n const baseDisplayFromTokens = shouldUseCurrentExactOutIntentSources\n ? currentExactOutIntentSourceTokens\n : shouldShowPredictiveExactOutDisplay\n ? (predictiveExactOutQuote?.sources ?? fromTokens)\n : fromTokens;\n const displayFromTokens = (() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n return baseDisplayFromTokens;\n }\n if (!destinationBalanceDisplayToken) {\n return mergeDisplaySourceTokens(\n sortDisplaySourcesByBalanceUsdDesc(baseDisplayFromTokens)\n );\n }\n\n const destinationKey = getTokenSelectionKey(destinationBalanceDisplayToken);\n let replacedEmptyDestinationToken = false;\n const tokens = baseDisplayFromTokens.map((token) => {\n const isDestinationToken = getTokenSelectionKey(token) === destinationKey;\n if (\n isDestinationToken &&\n !hasPositiveDecimalInput(token.userAmount) &&\n !hasPositiveDecimalInput(token.userAmountUsd)\n ) {\n replacedEmptyDestinationToken = true;\n return destinationBalanceDisplayToken;\n }\n return token;\n });\n\n const displayTokens = replacedEmptyDestinationToken\n ? tokens\n : [...tokens, destinationBalanceDisplayToken];\n return mergeDisplaySourceTokens(\n sortDisplaySourcesByBalanceUsdDesc(displayTokens)\n );\n })();\n const displayExactOutRouteLoading =\n isExactOutRouteLoading && !shouldShowPredictiveExactOutDisplay;\n const isSourcePickerRefreshDisabled =\n quoteRefreshing || intentLoading || previewQuoteRefreshing;\n const isIntentSourceUiActionable =\n activeMode === \"deposit\"\n ? !isDepositCtaDisabled\n : activeMode === \"send\"\n ? !isSendCtaDisabled\n : !isSwapCtaDisabled;\n\n useEffect(() => {\n if (!intentFetchTiming?.endTimeMs) {\n return;\n }\n if (latestIntentFetchRunIdRef.current !== intentFetchTiming.runId) {\n return;\n }\n if (intentUiRenderLoggedRunIdRef.current === intentFetchTiming.runId) {\n return;\n }\n const hasRenderedSources =\n displayFromTokens.length > 0 ||\n hasIntentSources ||\n Boolean(intentData?.destination);\n if (!hasRenderedSources) return;\n if (\n isSwapBalancePending ||\n isSourcePickerRefreshDisabled ||\n quoteRefreshing ||\n intentLoading ||\n previewQuoteRefreshing ||\n receiveMaxCalculating ||\n displayExactOutRouteLoading ||\n !isIntentSourceUiActionable\n ) {\n return;\n }\n\n const renderTimeMs = Date.now();\n const renderDurationMs = renderTimeMs - intentFetchTiming.startTimeMs;\n const fetchDurationMs =\n intentFetchTiming.fetchDurationMs ??\n intentFetchTiming.endTimeMs - intentFetchTiming.startTimeMs;\n const differenceMs = renderDurationMs - fetchDurationMs;\n\n console.log(\"[NexusWidget] intent sources UI render complete\", {\n runId: intentFetchTiming.runId,\n walletAddress: intentFetchTiming.walletAddress,\n operation: intentFetchTiming.operation,\n quoteInputKey: intentFetchTiming.quoteInputKey,\n sourceCount: displayFromTokens.length,\n dateNow: renderTimeMs,\n date: new Date(renderTimeMs),\n totalRenderTimeSeconds: renderDurationMs / 1000,\n elapsedMs: renderDurationMs,\n });\n console.log(\"[NexusWidget] intent render/fetch difference\", {\n runId: intentFetchTiming.runId,\n walletAddress: intentFetchTiming.walletAddress,\n operation: intentFetchTiming.operation,\n quoteInputKey: intentFetchTiming.quoteInputKey,\n differenceMs,\n dateNow: renderTimeMs,\n date: new Date(renderTimeMs),\n });\n intentUiRenderLoggedRunIdRef.current = intentFetchTiming.runId;\n }, [\n displayExactOutRouteLoading,\n displayFromTokens.length,\n hasIntentSources,\n intentLoading,\n intentData?.destination,\n intentFetchTiming,\n isDepositCtaDisabled,\n isIntentSourceUiActionable,\n isSendCtaDisabled,\n isSourcePickerRefreshDisabled,\n isSwapCtaDisabled,\n isSwapBalancePending,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n ]);\n\n const totalSwapBalanceUsd = getActiveTotalBalanceUsd()\n .toDecimalPlaces(2)\n .toFixed();\n const requestedOutputUsd =\n amount && toToken\n ? getTokenUsdValue(\n {\n ...toToken,\n userAmount: amount,\n userAmountMode: \"token\",\n },\n amount\n )\n : new Decimal(0);\n const sendAmountUsd = requestedOutputUsd.toNumber();\n const exactOutRequiredUsdAmount = (() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return undefined;\n\n const issueMissingUsd = parseFiatNumber(insufficientSourceIssue?.missingUsd);\n if (issueMissingUsd && issueMissingUsd.gt(0)) {\n return getExactOutAvailableSourceUsd().plus(issueMissingUsd);\n }\n\n const requiredFundingUsd = getExactOutRequiredFundingUsd();\n if (requiredFundingUsd?.gt(0)) return requiredFundingUsd;\n\n if (requestedOutputUsd.gt(0)) return requestedOutputUsd;\n\n return undefined;\n })();\n const exactOutRequiredUsdDisplay = exactOutRequiredUsdAmount\n ?.toDecimalPlaces(2)\n .toFixed();\n const assetSelectorRequiredUsdDisplay =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (swapStep === \"choose-swap-asset\" ||\n closingDrawerStep === \"choose-swap-asset\")\n ? (sourceSelectionRequiredUsdDisplay ?? exactOutRequiredUsdDisplay)\n : exactOutRequiredUsdDisplay;\n const captureSourceSelectionRequiredUsd = () => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n setSourceSelectionRequiredUsdDisplay(exactOutRequiredUsdDisplay);\n };\n const isIdleSwapQuoteLoading =\n activeMode === \"swap\" &&\n swapStep === \"idle\" &&\n (quoteRefreshing || intentLoading);\n const isReceiveAmountLoading =\n receiveMaxCalculating ||\n (isIdleSwapQuoteLoading &&\n swapType === \"exactIn\" &&\n !idleReceiveQuoteAmount);\n const isReceiveUsdLoading =\n receiveMaxCalculating ||\n (isIdleSwapQuoteLoading && swapType === \"exactIn\" && !idleReceiveQuoteUsd);\n const hasQuoteRefreshCountdown =\n (activeMode === \"swap\" ||\n activeMode === \"deposit\" ||\n activeMode === \"send\") &&\n hasCurrentQuoteIntent &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n const isRecipientDrawerClosing = closingDrawerStep === \"enter-recipient\";\n const isSwapAssetDrawerClosing = closingDrawerStep === \"choose-swap-asset\";\n const isReceiveAssetDrawerClosing =\n closingDrawerStep === \"choose-receive-asset\";\n const isReceiveAssetPickerLocked =\n activeMode === \"deposit\"\n ? configuredDepositOptions.length <= 1\n : (config.allowedDestinationPairs?.length ?? 0) === 1;\n const hideDestinationTokenDropdownIcon =\n activeMode === \"deposit\"\n ? configuredDepositOptions.length === 1\n : (config.allowedDestinationPairs?.length ?? 0) === 1;\n const isTokenAssetDrawerActive =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n closingDrawerStep === \"choose-swap-asset\" ||\n closingDrawerStep === \"choose-receive-asset\";\n const configuredReceiveTokenCount =\n activeMode === \"deposit\" || activeMode === \"send\"\n ? Math.max(\n configuredDestinationTokenOptions.length,\n config.allowedDestinationPairs?.length ?? 0\n )\n : 0;\n const isConfiguredReceiveAssetDrawerActive =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (swapStep === \"choose-receive-asset\" ||\n closingDrawerStep === \"choose-receive-asset\") &&\n Boolean(config.allowedDestinationPairs?.length);\n const tokenAssetDrawerMinRootContentHeight =\n isConfiguredReceiveAssetDrawerActive\n ? getConfiguredReceiveSelectorRootHeight(configuredReceiveTokenCount)\n : isTokenAssetDrawerActive\n ? TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT\n : 0;\n const isDrawerOverlayActive =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n swapStep === \"enter-recipient\" ||\n closingDrawerStep !== null;\n const isPreviewIntentScreen = swapStep === \"preview-intent\";\n const displayedRootContentHeight =\n hasMeasuredRootContent && rootContentHeight\n ? Math.max(\n rootContentHeight,\n tokenAssetDrawerMinRootContentHeight\n )\n : null;\n\n const widgetContent = (\n {\n const transitions = [];\n if (hasMeasuredRootContent && shouldAnimateRootHeight) {\n transitions.push(`height ${ROOT_HEIGHT_TRANSITION_MS}ms ease-out`);\n }\n return transitions.join(\", \");\n })(),\n willChange: \"height\",\n width: \"450px\",\n maxWidth: \"100%\",\n minWidth: \"320px\",\n WebkitFontSmoothing: \"antialiased\",\n MozOsxFontSmoothing: \"grayscale\",\n }}\n >\n \n \n
\n {canGoBack && (\n \n \n \n )}\n \n {getTitle()}\n
\n\n {/* Sub-screen asset counts */}\n {!isTitleCentered() &&\n activeMode === \"swap\" &&\n swapStep === \"choose-swap-asset\" &&\n swapType === \"exactIn\" && (\n \n {fromTokens.length} asset(s) selected\n \n )}\n \n\n {/* Right side icons */}\n \n {hasQuoteRefreshCountdown && (\n \n )}\n setSwapStep(\"history\")}\n style={{\n alignItems: \"center\",\n backgroundColor: theme.primitives.iconButton.backgroundColor,\n borderColor: theme.primitives.iconButton.borderColor,\n borderRadius: theme.radius.iconButton,\n borderStyle: \"solid\",\n borderWidth: \"1px\",\n boxShadow: theme.primitives.iconButton.boxShadow,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n height: \"28px\",\n justifyContent: \"center\",\n width: \"28px\",\n cursor: \"pointer\",\n padding: 0,\n }}\n >\n \n \n \n \n \n \n {showCloseButton && (\n \n \n \n \n \n )}\n \n \n\n {/* ------------------------------------------------------------------ */}\n {/* Main content area */}\n {/* ------------------------------------------------------------------ */}\n \n {/* =============================================================== */}\n {/* SHARED SUB-SCREENS (non-drawer panels) */}\n {/* =============================================================== */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep !== \"idle\" &&\n swapStep !== \"choose-swap-asset\" &&\n swapStep !== \"choose-receive-asset\" &&\n swapStep !== \"enter-recipient\" && (\n <>\n {/* Panel: preview. */}\n {swapStep === \"preview-intent\" && (\n \n {\n clearPendingSwapIntent();\n setSwapStep(\"idle\");\n }}\n onTransitionChange={setIsPreviewTransitioning}\n opportunity={selectedOpportunity}\n recipientAddress={transferRecipientAddress}\n steps={steps}\n supportedTokenAssets={supportedChainsAndTokens}\n swapBalances={swapBalance}\n swapType={swapType}\n toAmount={previewDestinationAmount}\n toAmountTokens={\n previewDestinationAmount\n ? `${previewDestinationAmount}`\n : undefined\n }\n toAmountUsd={previewToAmountUsd}\n toToken={toTokenWithFetchedBalance}\n totalFeeUsd={intentFeeUsd}\n />\n \n )}\n\n {swapStep === \"progress\" && (\n \n )}\n\n {(swapStep === \"success\" || swapStep === \"failed\") &&\n currentSwapEntry && (\n
\n \n
\n )}\n \n )}\n\n {/* =============================================================== */}\n {/* HISTORY SCREEN */}\n {/* =============================================================== */}\n {swapStep === \"history\" && (\n \n )}\n\n {/* =============================================================== */}\n {/* SWAP IDLE SCREEN */}\n {/* =============================================================== */}\n {activeMode === \"swap\" &&\n [\n \"idle\",\n \"choose-swap-asset\",\n \"choose-receive-asset\",\n \"enter-recipient\",\n ].includes(swapStep) && (\n <>\n {\n handleSwapAmountChange(val, panel);\n }}\n onOpenDestPicker={() =>\n openDrawerStep(\"choose-receive-asset\")\n }\n onOpenRecipientPicker={handleOpenRecipientEditor}\n onOpenSourcePicker={(index) => {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n setEditingAssetIndex(index ?? null);\n beginSourcePickerEdit();\n openDrawerStep(\"choose-swap-asset\");\n }}\n onUpdateTokens={handleSwapTokensUpdate}\n receiveQuoteAmount={\n swapType === \"exactIn\" ? idleReceiveQuoteAmount : undefined\n }\n receiveQuoteUsd={idleReceiveQuoteUsd}\n recipientAddress={effectiveRecipientAddress}\n sourceRouteMessage={insufficientSourceIssue?.message}\n sourceRouteStatus={\n insufficientSourceIssue\n ? \"insufficient\"\n : isExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n swapType={swapType}\n toToken={toTokenWithFetchedBalance}\n totalBalance={totalSwapBalanceUsd}\n usdValue={amount && usdValue > 0 ? usdValue.toFixed(2) : \"\"}\n />\n\n {receiveAmountIssue && (\n \n )}\n\n {configuredAmountIssue && (\n \n )}\n\n {txError && !blockingQuoteIssue && (\n \n )}\n\n {/* CTA Button */}\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n void handleEnterPreview();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: blockingQuoteIssue\n ? \"#FCEEED\"\n : isSwapCtaDisabled\n ? theme.colors.surfaceCool\n : primaryButtonBackground,\n border: blockingQuoteIssue ? \"1px solid #F7C4C1\" : \"none\",\n borderRadius: theme.radius.primaryButton,\n boxShadow:\n blockingQuoteIssue || isSwapCtaDisabled\n ? \"none\"\n : theme.shadows.primaryButton,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"7px\",\n height: \"42px\",\n justifyContent: \"center\",\n marginTop: \"8px\",\n paddingInline: \"16px\",\n cursor: isSwapCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {blockingQuoteIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n quoteRefreshing ||\n receiveMaxCalculating ? (\n\t \n ) : null}\n \n {needsWalletConnection\n ? walletCtaLabel\n : fromTokens.length === 0\n ? \"Add Assets to Bridge\"\n : quoteCtaLabel(\"Review swap\")}\n \n \n \n \n )}\n\n {/* =============================================================== */}\n {/* DEPOSIT MODE LAYOUT */}\n {/* =============================================================== */}\n {activeMode === \"deposit\" &&\n [\n \"idle\",\n \"choose-swap-asset\",\n \"choose-receive-asset\",\n \"enter-recipient\",\n ].includes(swapStep) && (\n <>\n {selectedOpportunity && (\n <>\n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n captureSourceSelectionRequiredUsd();\n beginSourcePickerEdit();\n openDrawerStep(\"choose-swap-asset\");\n }}\n onOpenTokenPicker={() =>\n openDrawerStep(\"choose-receive-asset\")\n }\n onSetPercent={handleDepositPercentSelect}\n routeMessage={insufficientSourceIssue?.message}\n routeStatus={\n insufficientSourceIssue\n ? \"insufficient\"\n : displayExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n showAutoBadge={!sourceSelectionTouched}\n isTokenPickerDisabled={isReceiveAssetPickerLocked}\n hideDestinationTokenDropdownIcon={\n hideDestinationTokenDropdownIcon\n }\n tokenValue={depositTokenDisplay}\n toToken={toTokenWithFetchedBalance}\n totalBalance={totalSwapBalanceUsd}\n usdValue={depositUsdDisplay}\n />\n\n {receiveAmountIssue && (\n \n )}\n\n {configuredAmountIssue && (\n \n )}\n\n {txError && !blockingQuoteIssue && (\n \n )}\n\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n void handleEnterPreview();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: blockingQuoteIssue\n ? \"#FCEEED\"\n : isDepositCtaDisabled\n ? theme.colors.surfaceCool\n : primaryButtonBackground,\n border: blockingQuoteIssue\n ? \"1px solid #F7C4C1\"\n : \"none\",\n borderRadius: blockingQuoteIssue\n ? \"4px\"\n : theme.radius.primaryButton,\n boxShadow:\n blockingQuoteIssue || isDepositCtaDisabled\n ? \"none\"\n : theme.shadows.primaryButton,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"7px\",\n height: \"40px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n cursor: isDepositCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {blockingQuoteIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n isExactOutPaymentQuotePending ||\n receiveMaxCalculating ? (\n\t \n ) : null}\n \n {quoteCtaLabel(\"Review deposit\")}\n \n \n \n \n )}\n \n )}\n\n {/* =============================================================== */}\n {/* SEND MODE — recipient first, then amount, then asset */}\n {/* =============================================================== */}\n {activeMode === \"send\" &&\n [\n \"idle\",\n \"choose-swap-asset\",\n \"choose-receive-asset\",\n \"enter-recipient\",\n ].includes(swapStep) && (\n <>\n \n openDrawerStep(\"choose-receive-asset\")\n }\n isRecipientLocked={isConfiguredRecipientLocked}\n onOpenRecipientPicker={\n isConfiguredRecipientLocked\n ? () => undefined\n : handleOpenRecipientEditor\n }\n onOpenSourcePicker={() => {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n setEditingAssetIndex(null);\n captureSourceSelectionRequiredUsd();\n beginSourcePickerEdit();\n openDrawerStep(\"choose-swap-asset\");\n }}\n onSetPercent={handleSendPercentSelect}\n recipientAddress={recipientAddress || \"\"}\n routeMessage={insufficientSourceIssue?.message}\n routeStatus={\n insufficientSourceIssue\n ? \"insufficient\"\n : displayExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n showAutoBadge={!sourceSelectionTouched}\n toToken={toTokenWithFetchedBalance}\n totalBalance={totalSwapBalanceUsd}\n usdValue={\n amount && sendAmountUsd > 0 ? sendAmountUsd.toFixed(2) : \"\"\n }\n />\n\n {receiveAmountIssue && (\n \n )}\n\n {configuredAmountIssue && (\n \n )}\n\n {txError && !blockingQuoteIssue && (\n \n )}\n\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n if (sendNeedsRecipient) {\n handleOpenRecipientEditor();\n return;\n }\n void handleEnterPreview();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: blockingQuoteIssue\n ? \"#FCEEED\"\n : isSendCtaDisabled\n ? theme.colors.surfaceCool\n : primaryButtonBackground,\n border: blockingQuoteIssue ? \"1px solid #F7C4C1\" : \"none\",\n borderRadius: blockingQuoteIssue\n ? \"4px\"\n : theme.radius.primaryButton,\n boxShadow:\n blockingQuoteIssue || isSendCtaDisabled\n ? \"none\"\n : theme.shadows.primaryButton,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"7px\",\n height: \"40px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n cursor: isSendCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {blockingQuoteIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n (!sendNeedsRecipient &&\n (isExactOutPaymentQuotePending ||\n receiveMaxCalculating)) ? (\n\t \n ) : null}\n \n {sendCtaLabel}\n \n \n \n \n )}\n \n \n\n {/* ================================================================== */}\n {/* DRAWER PANELS — rendered as direct children of root widget */}\n {/* so they overlay the main page as bottom drawers */}\n {/* ================================================================== */}\n\n {/* Drawer: enter-recipient */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep === \"enter-recipient\" && (\n \n {\n setTxError(null);\n closeDrawerToIdle();\n }}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n backgroundColor: \"rgba(0,0,0,0.35)\",\n pointerEvents: \"auto\",\n opacity: isRecipientDrawerClosing ? 0 : 1,\n transition: `opacity ${DRAWER_CLOSE_MS}ms ease`,\n }}\n />\n \n \n \n \n \n {\n setTxError(null);\n closeDrawerToIdle();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: theme.colors.surface,\n border: `1px solid ${theme.colors.border}`,\n borderRadius: \"8px\",\n cursor: \"pointer\",\n display: \"flex\",\n flexShrink: 0,\n height: \"32px\",\n justifyContent: \"center\",\n padding: 0,\n width: \"32px\",\n }}\n type=\"button\"\n >\n \n \n \n Recipient\n \n \n \n \n \n Wallet Address\n \n {activeMode === \"swap\" && defaultRecipientAddress && (\n \n Reset to default\n \n )}\n \n {\n setRecipientAddress(next);\n if (txError) setTxError(null);\n }}\n onClear={() => setRecipientAddress(\"\")}\n placeholder=\"Wallet address\"\n value={recipientAddress}\n />\n {txError && (\n \n {txError}\n \n )}\n {activeMode === \"send\" && (\n \n Recipient must be different from the connected wallet.\n \n )}\n \n Save\n \n \n \n )}\n\n {/* Drawer: choose-swap-asset */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep === \"choose-swap-asset\" && (\n \n \n \n {\n if (activeMode === \"swap\") {\n const next = [...fromTokens];\n const targetIndex =\n editingAssetIndex !== null &&\n editingAssetIndex < next.length\n ? editingAssetIndex\n : null;\n const existingToken =\n targetIndex !== null ? next[targetIndex] : undefined;\n const tokenChanged = !isSameTokenSelection(\n existingToken,\n token\n );\n const preservedAmount = tokenChanged\n ? \"\"\n : existingToken?.userAmount ||\n (targetIndex === 0 ? amount : \"\");\n const newToken = {\n ...token,\n userAmount: preservedAmount,\n };\n\n if (targetIndex !== null) {\n next[targetIndex] = newToken;\n } else {\n next.push(newToken);\n }\n\n if (tokenChanged) {\n clearPendingSwapIntent();\n setAmount(getSourceAmountInput(next));\n }\n if (swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n setFromTokens(next);\n closeDrawerToIdle();\n } else if (\n activeMode === \"deposit\" ||\n activeMode === \"send\"\n ) {\n handleSourcePickerDraftSelectionChange([\n { ...token, userAmount: \"\" },\n ]);\n }\n }}\n onSelectionChange={\n activeMode === \"deposit\" || activeMode === \"send\"\n ? handleSourcePickerDraftSelectionChange\n : undefined\n }\n onToggle={(token) => {\n const isDepositOrSendSourcePicker =\n activeMode === \"deposit\" || activeMode === \"send\";\n const prevTokens = isDepositOrSendSourcePicker\n ? sourcePickerSelectedTokens\n : fromTokens;\n const isSameSelection = (\n a: SwapTokenOption,\n b: SwapTokenOption\n ) => {\n if (a.isUnified || b.isUnified) {\n return Boolean(\n a.isUnified &&\n b.isUnified &&\n a.unifiedSymbol === b.unifiedSymbol\n );\n }\n return (\n a.contractAddress.toLowerCase() ===\n b.contractAddress.toLowerCase() && a.chainId === b.chainId\n );\n };\n const sourceTokens = token.sourceTokens ?? [];\n const isSameUnifiedGroup = (item: SwapTokenOption) =>\n Boolean(\n item.isUnified &&\n token.isUnified &&\n item.unifiedSymbol === token.unifiedSymbol\n );\n const withDefaultAmount = (item: SwapTokenOption) => ({\n ...item,\n userAmount:\n activeMode === \"swap\" && prevTokens.length === 0\n ? amount\n : \"\",\n });\n const getNextTokens = () => {\n if (\n isDepositOrSendSourcePicker &&\n token.isUnified &&\n sourceTokens.length > 0\n ) {\n const hasUnifiedSelection =\n prevTokens.some(isSameUnifiedGroup);\n const areAllChildrenSelected = sourceTokens.every(\n (source) =>\n prevTokens.some((item) =>\n isSameSelection(item, source)\n )\n );\n const withoutGroup = prevTokens.filter(\n (item) =>\n !isSameUnifiedGroup(item) &&\n !sourceTokens.some((source) =>\n isSameSelection(item, source)\n )\n );\n\n if (hasUnifiedSelection || areAllChildrenSelected) {\n return withoutGroup;\n }\n\n return [\n ...withoutGroup,\n ...sourceTokens.map((source) =>\n withDefaultAmount(source)\n ),\n ];\n }\n\n if (isDepositOrSendSourcePicker && !token.isUnified) {\n const unifiedSelection = prevTokens.find(\n (item) =>\n item.isUnified &&\n item.sourceTokens?.some((source) =>\n isSameSelection(source, token)\n )\n );\n\n if (unifiedSelection?.sourceTokens?.length) {\n const withoutUnified = prevTokens.filter(\n (item) => !isSameSelection(item, unifiedSelection)\n );\n return [\n ...withoutUnified,\n ...unifiedSelection.sourceTokens\n .filter((source) => !isSameSelection(source, token))\n .map((source) => withDefaultAmount(source)),\n ];\n }\n }\n\n const exists = prevTokens.find((item) =>\n isSameSelection(item, token)\n );\n if (exists) {\n return prevTokens.filter(\n (item) => !isSameSelection(item, token)\n );\n }\n const tokenSourceKeys = new Set(\n (token.sourceTokens ?? []).map(\n (source) =>\n `${source.chainId}-${source.contractAddress.toLowerCase()}`\n )\n );\n const next = prevTokens.filter((existing) => {\n if (\n token.isUnified &&\n tokenSourceKeys.has(\n `${existing.chainId}-${existing.contractAddress.toLowerCase()}`\n )\n ) {\n return false;\n }\n if (\n existing.isUnified &&\n existing.sourceTokens?.some(\n (source) =>\n source.chainId === token.chainId &&\n source.contractAddress.toLowerCase() ===\n token.contractAddress.toLowerCase()\n )\n ) {\n return false;\n }\n return true;\n });\n return [...next, withDefaultAmount(token)];\n };\n const nextTokens = getNextTokens();\n\n if (isDepositOrSendSourcePicker) {\n handleSourcePickerDraftSelectionChange(nextTokens);\n } else {\n clearPendingSwapIntent();\n setFromTokens(nextTokens);\n }\n }}\n preserveSelectedBelowMinimum={false}\n requiredUsd={assetSelectorRequiredUsdDisplay}\n selectedTokens={sourcePickerSelectedTokens}\n showBelowMinimumInline={\n activeMode === \"swap\" && swapType === \"exactIn\"\n }\n swapBalance={swapBalance}\n swapSupportedChains={swapSupportedChainsAndTokens}\n title={\n activeMode === \"deposit\" || activeMode === \"send\"\n ? \"Choose Assets to Pay with\"\n : \"Choose assets to send\"\n }\n />\n \n \n )}\n\n {/* Drawer: choose-receive-asset */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep === \"choose-receive-asset\" && (\n \n \n \n {\n const tokenChanged = !isSameTokenSelection(toToken, token);\n if (tokenChanged) {\n onReceiveAssetChange?.({\n chainId: token.chainId,\n chainName: token.chainName,\n contractAddress: token.contractAddress,\n symbol: token.symbol,\n });\n }\n if (activeMode === \"send\" || activeMode === \"deposit\") {\n setExactOutQuoteSourceModeValue(\"all\");\n if (tokenChanged) {\n clearPendingSwapIntent();\n if (!isConfiguredAmountFixed) {\n setAmount(\"\");\n }\n }\n setSwapType(\"exactOut\");\n if (activeMode === \"deposit\") {\n const nextDeposit = getDepositForTokenSelection(\n configuredDepositOptions,\n token\n );\n if (nextDeposit) {\n setSelectedOpportunity(nextDeposit);\n }\n }\n setToToken(token);\n closeDrawerToIdle();\n return;\n }\n if (tokenChanged) {\n clearPendingSwapIntent();\n }\n if (swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n setToToken(token);\n closeDrawerToIdle();\n }}\n />\n \n \n )}\n \n );\n\n if (embed) return widgetContent;\n\n return (\n \n \n \n \n \n {widgetContent}\n \n \n );\n}\n\nexport default NexusWidget;\n", + "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\"use client\";\n\nimport {\n ERROR_CODES,\n type EthereumProvider,\n} from \"@avail-project/nexus-core\";\nimport Decimal from \"decimal.js\";\nimport { AlertCircle, ArrowLeft, ChevronDown, Loader2 } from \"lucide-react\";\nimport React, {\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { flushSync } from \"react-dom\";\nimport {\n createPublicClient,\n encodeFunctionData,\n erc20Abi,\n http,\n isAddress,\n parseUnits,\n zeroAddress,\n} from \"viem\";\nimport { mainnet } from \"viem/chains\";\nimport { normalize } from \"viem/ens\";\nimport {\n useAccount,\n useConnect,\n useConnectorClient,\n usePublicClient,\n useWalletClient,\n} from \"wagmi\";\nimport { ErrorBoundary } from \"../common/components/ErrorBoundary\";\nimport { useTransactionSteps } from \"../common/tx/useTransactionSteps\";\nimport type {\n BridgeStepType,\n SwapStepType,\n} from \"../common/types/transaction-flow\";\nimport {\n CHAIN_METADATA,\n getShortChainName,\n isSwapSupportedBySdkChainList,\n SUPPORTED_CHAINS,\n TOKEN_CONTRACT_ADDRESSES,\n TOKEN_METADATA,\n} from \"../common/utils/constant\";\nimport { type UserAsset, useNexus } from \"../nexus/NexusProvider\";\nimport { Button } from \"../ui/button\";\nimport { Dialog, DialogContent, DialogTrigger } from \"../ui/dialog\";\nimport { DepositFundingMethod } from \"./components/deposit-funding-method\";\nimport { DepositIdleForm } from \"./components/deposit-idle-form\";\nimport { DepositOnrampFlow } from \"./components/deposit-onramp-flow\";\nimport {\n type NexusWidgetProgressEvent,\n NexusWidgetProgressScreen,\n} from \"./components/nexus-widget-progress-screen\";\nimport {\n getCachedReceiveTokenMatch,\n preloadReceiveTokens,\n ReceiveAssetSelector,\n} from \"./components/receive-asset-selector\";\nimport { RecipientInput } from \"./components/recipient-input\";\nimport { SendIdleForm } from \"./components/send-idle-form\";\nimport { StatusAlert } from \"./components/status-alerts\";\nimport {\n deriveTokenOptions,\n SwapAssetSelector,\n type SwapTokenOption,\n} from \"./components/swap-asset-selector\";\nimport { SwapIdleForm } from \"./components/swap-idle-form\";\nimport {\n type BridgeProvider,\n type SwapIntentData,\n type SwapIntentDestination,\n type SwapIntentSource,\n SwapIntentPreview,\n} from \"./components/swap-intent-preview\";\nimport {\n NEXUS_WIDGET_DEFAULT_PRIMARY_COLOR,\n NEXUS_WIDGET_FAST_SPINNER_STYLE,\n nexusWidgetTheme,\n} from \"./theme\";\nimport {\n type NexusWidgetAppearance,\n type NexusWidgetConfig,\n type NexusWidgetDepositConfig,\n type NexusWidgetDestination,\n type NexusWidgetDestinationToken,\n type NexusWidgetDepositOpportunityConfig,\n type NexusWidgetDepositOpportunityMetadata,\n type NexusWidgetMode,\n type NexusWidgetPrefillToken,\n type NexusWidgetProps,\n type NexusWidgetRuntimePrefill,\n type SwapType,\n} from \"./types\";\nimport { findCitreaReceiveToken } from \"./utils/citrea-tokens\";\nimport {\n type DepositSourceFilter,\n getDepositSourceId,\n resolveDepositSourceSelection,\n} from \"./utils/deposit-source-selection\";\n\n// ---------------------------------------------------------------------------\n// Types for swap step machine\n// ---------------------------------------------------------------------------\n\ntype SwapStep =\n | \"idle\" // main screen\n | \"choose-swap-asset\" // pick source token\n | \"choose-receive-asset\" // pick receive token\n | \"enter-recipient\" // pick recipient (send mode)\n | \"preview-intent\" // intent preview card\n | \"progress\" // transaction in flight\n | \"success\" // completed seamlessly\n | \"failed\" // failed swap receipt\n | \"history\"; // transaction history\n\ntype DepositFundingStep = \"method\" | \"wallet\" | \"onramp\";\n\ntype SourceFilterTab = \"all\" | \"native\" | \"stables\";\n\ntype SwapHistoryStatus =\n | \"pending\"\n | \"fulfilled\"\n | \"failed\"\n | \"timeout\"\n | \"refund-initiated\";\n\ninterface SwapHistoryEntry {\n autoRefundAvailable?: boolean;\n createdAt: number;\n durationSeconds?: number;\n endedAt?: number;\n error?: string;\n failedStepType?: string;\n failureDescription?: string;\n failureMessage?: string;\n feeUsd?: string;\n finalExplorerUrl?: string | null;\n fromTokens: SwapTokenOption[];\n id: string;\n intentData: SwapIntentData | null;\n intentExplorerUrl?: string | null;\n intentId?: number;\n mode: NexusWidgetMode;\n opportunity?: NexusWidgetDepositOpportunityMetadata;\n recipientAddress?: string;\n requestedToAmount?: string;\n requestedToValue?: string;\n sourceExplorerUrl?: string | null;\n startedAt: number;\n status: SwapHistoryStatus;\n toToken?: SwapTokenOption;\n}\n\ntype HistorySourceRow = {\n amount: string;\n chainId?: number;\n chainLogo?: string;\n chainName: string;\n contractAddress?: string;\n key: string;\n symbol: string;\n tokenLogo?: string;\n value?: unknown;\n};\n\ntype SwapQuoteIssue = {\n type: \"insufficientSources\";\n message: string;\n missingUsd?: string;\n};\n\ntype ReceiveAmountIssue = {\n ctaLabel: string;\n message: string;\n type: \"configuredAmountLimit\" | \"receiveLimitExceeded\" | \"unpricedReceiveToken\";\n};\n\ntype CachedIntentUsdRate = {\n amount: string;\n rate: string;\n updatedAt: number;\n value: string;\n};\n\ntype PredictiveQuote = {\n key: string;\n mode: \"exactIn\" | \"exactOut\";\n sources?: SwapTokenOption[];\n toAmount?: string;\n toUsd?: string;\n};\n\ntype IntentFetchTiming = {\n background: boolean;\n endTimeMs?: number;\n fetchDurationMs?: number;\n operation: string;\n quoteInputKey: string;\n runId: number;\n sourceCount?: number;\n startTimeMs: number;\n walletAddress?: string | null;\n};\n\ntype PredictiveQuoteBaseline = {\n destinationUsdRate: string;\n exactInDestinationAmountPerSourceUsd?: string;\n exactOutSourceUsdPerDestinationUsd?: string;\n updatedAt: number;\n};\n\nconst DESTINATION_RECEIVE_LIMIT_USD_BY_CHAIN_ID: Record = {\n [SUPPORTED_CHAINS.MEGAETH]: 10_000,\n [SUPPORTED_CHAINS.CITREA]: 2000,\n [SUPPORTED_CHAINS.SCROLL]: 500,\n};\n\nconst SCIENTIFIC_DECIMAL_REGEX = /^-?(?:\\d+\\.?\\d*|\\.\\d+)e[+-]?\\d+$/i;\n\nconst QUOTE_REFRESH_INTERVAL_MS = 30000;\nconst EXACT_OUT_INPUT_DEBOUNCE_MS = 1300;\nconst DRAWER_CLOSE_MS = 220;\nconst BALANCE_REFRESH_AFTER_TERMINAL_MS = 3000;\nconst MODAL_HEIGHT_TRANSITION_MS = 220;\nconst ROOT_HEIGHT_TRANSITION_MS = 140;\nconst ASSET_SELECTOR_DRAWER_HEIGHT = \"90%\";\nconst TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT = 620;\nconst CONFIGURED_RECEIVE_SELECTOR_BASE_HEIGHT = 170;\nconst CONFIGURED_RECEIVE_SELECTOR_ROW_HEIGHT = 62;\nconst BASIS_POINTS = 10000;\nconst PREDICTIVE_EXACT_IN_DISCOUNT_BPS = 50;\nconst PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_PCT = 0.1;\nconst PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_MAX_USD = 2;\nconst PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_PCT = 0.02;\nconst PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_MAX_USD = 1;\nconst PREDICTIVE_QUOTE_DISPLAY_DECIMALS = 8;\nconst DEPOSIT_TOKEN_DISPLAY_DECIMALS = 8;\nconst ETHEREUM_MAINNET_CHAIN_ID = 1;\nconst SDK_EXACT_OUT_STABLE_SYMBOLS = new Set([\n \"USDC\",\n \"USDT\",\n \"DAI\",\n \"BUSD\",\n \"TUSD\",\n \"FRAX\",\n \"LUSD\",\n \"USDD\",\n \"USDP\",\n \"GUSD\",\n]);\nconst SWAP_HISTORY_STORAGE_KEY_PREFIX = \"nexus-widget-transaction-history-v1\";\nconst TIMEOUT_LABEL = \"Timed Out\";\nconst PROGRESS_EVENT_NAMES = {\n BRIDGE_PLAN_LIST: \"bridge_plan_list\",\n BRIDGE_PLAN_PROGRESS: \"bridge_plan_progress\",\n SWAP_PLAN_LIST: \"swap_plan_list\",\n SWAP_PLAN_PROGRESS: \"swap_plan_progress\",\n} as const;\nconst PLAN_FINAL_STATES = new Set([\"completed\", \"confirmed\", \"submitted\"]);\nconst PLAN_STEP_FUNDS_MAY_HAVE_MOVED_STATES = new Set([\n \"completed\",\n \"confirmed\",\n \"submitted\",\n \"failed\",\n]);\nconst theme = nexusWidgetTheme;\nconst tooltipSurface = theme.colors.surface;\nconst tooltipText = theme.colors.textStrong;\nconst tooltipBorder = theme.colors.border;\nconst uiFont = theme.fonts.sans;\nconst modalHeightTransitionStyle = {\n interpolateSize: \"allow-keywords\",\n} as React.CSSProperties;\n\nconst getCappedTokenDisplayDecimals = (decimals?: number) => {\n const parsedDecimals = Number(decimals);\n if (!Number.isFinite(parsedDecimals) || parsedDecimals < 0) {\n return DEPOSIT_TOKEN_DISPLAY_DECIMALS;\n }\n return Math.min(Math.floor(parsedDecimals), DEPOSIT_TOKEN_DISPLAY_DECIMALS);\n};\nconst modalHeightTransition = `height ${MODAL_HEIGHT_TRANSITION_MS}ms ease, max-height ${MODAL_HEIGHT_TRANSITION_MS}ms ease`;\n\nconst normalizeSdkExactOutStableSymbol = (symbol?: string) =>\n (symbol ?? \"\")\n .trim()\n .toUpperCase()\n .replaceAll(\"\\u20ae\", \"T\")\n .replaceAll(/[^A-Z0-9]/g, \"\");\n\nconst isSdkExactOutStableSymbol = (symbol?: string) =>\n SDK_EXACT_OUT_STABLE_SYMBOLS.has(normalizeSdkExactOutStableSymbol(symbol));\n\nconst applyPredictiveExactOutBufferUsd = (\n amount: Decimal,\n pct: number,\n maxUsd: number\n) => Decimal.min(amount.mul(pct), new Decimal(maxUsd));\n\nconst getPredictiveExactOutSourceTargetUsd = (\n destinationUsdNeedingSources: Decimal,\n cachedSourceUsdRatio?: Decimal\n) => {\n if (destinationUsdNeedingSources.lte(0)) return new Decimal(0);\n if (cachedSourceUsdRatio?.gt(0)) {\n return destinationUsdNeedingSources.mul(cachedSourceUsdRatio);\n }\n\n const destinationBuffer = applyPredictiveExactOutBufferUsd(\n destinationUsdNeedingSources,\n PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_PCT,\n PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_MAX_USD\n );\n const destinationBuffered = destinationUsdNeedingSources.plus(\n destinationBuffer\n );\n const sourceBuffer = applyPredictiveExactOutBufferUsd(\n destinationBuffered,\n PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_PCT,\n PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_MAX_USD\n );\n\n return destinationBuffered.plus(sourceBuffer);\n};\n\nconst getSwapHistoryStorageKey = (ownerAddress?: string) =>\n `${SWAP_HISTORY_STORAGE_KEY_PREFIX}:${ownerAddress?.toLowerCase() || \"anonymous\"}`;\n\nconst getTokenSelectionKey = (token?: SwapTokenOption | null) => {\n if (!token) return \"\";\n if (token.isUnified) {\n return `unified:${token.unifiedSymbol ?? token.symbol}`;\n }\n return `${token.chainId ?? \"unknown\"}:${token.contractAddress.toLowerCase()}`;\n};\n\nconst getDisplaySourceTokenKey = (token?: SwapTokenOption | null) => {\n if (!token) return \"\";\n const chainId = token.chainId ?? \"unknown\";\n const address = token.contractAddress?.toLowerCase();\n return address\n ? `${chainId}:${address}`\n : `${chainId}:symbol:${token.symbol.toUpperCase()}`;\n};\n\nconst sumDecimalStrings = (left?: string, right?: string) => {\n const leftAmount = parseDecimalLoose(left) ?? new Decimal(0);\n const rightAmount = parseDecimalLoose(right) ?? new Decimal(0);\n const sum = leftAmount.plus(rightAmount);\n return sum.gt(0) ? sum.toFixed() : undefined;\n};\n\nconst mergeDisplaySourceTokens = (tokens: SwapTokenOption[]) => {\n const merged = new Map();\n\n for (const token of tokens) {\n const key = getDisplaySourceTokenKey(token);\n if (!key) continue;\n const existing = merged.get(key);\n if (!existing) {\n merged.set(key, token);\n continue;\n }\n\n const userAmount = sumDecimalStrings(\n existing.userAmount || existing.balance,\n token.userAmount || token.balance\n );\n const userAmountUsd = sumDecimalStrings(\n existing.userAmountUsd || existing.balanceInFiat,\n token.userAmountUsd || token.balanceInFiat\n );\n\n merged.set(key, {\n ...existing,\n balance: userAmount ?? existing.balance,\n balanceInFiat: userAmountUsd ?? existing.balanceInFiat,\n chainLogo: existing.chainLogo || token.chainLogo,\n logo: existing.logo || token.logo,\n userAmount: userAmount ?? existing.userAmount,\n userAmountUsd: userAmountUsd ?? existing.userAmountUsd,\n });\n }\n\n return Array.from(merged.values());\n};\n\nconst getTokenQuoteKey = (token?: SwapTokenOption | null) => {\n if (!token) return \"\";\n return [\n getTokenSelectionKey(token),\n token.symbol ?? \"\",\n token.decimals ?? \"\",\n ].join(\":\");\n};\n\nconst getSourceTokensQuoteKey = (tokens: SwapTokenOption[]) =>\n tokens\n .filter((token) => {\n const amt = token.userAmount ?? \"\";\n const cleaned = amt.replaceAll(/[^0-9.]/g, \"\");\n const num = Number.parseFloat(cleaned);\n return !Number.isNaN(num) && num > 0;\n })\n .map((token) =>\n [\n getTokenSelectionKey(token),\n token.symbol ?? \"\",\n token.decimals ?? \"\",\n token.userAmount ?? \"\",\n token.userAmountUsd ?? \"\",\n token.userAmountMode ?? \"\",\n ].join(\":\")\n )\n .join(\"|\");\n\nconst getSourceTokenSyncKey = (token: SwapTokenOption) =>\n [\n getTokenSelectionKey(token),\n token.userAmount ?? \"\",\n token.userAmountUsd ?? \"\",\n token.balance ?? \"\",\n token.balanceInFiat ?? \"\",\n ].join(\":\");\n\nconst getSourceTokenListSyncKey = (tokens: SwapTokenOption[]) =>\n tokens.map(getSourceTokenSyncKey).join(\"|\");\n\nconst isSameTokenSelection = (\n a?: SwapTokenOption | null,\n b?: SwapTokenOption | null\n) => Boolean(a && b && getTokenSelectionKey(a) === getTokenSelectionKey(b));\n\nconst getDepositConfigIdentity = (deposit?: NexusWidgetDepositOpportunityMetadata | null) => {\n if (!deposit) return \"\";\n return [\n deposit.chainId,\n deposit.tokenAddress.toLowerCase(),\n deposit.tokenSymbol,\n deposit.tokenDecimals,\n deposit.protocol ?? \"\",\n deposit.title ?? \"\",\n ].join(\":\");\n};\n\nconst isSameDepositConfig = (\n a?: NexusWidgetDepositOpportunityConfig | null,\n b?: NexusWidgetDepositOpportunityConfig | null\n) => {\n if (!a || !b) return false;\n return getDepositConfigIdentity(a) === getDepositConfigIdentity(b);\n};\n\nconst getConfiguredReceiveSelectorRootHeight = (tokenCount: number) => {\n if (tokenCount <= 0) return TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT;\n const visibleRows = Math.min(Math.max(tokenCount, 1), 6);\n return Math.min(\n TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT,\n Math.ceil(\n (CONFIGURED_RECEIVE_SELECTOR_BASE_HEIGHT +\n visibleRows * CONFIGURED_RECEIVE_SELECTOR_ROW_HEIGHT) /\n 0.9\n )\n );\n};\n\nconst getConfiguredDeposit = (\n config: RuntimeNexusWidgetConfig\n): NexusWidgetDepositOpportunityConfig | undefined => config.deposit ?? config.deposits?.[0];\n\ntype RuntimeDestinationPair = {\n chain: number;\n decimals?: number;\n logo?: string;\n symbol?: string;\n token: `0x${string}`;\n};\n\ntype RuntimeNexusWidgetAppearance = NexusWidgetAppearance & {\n logoUrl?: string;\n themeMode?: \"auto\" | \"light\" | \"dark\";\n widgetHeading?: string;\n};\n\ntype RuntimeNexusWidgetConfig = {\n allowedDestinationChains?: number[];\n allowedDestinationPairs?: RuntimeDestinationPair[];\n allowedSourcePairs?: RuntimeDestinationPair[];\n appearance?: RuntimeNexusWidgetAppearance;\n deposit?: NexusWidgetDepositOpportunityConfig;\n deposits?: NexusWidgetDepositOpportunityConfig[];\n mode: NexusWidgetMode;\n prefill?: NexusWidgetRuntimePrefill;\n};\n\ntype RuntimeNexusWidgetAmountInput = {\n max?: string;\n min?: string;\n mode: \"fixed\" | \"user\";\n value?: string;\n};\n\ntype NormalizedNexusWidgetConfig = {\n activeMode: NexusWidgetMode;\n amountInput?: RuntimeNexusWidgetAmountInput;\n appearance?: RuntimeNexusWidgetAppearance;\n config: RuntimeNexusWidgetConfig;\n depositOptions: NexusWidgetDepositOpportunityConfig[];\n isAmountFixed: boolean;\n isRecipientLocked: boolean;\n};\n\nconst normalizeConfiguredString = (value: unknown) => {\n if (value === null || value === undefined) return undefined;\n const text = String(value).trim();\n return text || undefined;\n};\n\nconst normalizeConfiguredAddress = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n return text && isAddress(text) ? text : undefined;\n};\n\nconst isPositiveGasLimit = (value: unknown): value is bigint =>\n typeof value === \"bigint\" && value > BigInt(0);\n\nconst normalizePositiveNumericString = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n if (!text) return undefined;\n try {\n const decimal = new Decimal(text);\n return decimal.isFinite() && decimal.gt(0) ? text : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst normalizeNonNegativeNumericString = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n if (!text) return undefined;\n try {\n const decimal = new Decimal(text);\n return decimal.isFinite() && decimal.gte(0) ? text : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst normalizeHttpUrlString = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n if (!text) return undefined;\n return /^https?:\\/\\//i.test(text) ? text : undefined;\n};\n\nconst normalizeNexusWidgetPrimaryColor = (value?: string) => {\n return normalizeConfiguredString(value);\n};\n\nconst expandHexColor = (value: string) => {\n const text = value.trim();\n if (/^#[0-9a-fA-F]{6}$/.test(text)) return text;\n if (/^#[0-9a-fA-F]{3}$/.test(text)) {\n const [r, g, b] = text.slice(1);\n return `#${r}${r}${g}${g}${b}${b}`;\n }\n return undefined;\n};\n\nconst parseNumberComponent = (value: string, max: number) => {\n const text = value.trim();\n if (!text) return undefined;\n if (text.endsWith(\"%\")) {\n const percent = Number.parseFloat(text.slice(0, -1));\n return Number.isFinite(percent)\n ? Math.max(0, Math.min(max, (percent / 100) * max))\n : undefined;\n }\n const parsed = Number.parseFloat(text);\n return Number.isFinite(parsed) ? Math.max(0, Math.min(max, parsed)) : undefined;\n};\n\nconst parseAngleComponent = (value: string) => {\n const parsed = Number.parseFloat(value.trim());\n if (!Number.isFinite(parsed)) return undefined;\n return ((parsed % 360) + 360) % 360;\n};\n\nconst hslToRgb = (h: number, s: number, l: number): [number, number, number] => {\n const saturation = Math.max(0, Math.min(100, s)) / 100;\n const lightness = Math.max(0, Math.min(100, l)) / 100;\n const c = (1 - Math.abs(2 * lightness - 1)) * saturation;\n const hp = h / 60;\n const x = c * (1 - Math.abs((hp % 2) - 1));\n let r1 = 0;\n let g1 = 0;\n let b1 = 0;\n if (hp >= 0 && hp < 1) {\n r1 = c;\n g1 = x;\n } else if (hp >= 1 && hp < 2) {\n r1 = x;\n g1 = c;\n } else if (hp >= 2 && hp < 3) {\n g1 = c;\n b1 = x;\n } else if (hp >= 3 && hp < 4) {\n g1 = x;\n b1 = c;\n } else if (hp >= 4 && hp < 5) {\n r1 = x;\n b1 = c;\n } else {\n r1 = c;\n b1 = x;\n }\n const m = lightness - c / 2;\n return [\n Math.round((r1 + m) * 255),\n Math.round((g1 + m) * 255),\n Math.round((b1 + m) * 255),\n ];\n};\n\nconst parseCssColorToRgb = (\n color: string,\n allowBrowserResolution = true\n): [number, number, number] | undefined => {\n const text = normalizeConfiguredString(color);\n if (!text) return undefined;\n\n const hex = expandHexColor(text);\n if (hex) {\n return [\n Number.parseInt(hex.slice(1, 3), 16),\n Number.parseInt(hex.slice(3, 5), 16),\n Number.parseInt(hex.slice(5, 7), 16),\n ];\n }\n\n const rgbMatch = text.match(/^rgba?\\((.*)\\)$/i);\n if (rgbMatch) {\n const parts = rgbMatch[1].trim().split(/[\\s,\\/]+/).filter(Boolean);\n if (parts.length >= 3) {\n const r = parseNumberComponent(parts[0], 255);\n const g = parseNumberComponent(parts[1], 255);\n const b = parseNumberComponent(parts[2], 255);\n if (r !== undefined && g !== undefined && b !== undefined) {\n return [Math.round(r), Math.round(g), Math.round(b)];\n }\n }\n }\n\n const hslMatch = text.match(/^hsla?\\((.*)\\)$/i);\n if (hslMatch) {\n const parts = hslMatch[1].trim().split(/[\\s,\\/]+/).filter(Boolean);\n if (parts.length >= 3) {\n const h = parseAngleComponent(parts[0]);\n const s = parseNumberComponent(parts[1], 100);\n const l = parseNumberComponent(parts[2], 100);\n if (h !== undefined && s !== undefined && l !== undefined) {\n return hslToRgb(h, s, l);\n }\n }\n }\n\n if (allowBrowserResolution && typeof document !== \"undefined\") {\n const canvas = document.createElement(\"canvas\");\n const context = canvas.getContext(\"2d\");\n if (context) {\n const baseline = \"#010203\";\n context.fillStyle = baseline;\n context.fillStyle = text;\n const resolved = String(context.fillStyle);\n if (resolved !== baseline || text.toLowerCase() === baseline) {\n return parseCssColorToRgb(resolved, false);\n }\n }\n }\n\n return undefined;\n};\n\nconst getReadableTextColor = (background: string) => {\n const rgb = parseCssColorToRgb(background);\n if (!rgb) return \"#FFFFFE\";\n const [rValue, gValue, bValue] = rgb;\n const r = rValue / 255;\n const g = gValue / 255;\n const b = bValue / 255;\n const linear = [r, g, b].map((channel) =>\n channel <= 0.03928\n ? channel / 12.92\n : Math.pow((channel + 0.055) / 1.055, 2.4)\n );\n const luminance = 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];\n return luminance > 0.54 ? \"#161615\" : \"#FFFFFE\";\n};\n\nconst normalizeNexusWidgetAmountInput = (\n config: NexusWidgetConfig\n): RuntimeNexusWidgetAmountInput | undefined => {\n if (config.mode === \"swap\") return undefined;\n const value = normalizePositiveNumericString(\n \"amount\" in (config.prefill ?? {}) ? config.prefill?.amount : undefined\n );\n const min = normalizeNonNegativeNumericString(config.validation?.minAmount);\n const max = normalizePositiveNumericString(config.validation?.maxAmount);\n return value || min || max ? { mode: \"user\", value, min, max } : undefined;\n};\n\nconst normalizeNexusWidgetAppearance = (\n appearance?: NexusWidgetAppearance\n): RuntimeNexusWidgetAppearance | undefined => {\n if (!appearance) return undefined;\n const appName = normalizeConfiguredString(appearance.appName);\n const appLogoURL = normalizeHttpUrlString(appearance.appLogoURL);\n const heading = normalizeConfiguredString(appearance.heading);\n const mode =\n appearance.mode === \"dark\" || appearance.mode === \"light\"\n ? appearance.mode\n : \"system\";\n const primaryColor = normalizeConfiguredString(appearance.primaryColor);\n return {\n ...appearance,\n appLogoURL,\n appName,\n heading,\n logoUrl: appLogoURL,\n mode,\n primaryColor,\n themeMode: mode === \"system\" ? \"auto\" : mode,\n widgetHeading: heading,\n };\n};\n\nconst normalizeConfiguredChainId = (value: unknown) => {\n const chainId = Number(value);\n return Number.isFinite(chainId) && chainId > 0 ? chainId : undefined;\n};\n\nconst getConfiguredDestinationChainId = (\n destination?: NexusWidgetDestination\n) => normalizeConfiguredChainId(destination?.chain);\n\nconst getConfiguredDestinationTokenSymbol = (\n token: NexusWidgetDestinationToken\n) => normalizeConfiguredString(token.symbol);\n\nconst getConfiguredDestinationTokenAddress = (\n token: NexusWidgetDestinationToken\n) => normalizeConfiguredAddress(token.address);\n\nconst toSwapTokenOptionFromConfiguredDestinationToken = (\n token: NexusWidgetDestinationToken,\n chainId: number\n): SwapTokenOption | undefined => {\n const contractAddress = getConfiguredDestinationTokenAddress(token);\n const symbol = getConfiguredDestinationTokenSymbol(token);\n if (!contractAddress || !symbol) return undefined;\n const chainMeta = CHAIN_METADATA[chainId];\n return {\n balance: \"0\",\n balanceInFiat: \"$0.00\",\n chainId,\n chainLogo: chainMeta?.logo,\n chainName: getShortChainName(chainId, chainMeta?.name ?? `Chain ${chainId}`),\n contractAddress,\n decimals: token.decimals ?? 18,\n logo: token.logo ?? \"\",\n name: symbol,\n symbol,\n };\n};\n\nconst toSwapTokenOptionFromDepositConfig = (\n deposit: NexusWidgetDepositOpportunityConfig\n): SwapTokenOption => {\n const chainMeta = CHAIN_METADATA[deposit.chainId];\n return {\n balance: \"0\",\n balanceInFiat: \"$0.00\",\n chainId: deposit.chainId,\n chainLogo: chainMeta?.logo,\n chainName: getShortChainName(\n deposit.chainId,\n chainMeta?.name ?? `Chain ${deposit.chainId}`\n ),\n contractAddress: deposit.tokenAddress,\n decimals: deposit.tokenDecimals,\n logo: deposit.tokenLogo || deposit.logo || \"\",\n name: deposit.tokenSymbol,\n symbol: deposit.tokenSymbol,\n };\n};\n\nconst applyNexusWidgetAppearanceToDeposit = (\n deposit: NexusWidgetDepositOpportunityConfig,\n appearance?: RuntimeNexusWidgetAppearance\n): NexusWidgetDepositOpportunityConfig => {\n const appName = normalizeConfiguredString(appearance?.appName);\n const logoUrl = normalizeConfiguredString(appearance?.logoUrl);\n if (!appName && !logoUrl) return deposit;\n return {\n ...deposit,\n depositTargetLogo: logoUrl ?? deposit.depositTargetLogo,\n logo: logoUrl ?? deposit.logo,\n protocol: appName ?? deposit.protocol,\n title: appName ?? deposit.title,\n };\n};\n\nconst getConfiguredDestinationPairs = (\n destination?: NexusWidgetDestination\n) => {\n const pairs: RuntimeNexusWidgetConfig[\"allowedDestinationPairs\"] = [];\n const chain = getConfiguredDestinationChainId(destination);\n if (!chain) return pairs;\n for (const token of destination?.tokens ?? []) {\n const address = getConfiguredDestinationTokenAddress(token);\n if (!address) continue;\n pairs.push({ chain, token: address });\n }\n return pairs;\n};\n\nconst hasConfiguredDestinationTokenList = (\n destination?: NexusWidgetDestination\n) => Boolean(destination?.tokens?.length);\n\nconst getConfiguredPrefillTokenPair = (\n prefill?: { token?: NexusWidgetPrefillToken }\n): RuntimeDestinationPair | undefined => {\n const token = prefill?.token;\n if (!token) return undefined;\n const chain = normalizeConfiguredChainId(token.chain);\n const address = getConfiguredDestinationTokenAddress(token);\n const symbol = getConfiguredDestinationTokenSymbol(token);\n if (!chain || !address || !symbol) return undefined;\n return {\n chain,\n decimals: token.decimals,\n logo: normalizeConfiguredString(token.logo),\n symbol,\n token: address,\n };\n};\n\nconst getConfiguredDestinationChainIds = (\n destination?: NexusWidgetDestination\n) => {\n const chain = getConfiguredDestinationChainId(destination);\n return chain ? [chain] : [];\n};\n\nconst toDepositConfigFromDestination = (\n config: NexusWidgetDepositConfig,\n token: NexusWidgetDestinationToken,\n appearance?: RuntimeNexusWidgetAppearance\n): NexusWidgetDepositOpportunityConfig | undefined => {\n const chainId = getConfiguredDestinationChainId(config.destination);\n const depositAddress = normalizeConfiguredAddress(config.depositAddress);\n const tokenAddress = getConfiguredDestinationTokenAddress(token);\n const tokenSymbol = getConfiguredDestinationTokenSymbol(token);\n if (\n !chainId ||\n !depositAddress ||\n !tokenAddress ||\n !tokenSymbol ||\n typeof config.executeDeposit !== \"function\"\n ) {\n return undefined;\n }\n\n const chainMeta = CHAIN_METADATA[chainId];\n return {\n chainId,\n depositTargetLogo: appearance?.logoUrl,\n executeDeposit: config.executeDeposit,\n logo: appearance?.logoUrl ?? token.logo ?? undefined,\n protocol:\n appearance?.appName ??\n appearance?.widgetHeading ??\n \"Deposit\",\n subtitle:\n chainMeta?.name\n ? `on ${getShortChainName(chainId, chainMeta.name)}`\n : undefined,\n title: appearance?.appName ?? appearance?.widgetHeading ?? \"Deposit\",\n tokenAddress,\n tokenDecimals: token.decimals ?? 18,\n tokenLogo: token.logo,\n tokenSymbol,\n };\n};\n\nconst getNexusWidgetDepositOptions = (\n config: NexusWidgetConfig,\n appearance?: RuntimeNexusWidgetAppearance\n) => {\n if (config.mode !== \"deposit\") return [];\n return (config.destination.tokens ?? [])\n .map((token) => toDepositConfigFromDestination(config, token, appearance))\n .filter((deposit): deposit is NexusWidgetDepositOpportunityConfig => Boolean(deposit));\n};\n\nconst normalizeNexusWidgetConfig = (\n rawConfig: NexusWidgetConfig\n): NormalizedNexusWidgetConfig => {\n const appearance = normalizeNexusWidgetAppearance(rawConfig.appearance);\n const amountInput = normalizeNexusWidgetAmountInput(rawConfig);\n const prefill: NexusWidgetRuntimePrefill = {};\n let activeMode: NexusWidgetMode =\n rawConfig.mode === \"deposit\" ||\n rawConfig.mode === \"send\" ||\n rawConfig.mode === \"swap\"\n ? rawConfig.mode\n : \"swap\";\n let isRecipientLocked = false;\n const depositOptions = getNexusWidgetDepositOptions(rawConfig, appearance).map(\n (deposit) => applyNexusWidgetAppearanceToDeposit(deposit, appearance)\n );\n let allowedDestinationPairs:\n | RuntimeNexusWidgetConfig[\"allowedDestinationPairs\"]\n | undefined;\n let allowedDestinationChains:\n | RuntimeNexusWidgetConfig[\"allowedDestinationChains\"]\n | undefined;\n\n if (amountInput?.value) {\n prefill.amount = amountInput.value;\n }\n\n if (rawConfig.mode === \"deposit\") {\n activeMode = \"deposit\";\n const pairs = getConfiguredDestinationPairs(rawConfig.destination);\n if (pairs.length > 0) {\n allowedDestinationPairs = pairs;\n prefill.destination = pairs[0];\n }\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (chainIds.length > 0) {\n allowedDestinationChains = chainIds;\n }\n } else if (rawConfig.mode === \"send\") {\n const recipient = normalizeConfiguredAddress(rawConfig.recipientAddress);\n if (recipient) {\n prefill.recipient = recipient;\n isRecipientLocked = true;\n }\n\n const hasDestinationTokens = hasConfiguredDestinationTokenList(\n rawConfig.destination\n );\n const pairs = getConfiguredDestinationPairs(rawConfig.destination);\n if (pairs.length > 0) {\n allowedDestinationPairs = pairs;\n prefill.destination ??= pairs[0];\n } else if (!hasDestinationTokens) {\n const prefillToken = getConfiguredPrefillTokenPair(rawConfig.prefill);\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (\n prefillToken &&\n (!chainIds.length || chainIds.includes(prefillToken.chain))\n ) {\n prefill.destination = prefillToken;\n }\n }\n\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (chainIds.length > 0) {\n allowedDestinationChains = chainIds;\n }\n } else if (rawConfig.mode === \"swap\") {\n const recipient = normalizeConfiguredAddress(rawConfig.recipientAddress);\n if (recipient) {\n prefill.recipient = recipient;\n }\n\n const hasDestinationTokens = hasConfiguredDestinationTokenList(\n rawConfig.destination\n );\n const pairs = getConfiguredDestinationPairs(rawConfig.destination);\n if (pairs.length > 0) {\n allowedDestinationPairs = pairs;\n prefill.destination ??= pairs[0];\n } else if (!hasDestinationTokens) {\n const prefillToken = getConfiguredPrefillTokenPair(rawConfig.prefill);\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (\n prefillToken &&\n (!chainIds.length || chainIds.includes(prefillToken.chain))\n ) {\n prefill.destination = prefillToken;\n }\n }\n\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (chainIds.length > 0) {\n allowedDestinationChains = chainIds;\n }\n }\n\n const runtimeConfig: RuntimeNexusWidgetConfig = {\n allowedDestinationChains,\n allowedDestinationPairs,\n deposit:\n activeMode === \"deposit\"\n ? depositOptions[0]\n : undefined,\n deposits: activeMode === \"deposit\" ? depositOptions : [],\n appearance,\n mode: activeMode,\n prefill,\n };\n\n return {\n activeMode,\n amountInput,\n appearance,\n config: runtimeConfig,\n depositOptions,\n isAmountFixed: false,\n isRecipientLocked,\n };\n};\n\nconst getDepositForTokenSelection = (\n deposits: NexusWidgetDepositOpportunityConfig[],\n token?: SwapTokenOption | null\n) => {\n if (!token) return undefined;\n return deposits.find(\n (deposit) =>\n deposit.chainId === token.chainId &&\n deposit.tokenAddress.toLowerCase() ===\n token.contractAddress.toLowerCase()\n );\n};\n\nconst sanitizeOpportunityForHistory = (\n opportunity?: NexusWidgetDepositOpportunityMetadata\n): NexusWidgetDepositOpportunityMetadata | undefined => {\n if (!opportunity) return undefined;\n return {\n label: opportunity.label,\n protocol: opportunity.protocol,\n logo: opportunity.logo,\n title: opportunity.title,\n subtitle: opportunity.subtitle,\n chainId: opportunity.chainId,\n tokenSymbol: opportunity.tokenSymbol,\n tokenDecimals: opportunity.tokenDecimals,\n tokenLogo: opportunity.tokenLogo,\n tokenAddress: opportunity.tokenAddress,\n apy: opportunity.apy,\n description: opportunity.description,\n };\n};\n\nconst sanitizeHistoryEntry = (entry: SwapHistoryEntry): SwapHistoryEntry => ({\n ...entry,\n createdAt: entry.createdAt ?? entry.startedAt ?? Date.now(),\n failureMessage:\n entry.status === \"timeout\" ? TIMEOUT_LABEL : entry.failureMessage,\n opportunity: sanitizeOpportunityForHistory(entry.opportunity),\n});\n\nconst sortSwapHistoryEntries = (entries: SwapHistoryEntry[]) =>\n [...entries].sort(\n (a, b) =>\n (b.createdAt ?? b.startedAt ?? 0) - (a.createdAt ?? a.startedAt ?? 0)\n );\n\nconst isStoredHistoryStatus = (value: unknown): value is SwapHistoryStatus =>\n value === \"pending\" ||\n value === \"fulfilled\" ||\n value === \"failed\" ||\n value === \"timeout\" ||\n value === \"refund-initiated\";\n\nconst isStoredMode = (value: unknown): value is NexusWidgetMode =>\n value === \"swap\" || value === \"deposit\" || value === \"send\";\n\nconst normalizeStoredHistoryEntry = (\n value: unknown\n): SwapHistoryEntry | null => {\n if (!value || typeof value !== \"object\") return null;\n const entry = value as Partial;\n const startedAt =\n typeof entry.startedAt === \"number\" && Number.isFinite(entry.startedAt)\n ? entry.startedAt\n : undefined;\n const createdAt =\n typeof entry.createdAt === \"number\" && Number.isFinite(entry.createdAt)\n ? entry.createdAt\n : startedAt;\n\n if (\n !entry.id ||\n typeof entry.id !== \"string\" ||\n !isStoredMode(entry.mode) ||\n !isStoredHistoryStatus(entry.status) ||\n !createdAt ||\n !startedAt\n ) {\n return null;\n }\n\n return {\n ...entry,\n id: entry.id,\n mode: entry.mode,\n status: entry.status,\n createdAt,\n startedAt,\n failureMessage:\n entry.status === \"timeout\" ? TIMEOUT_LABEL : entry.failureMessage,\n intentData: entry.intentData ?? null,\n fromTokens: Array.isArray(entry.fromTokens) ? entry.fromTokens : [],\n opportunity: sanitizeOpportunityForHistory(entry.opportunity),\n } as SwapHistoryEntry;\n};\n\nconst readSwapHistoryFromStorage = (storageKey: string): SwapHistoryEntry[] => {\n if (typeof window === \"undefined\") return [];\n\n try {\n const raw = window.localStorage.getItem(storageKey);\n if (!raw) return [];\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n return sortSwapHistoryEntries(\n parsed\n .map(normalizeStoredHistoryEntry)\n .filter((entry): entry is SwapHistoryEntry => Boolean(entry))\n );\n } catch {\n return [];\n }\n};\n\nconst writeSwapHistoryToStorage = (\n storageKey: string,\n entries: SwapHistoryEntry[]\n) => {\n if (typeof window === \"undefined\") return;\n\n try {\n const persistableEntries =\n sortSwapHistoryEntries(entries).map(sanitizeHistoryEntry);\n window.localStorage.setItem(\n storageKey,\n JSON.stringify(persistableEntries, (_key, value) =>\n typeof value === \"bigint\" ? value.toString() : value\n )\n );\n } catch {\n // localStorage can be unavailable or full; in-memory history still works.\n }\n};\n\nfunction QuoteRefreshCountdown({\n progress,\n isRefreshing,\n secondsRemaining,\n}: {\n progress: number;\n isRefreshing: boolean;\n secondsRemaining: number;\n}) {\n const [showTooltip, setShowTooltip] = useState(false);\n const radius = 7;\n const circumference = 2 * Math.PI * radius;\n const clampedProgress = Math.max(0, Math.min(1, progress));\n const tooltipLabel = isRefreshing\n ? \"Refreshing quotes...\"\n : `Refreshing quotes in ${Math.max(0, secondsRemaining)} second${\n secondsRemaining === 1 ? \"\" : \"s\"\n }`;\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n style={{\n alignItems: \"center\",\n backgroundColor: \"#FFFFFE\",\n borderRadius: \"999px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n height: \"22px\",\n justifyContent: \"center\",\n outline: \"1px solid #E8E8E7\",\n position: \"relative\",\n width: \"22px\",\n }}\n tabIndex={0}\n >\n {showTooltip && (\n \n {tooltipLabel}\n \n )}\n \n \n \n \n \n );\n}\n\nconst normalizeDecimalInputText = (value: unknown) => {\n const raw = String(value).trim();\n if (!raw) return \"\";\n if (SCIENTIFIC_DECIMAL_REGEX.test(raw)) return raw;\n return raw.replace(/[^0-9.-]/g, \"\");\n};\n\nconst parseDecimalLoose = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = normalizeDecimalInputText(value);\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst toViemDecimalString = (value: unknown, decimals: number) => {\n const parsed = parseDecimalLoose(value);\n if (!parsed || parsed.lte(0)) return \"0\";\n return parsed\n .toDecimalPlaces(Math.max(0, decimals), Decimal.ROUND_DOWN)\n .toFixed();\n};\n\nconst formatDecimalDisplay = (\n value: unknown,\n options: { min?: number; max?: number } = {}\n) => {\n const amount = parseDecimalLoose(value) ?? new Decimal(0);\n const max = options.max ?? 2;\n return amount.toDecimalPlaces(max).toFixed();\n};\n\nconst formatUsdDisplay = (value: unknown) => {\n const amount = parseDecimalLoose(value) ?? new Decimal(0);\n if (amount.gt(0) && amount.lt(0.01)) return \"<$0.01\";\n return `$${formatDecimalDisplay(amount, { min: 2, max: 2 })}`;\n};\n\nconst formatTokenDisplay = (value: unknown) => {\n const amount = parseDecimalLoose(value) ?? new Decimal(0);\n return formatDecimalDisplay(amount, { max: 8 });\n};\n\nconst getSwapTokenUsdValue = (token: SwapTokenOption) =>\n parseDecimalLoose(token.userAmountUsd) ??\n parseDecimalLoose(token.balanceInFiat) ??\n new Decimal(0);\n\nconst getSwapTokenBalanceUsdValue = (token: SwapTokenOption) =>\n parseDecimalLoose(token.balanceInFiat) ?? new Decimal(0);\n\nconst sortSwapTokensByUsdDesc = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const usdDelta = getSwapTokenUsdValue(b).cmp(getSwapTokenUsdValue(a));\n if (usdDelta !== 0) return usdDelta;\n return (a.symbol ?? \"\").localeCompare(b.symbol ?? \"\");\n });\n\nconst sortDisplaySourcesByBalanceUsdDesc = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const balanceUsdDelta = getSwapTokenBalanceUsdValue(b).cmp(\n getSwapTokenBalanceUsdValue(a)\n );\n if (balanceUsdDelta !== 0) return balanceUsdDelta;\n\n const spendUsdDelta = getSwapTokenUsdValue(b).cmp(getSwapTokenUsdValue(a));\n if (spendUsdDelta !== 0) return spendUsdDelta;\n\n return `${a.symbol ?? \"\"} ${a.chainName ?? \"\"}`.localeCompare(\n `${b.symbol ?? \"\"} ${b.chainName ?? \"\"}`\n );\n });\n\nconst getIntentSourceUsdValue = (source: SwapIntentData[\"sources\"][number]) =>\n parseDecimalLoose(source.value) ?? new Decimal(0);\n\nconst sortIntentSourcesByUsdDesc = (sources: SwapIntentData[\"sources\"]) =>\n [...sources].sort((a, b) => {\n const usdDelta = getIntentSourceUsdValue(b).cmp(getIntentSourceUsdValue(a));\n if (usdDelta !== 0) return usdDelta;\n return (a.token?.symbol ?? \"\").localeCompare(b.token?.symbol ?? \"\");\n });\n\nconst extractIntentIdFromUrl = (url?: string | null) => {\n if (!url) return undefined;\n const match = url.match(/(\\d+)(?:\\/)?$/);\n if (!match) return undefined;\n const parsed = Number(match[1]);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;\n};\n\nconst getNonEmptyString = (...values: unknown[]) => {\n for (const value of values) {\n if (typeof value !== \"string\") continue;\n const trimmed = value.trim();\n if (trimmed.length > 0) return trimmed;\n }\n return null;\n};\n\nconst isHttpUrl = (value?: string | null): value is string =>\n Boolean(value && /^https?:\\/\\//i.test(value));\n\nconst hasValidIntentExplorer = (\n entry: Pick\n) => isHttpUrl(entry.intentExplorerUrl);\n\nconst getHistoryExplorerUrl = (\n entry: Pick<\n SwapHistoryEntry,\n \"finalExplorerUrl\" | \"intentExplorerUrl\" | \"sourceExplorerUrl\"\n >\n) =>\n [\n entry.intentExplorerUrl,\n entry.finalExplorerUrl,\n entry.sourceExplorerUrl,\n ].find(isHttpUrl) ?? null;\n\nconst getFiniteNumber = (...values: unknown[]) => {\n for (const value of values) {\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value !== \"string\") continue;\n const parsed = Number(value);\n if (Number.isFinite(parsed)) return parsed;\n }\n return undefined;\n};\n\nconst getObjectChainId = (value: any) =>\n getFiniteNumber(\n value?.chainId,\n value?.chain?.id,\n value?.chain?.chainId,\n value?.toChainId,\n value?.destinationChainId,\n value?.data?.chainId,\n value?.data?.chain?.id,\n value?.data?.chain?.chainId,\n value?.data?.toChainId,\n value?.data?.destinationChainId,\n value?.result?.chainId,\n value?.result?.chain?.id\n );\n\nconst getExplorerBaseUrl = (chainId?: number, ...candidates: unknown[]) => {\n const directCandidates = candidates.flatMap((candidate: any) => [\n candidate?.blockExplorerUrl,\n candidate?.blockExplorerURL,\n candidate?.chainBlockExplorerUrl,\n candidate?.explorerBaseUrl,\n candidate?.explorerUrlBase,\n candidate?.blockExplorerUrls?.[0],\n candidate?.blockExplorers?.default?.url,\n candidate?.chain?.blockExplorerUrl,\n candidate?.chain?.blockExplorerURL,\n candidate?.chain?.chainBlockExplorerUrl,\n candidate?.chain?.explorerBaseUrl,\n candidate?.chain?.explorerUrlBase,\n candidate?.chain?.blockExplorerUrls?.[0],\n candidate?.chain?.blockExplorers?.default?.url,\n candidate?.data?.blockExplorerUrl,\n candidate?.data?.blockExplorerURL,\n candidate?.data?.chainBlockExplorerUrl,\n candidate?.data?.blockExplorerUrls?.[0],\n candidate?.data?.blockExplorers?.default?.url,\n candidate?.data?.chain?.blockExplorerUrl,\n candidate?.data?.chain?.blockExplorerURL,\n candidate?.data?.chain?.chainBlockExplorerUrl,\n candidate?.data?.chain?.blockExplorerUrls?.[0],\n candidate?.data?.chain?.blockExplorers?.default?.url,\n ]);\n return getNonEmptyString(\n ...directCandidates,\n chainId ? CHAIN_METADATA[chainId]?.blockExplorerUrls?.[0] : undefined,\n chainId\n ? (CHAIN_METADATA[chainId] as any)?.blockExplorers?.default?.url\n : undefined\n );\n};\n\nconst getTransactionHash = (...values: unknown[]) => {\n for (const value of values) {\n if (typeof value !== \"string\") continue;\n const trimmed = value.trim();\n if (/^0x[a-fA-F0-9]{64}$/.test(trimmed)) return trimmed;\n }\n return null;\n};\n\nconst getIntentHash = (...values: unknown[]) => getTransactionHash(...values);\n\nconst getObjectIntentHash = (value: any) =>\n getIntentHash(\n value?.intentHash,\n value?.intent_hash,\n value?.intent?.hash,\n value?.intent?.intentHash,\n value?.intent?.intent_hash,\n value?.requestHash,\n value?.request_hash,\n value?.request?.hash,\n value?.request?.requestHash,\n value?.rffHash,\n value?.rff_hash,\n value?.rff?.hash,\n value?.data?.intentHash,\n value?.data?.intent_hash,\n value?.data?.intent?.hash,\n value?.data?.intent?.intentHash,\n value?.data?.intent?.intent_hash,\n value?.data?.requestHash,\n value?.data?.request_hash,\n value?.data?.request?.hash,\n value?.data?.request?.requestHash,\n value?.data?.rffHash,\n value?.data?.rff_hash,\n value?.data?.rff?.hash,\n value?.result?.intentHash,\n value?.result?.intent_hash,\n value?.result?.intent?.hash,\n value?.result?.requestHash,\n value?.result?.request_hash,\n value?.result?.rffHash,\n value?.result?.rff_hash\n );\n\nconst getNexusExplorerNetwork = (network?: unknown) => {\n const normalized =\n typeof network === \"string\" ? network.trim().toLowerCase() : \"\";\n if (normalized === \"canary\" || normalized === \"testnet\") return normalized;\n return \"mainnet\";\n};\n\nconst getRffExplorerUrl = (network: unknown, intentHash?: string | null) =>\n intentHash\n ? `https://nexus-v2.${getNexusExplorerNetwork(network)}.avail.so/rff/${intentHash}`\n : null;\n\nconst getObjectTransactionHash = (value: any) =>\n getTransactionHash(\n value?.txHash,\n value?.transactionHash,\n value?.executeTxHash,\n value?.executeTransactionHash,\n value?.transferTransactionHash,\n value?.receipt?.transactionHash,\n value?.tx?.hash,\n value?.transaction?.hash,\n value?.data?.txHash,\n value?.data?.transactionHash,\n value?.data?.executeTxHash,\n value?.data?.executeTransactionHash,\n value?.data?.transferTransactionHash,\n value?.data?.receipt?.transactionHash,\n value?.data?.tx?.hash,\n value?.data?.transaction?.hash,\n value?.result?.txHash,\n value?.result?.transactionHash,\n value?.result?.receipt?.transactionHash\n );\n\nconst getExplorerTxUrl = (\n chainId?: number,\n txHash?: string | null,\n ...candidates: unknown[]\n) => {\n if (!chainId || !txHash) return null;\n const baseUrl = getExplorerBaseUrl(chainId, ...candidates);\n return baseUrl ? `${String(baseUrl).replace(/\\/$/, \"\")}/tx/${txHash}` : null;\n};\n\nconst getSdkSwapResult = (result: any) => {\n const candidate = result?.swapResult ?? result?.result;\n return candidate && typeof candidate === \"object\" ? candidate : null;\n};\n\nconst getSdkTransactionHash = (result: any) =>\n getObjectTransactionHash(result) ||\n getObjectTransactionHash(result?.executeResponse) ||\n getObjectTransactionHash(result?.execute) ||\n getObjectTransactionHash(result?.transfer) ||\n getObjectTransactionHash(result?.swapResult) ||\n getObjectTransactionHash(result?.result) ||\n null;\n\nconst getSdkExplorerUrl = (result: any) =>\n getNonEmptyString(\n result?.explorerUrl,\n result?.explorerURL,\n result?.txExplorerUrl,\n result?.transactionExplorerUrl,\n result?.execute?.explorerUrl,\n result?.execute?.explorerURL,\n result?.execute?.txExplorerUrl,\n result?.execute?.transactionExplorerUrl,\n result?.executeResponse?.explorerUrl,\n result?.executeResponse?.explorerURL,\n result?.executeResponse?.txExplorerUrl,\n result?.executeResponse?.transactionExplorerUrl,\n result?.executeExplorerUrl,\n result?.transferExplorerUrl,\n result?.swapResult?.explorerUrl,\n result?.swapResult?.explorerURL,\n result?.swapResult?.txExplorerUrl,\n result?.swapResult?.transactionExplorerUrl,\n result?.result?.explorerUrl,\n result?.result?.explorerURL,\n result?.result?.txExplorerUrl,\n result?.result?.transactionExplorerUrl\n );\n\nconst getSdkIntentExplorerUrl = (result: any, swapResult?: any) =>\n getNonEmptyString(\n swapResult?.intentExplorerUrl,\n swapResult?.intentExplorerURL,\n swapResult?.intentUrl,\n swapResult?.intentURL,\n swapResult?.rffUrl,\n swapResult?.rffURL,\n swapResult?.rffExplorerUrl,\n swapResult?.rffExplorerURL,\n swapResult?.explorerUrl,\n swapResult?.explorerURL,\n result?.intentExplorerUrl,\n result?.intentExplorerURL,\n result?.intentUrl,\n result?.intentURL,\n result?.rffUrl,\n result?.rffURL,\n result?.rffExplorerUrl,\n result?.rffExplorerURL,\n result?.swapResult?.intentExplorerUrl,\n result?.swapResult?.intentExplorerURL,\n result?.swapResult?.intentUrl,\n result?.swapResult?.intentURL,\n result?.swapResult?.rffUrl,\n result?.swapResult?.rffURL,\n result?.swapResult?.rffExplorerUrl,\n result?.swapResult?.rffExplorerURL,\n result?.swapResult?.explorerUrl,\n result?.swapResult?.explorerURL,\n result?.result?.intentExplorerUrl,\n result?.result?.intentExplorerURL,\n result?.result?.intentUrl,\n result?.result?.intentURL,\n result?.result?.rffUrl,\n result?.result?.rffURL,\n result?.result?.rffExplorerUrl,\n result?.result?.rffExplorerURL,\n result?.result?.explorerUrl,\n result?.result?.explorerURL\n );\n\nconst getSdkIntentExplorerUrlForNetwork = (\n network: unknown,\n result: any,\n swapResult?: any\n) =>\n getSdkIntentExplorerUrl(result, swapResult) ||\n getRffExplorerUrl(\n network,\n getObjectIntentHash(swapResult) || getObjectIntentHash(result)\n );\n\nfunction MiniLogo({\n src,\n label,\n size = 30,\n fontSize = 13,\n outline,\n style,\n}: {\n src?: string;\n label?: string;\n size?: number;\n fontSize?: number;\n outline?: string;\n style?: React.CSSProperties;\n}) {\n const [failed, setFailed] = useState(!src);\n\n useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n background: \"#FFFFFE\",\n borderRadius: \"999px\",\n height: size,\n objectFit: \"cover\",\n outline,\n width: size,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {(label || \"?\").trim().slice(0, 1).toUpperCase()}\n \n );\n}\n\nfunction TokenLogoPair({\n tokenLogo,\n chainLogo,\n tokenSymbol,\n chainName,\n tokenOutline,\n size = 34,\n}: {\n tokenLogo?: string;\n chainLogo?: string;\n tokenSymbol?: string;\n chainName?: string;\n tokenOutline?: string;\n size?: number;\n}) {\n return (\n \n \n {chainLogo && (\n \n )}\n \n );\n}\n\nfunction SourceLogoStack({\n sources,\n size = 24,\n maxVisible = 3,\n}: {\n sources: HistorySourceRow[];\n size?: number;\n maxVisible?: number;\n}) {\n const visibleSources = sources.slice(0, maxVisible);\n const hiddenCount = Math.max(0, sources.length - visibleSources.length);\n\n return (\n \n {visibleSources.map((source, index) => (\n \n \n \n ))}\n {hiddenCount > 0 && (\n \n +{hiddenCount}\n \n )}\n \n );\n}\n\nfunction TruncatedAddress({\n address,\n color = \"var(--foreground-brand)\",\n}: {\n address: string;\n color?: string;\n}) {\n const [showTooltip, setShowTooltip] = useState(false);\n const label =\n address.length > 12\n ? `${address.slice(0, 6)}...${address.slice(-4)}`\n : address;\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n style={{\n color,\n display: \"inline-flex\",\n fontFamily: uiFont,\n fontSize: \"15px\",\n fontWeight: 500,\n lineHeight: \"20px\",\n outline: \"none\",\n position: \"relative\",\n }}\n tabIndex={0}\n >\n {label}\n {showTooltip && (\n \n {address}\n \n )}\n \n );\n}\n\nconst getDisplayDestinationSourceRow = (\n entry: SwapHistoryEntry\n): HistorySourceRow | null => {\n if (entry.mode !== \"deposit\" && entry.mode !== \"send\") return null;\n if (!entry.toToken || !entry.requestedToAmount) return null;\n\n const requestedAmount = parseDecimalLoose(entry.requestedToAmount);\n const intentDestinationAmount = parseDecimalLoose(\n entry.intentData?.destination.amount\n );\n const destinationBalanceAmount = parseDecimalLoose(\n entry.toToken.balance?.replace(entry.toToken.symbol, \"\")\n );\n if (\n !requestedAmount ||\n !destinationBalanceAmount ||\n requestedAmount.lte(0) ||\n destinationBalanceAmount.lte(0)\n ) {\n return null;\n }\n\n const intentCoversAmount = intentDestinationAmount ?? new Decimal(0);\n const displayAmount = Decimal.min(\n destinationBalanceAmount,\n Decimal.max(0, requestedAmount.minus(intentCoversAmount))\n );\n if (displayAmount.lte(0)) return null;\n\n const requestedValue = parseDecimalLoose(entry.requestedToValue);\n const destinationValue = parseDecimalLoose(\n entry.intentData?.destination.value\n );\n const rate =\n requestedValue && requestedAmount.gt(0)\n ? requestedValue.div(requestedAmount)\n : destinationValue && intentCoversAmount.gt(0)\n ? destinationValue.div(intentCoversAmount)\n : undefined;\n\n return {\n key: `destination-balance-${entry.toToken.chainId}-${entry.toToken.contractAddress}`,\n chainId: entry.toToken.chainId,\n contractAddress: entry.toToken.contractAddress,\n tokenLogo: entry.intentData?.destination.token.logo || entry.toToken.logo,\n chainLogo: entry.toToken.chainLogo,\n symbol: entry.toToken.symbol,\n chainName: getShortChainName(\n entry.toToken.chainId,\n entry.toToken.chainName\n ),\n amount: displayAmount\n .toDecimalPlaces(\n Math.max(0, entry.toToken.decimals ?? 18),\n Decimal.ROUND_DOWN\n )\n .toFixed(),\n value: rate\n ? displayAmount.mul(rate).toFixed()\n : entry.toToken.balanceInFiat,\n };\n};\n\nconst getHistorySourceRowMergeKey = (row: HistorySourceRow) => {\n const chainKey = row.chainId ?? row.chainName;\n const address = row.contractAddress?.toLowerCase();\n return address\n ? `${chainKey}:${address}`\n : `${chainKey}:symbol:${row.symbol.toUpperCase()}`;\n};\n\nconst mergeHistorySourceRows = (rows: HistorySourceRow[]) => {\n const merged = new Map();\n\n for (const row of rows) {\n const key = getHistorySourceRowMergeKey(row);\n const existing = merged.get(key);\n if (!existing) {\n merged.set(key, row);\n continue;\n }\n\n merged.set(key, {\n ...existing,\n amount: sumDecimalStrings(existing.amount, row.amount) ?? existing.amount,\n chainLogo: existing.chainLogo || row.chainLogo,\n key: existing.key,\n tokenLogo: existing.tokenLogo || row.tokenLogo,\n value: sumDecimalStrings(\n String(existing.value ?? \"\"),\n String(row.value ?? \"\")\n ),\n });\n }\n\n return Array.from(merged.values());\n};\n\nconst getProgressStepType = (step?: SwapStepType | BridgeStepType | null) =>\n String((step as any)?.type ?? (step as any)?.typeID ?? \"\").toUpperCase();\n\nconst isSourceMovementProgressStep = (type: string, rawStepType: string) =>\n rawStepType === \"source_swap\" ||\n rawStepType === \"eoa_to_ephemeral_transfer\" ||\n rawStepType === \"bridge_deposit\" ||\n rawStepType === \"vault_deposit\" ||\n type.includes(\"SOURCE_SWAP\") ||\n type.includes(\"EOA_TO_EPHEMERAL_TRANSFER\") ||\n type.includes(\"BRIDGE_DEPOSIT\") ||\n type.includes(\"VAULT_DEPOSIT\");\n\nconst isBridgeRefundStepType = (type: string) =>\n type.includes(\"BRIDGE_INTENT_SUBMISSION\") || type.includes(\"BRIDGE_DEPOSIT\");\n\nconst isSwapSkippedStepType = (type: string) => type.includes(\"SWAP_SKIPPED\");\n\nconst isAutoRefundAvailableProgressEvent = (event?: NexusWidgetProgressEvent) =>\n event?.name === PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS &&\n isBridgeRefundStepType(getProgressStepType(event.step));\n\nconst normalizeBridgeProvider = (\n value: unknown\n): BridgeProvider | undefined => {\n if (value === \"nexus\" || value === \"mayan\" || value === null) {\n return value;\n }\n return undefined;\n};\n\nconst normalizePlanStepType = (stepType: unknown, state?: unknown) => {\n const normalized = String(stepType ?? \"\").toLowerCase();\n const normalizedState = String(state ?? \"\").toLowerCase();\n\n if (normalized === \"execute_transaction\") {\n return normalizedState === \"confirmed\" || normalizedState === \"completed\"\n ? \"TRANSACTION_CONFIRMED\"\n : \"TRANSACTION_SENT\";\n }\n\n const mapped: Record = {\n allowance_approval: \"APPROVAL\",\n bridge_deposit: \"BRIDGE_DEPOSIT\",\n bridge_fill: \"BRIDGE_FILL\",\n bridge_intent_submission: \"BRIDGE_INTENT_SUBMISSION\",\n destination_swap: \"DESTINATION_SWAP\",\n eoa_to_ephemeral_transfer: \"EOA_TO_EPHEMERAL_TRANSFER\",\n execute_approval: \"APPROVAL\",\n request_signing: \"REQUEST_SIGNING\",\n request_submission: \"REQUEST_SUBMISSION\",\n source_swap: \"SOURCE_SWAP\",\n vault_deposit: \"BRIDGE_DEPOSIT\",\n };\n\n return mapped[normalized] ?? normalized.toUpperCase();\n};\n\nconst normalizePlanStep = (\n stepLike: unknown,\n fallbackStepType?: unknown,\n state?: unknown,\n completed?: boolean\n): SwapStepType | BridgeStepType => {\n const source =\n stepLike && typeof stepLike === \"object\" ? (stepLike as any) : {};\n const rawStepType = fallbackStepType ?? source.stepType ?? source.type;\n const progressType = normalizePlanStepType(\n rawStepType ?? source.typeID,\n state\n );\n const progressKey =\n source.id ?? source.stepId ?? source.typeID ?? progressType;\n\n return {\n ...source,\n completed,\n rawType: rawStepType,\n type: progressType,\n typeID: String(progressKey),\n } as SwapStepType | BridgeStepType;\n};\n\nconst getPlanStepChainId = (event: any, step: any) =>\n getObjectChainId(event) ?? getObjectChainId(step);\n\nconst getPlanStepTransactionHash = (event: any, step: any) =>\n getObjectTransactionHash(event) ?? getObjectTransactionHash(step);\n\nconst getPlanStepExplorerUrl = (event: any, step: any) => {\n const directExplorerUrl = getNonEmptyString(\n event?.explorerUrl,\n event?.explorerURL,\n event?.txExplorerUrl,\n event?.transactionExplorerUrl,\n step?.explorerUrl,\n step?.explorerURL,\n step?.txExplorerUrl,\n step?.transactionExplorerUrl,\n step?.data?.explorerUrl,\n step?.data?.explorerURL,\n step?.data?.txExplorerUrl,\n step?.data?.transactionExplorerUrl\n );\n if (directExplorerUrl) return directExplorerUrl;\n\n return getExplorerTxUrl(\n getPlanStepChainId(event, step),\n getPlanStepTransactionHash(event, step),\n event,\n step\n );\n};\n\nconst getPlanStepIntentExplorerUrl = (event: any, step: any) =>\n getNonEmptyString(\n event?.intentExplorerUrl,\n event?.intentExplorerURL,\n event?.intentUrl,\n event?.intentURL,\n event?.rffUrl,\n event?.rffURL,\n event?.rffExplorerUrl,\n event?.rffExplorerURL,\n step?.intentExplorerUrl,\n step?.intentExplorerURL,\n step?.intentUrl,\n step?.intentURL,\n step?.rffUrl,\n step?.rffURL,\n step?.rffExplorerUrl,\n step?.rffExplorerURL,\n step?.data?.intentExplorerUrl,\n step?.data?.intentExplorerURL,\n step?.data?.intentUrl,\n step?.data?.intentURL,\n step?.data?.rffUrl,\n step?.data?.rffURL,\n step?.data?.rffExplorerUrl,\n step?.data?.rffExplorerURL\n );\n\nconst isIntentSubmissionLikeEvent = (event: any, step?: any) => {\n const text = [\n event?.type,\n event?.event,\n event?.name,\n event?.status,\n event?.stepType,\n step?.type,\n step?.typeID,\n step?.rawType,\n ]\n .filter((value) => value !== null && value !== undefined)\n .join(\" \")\n .toLowerCase();\n\n return (\n text.includes(\"intent\") ||\n text.includes(\"request_submission\") ||\n text.includes(\"rff\")\n );\n};\n\nconst getGenericEventHash = (event: any, step?: any) =>\n getTransactionHash(\n event?.hash,\n event?.data?.hash,\n event?.result?.hash,\n step?.hash,\n step?.data?.hash,\n step?.result?.hash\n );\n\nconst getEventIntentExplorerUrl = (\n network: unknown,\n event: any,\n step?: any\n) => {\n const directUrl = step\n ? getPlanStepIntentExplorerUrl(event, step)\n : getNonEmptyString(\n event?.intentExplorerUrl,\n event?.intentExplorerURL,\n event?.intentUrl,\n event?.intentURL,\n event?.rffUrl,\n event?.rffURL,\n event?.rffExplorerUrl,\n event?.rffExplorerURL,\n event?.data?.intentExplorerUrl,\n event?.data?.intentExplorerURL,\n event?.data?.intentUrl,\n event?.data?.intentURL,\n event?.data?.rffUrl,\n event?.data?.rffURL,\n event?.data?.rffExplorerUrl,\n event?.data?.rffExplorerURL\n );\n if (directUrl) return directUrl;\n\n return getRffExplorerUrl(\n network,\n getObjectIntentHash(event) ||\n getObjectIntentHash(step) ||\n (isIntentSubmissionLikeEvent(event, step)\n ? getGenericEventHash(event, step)\n : null)\n );\n};\n\nconst isTimeoutLikeError = (error: unknown) => {\n const err = error as {\n code?: unknown;\n name?: unknown;\n message?: unknown;\n shortMessage?: unknown;\n };\n const text = [\n err?.code,\n err?.name,\n err?.message,\n err?.shortMessage,\n typeof error === \"string\" ? error : undefined,\n ]\n .filter((value) => value !== null && value !== undefined)\n .join(\" \");\n\n return /timeout|timed out|time out|deadline exceeded|expired while waiting|wait.*expired|poll.*expired/i.test(\n text\n );\n};\n\nconst getSdkEventType = (event: any) =>\n event?.type ?? event?.name ?? event?.event ?? \"unknown\";\n\nconst summarizeSdkProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n index?: number\n) => ({\n completed: (step as any)?.completed,\n index,\n rawType: (step as any)?.rawType ?? (step as any)?.stepType,\n state: (step as any)?.state,\n type: getProgressStepType(step),\n});\n\nconst logSdkSwapEvent = (\n label: string,\n event: any,\n meta?: Record\n) => {\n console.log(`[NexusWidget SDK][swap] ${label}`, {\n event,\n eventType: getSdkEventType(event),\n ...meta,\n });\n};\n\nconst logSdkIntentEvent = (\n label: string,\n data: any,\n meta?: Record\n) => {\n console.log(`[NexusWidget SDK][intent] ${label}`, {\n hasAllow: typeof data?.allow === \"function\",\n hasDeny: typeof data?.deny === \"function\",\n hasRefresh: typeof data?.refresh === \"function\",\n intent: data?.intent,\n raw: data,\n ...meta,\n });\n};\n\nconst logSdkIntentInput = (\n operation: string,\n input: unknown,\n meta?: Record\n) => {\n console.log(`[NexusWidget SDK][intent input] ${operation}`, {\n input,\n ...meta,\n });\n};\n\nconst normalizeSdkIntentString = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n return String(value);\n};\n\nconst normalizeSdkIntentAmount = (\n value: unknown,\n fallback: string = \"0\"\n) => normalizeSdkIntentString(value) ?? fallback;\n\nconst getLogoFromMetadata = (metadata: any) =>\n metadata?.logo ??\n metadata?.logoURI ??\n metadata?.logoUri ??\n metadata?.logoUrl ??\n metadata?.icon ??\n \"\";\n\nconst normalizeSdkIntentChain = (chain: any) => {\n const id = Number(chain?.id ?? chain?.chainId);\n if (!Number.isFinite(id)) return undefined;\n const chainMeta = CHAIN_METADATA[id];\n return {\n id,\n logo: getLogoFromMetadata(chain) || chainMeta?.logo || \"\",\n name: chain?.name ?? chainMeta?.name ?? \"\",\n };\n};\n\nconst normalizeSdkIntentToken = (token: any, chainId?: number) => {\n const chainMeta = chainId ? CHAIN_METADATA[chainId] : undefined;\n const decimals = Number(token?.decimals ?? chainMeta?.nativeCurrency.decimals);\n return {\n contractAddress:\n token?.contractAddress ??\n token?.address ??\n token?.tokenAddress ??\n zeroAddress,\n decimals: Number.isFinite(decimals) ? decimals : 18,\n logo: getLogoFromMetadata(token) || undefined,\n symbol: token?.symbol ?? token?.tokenSymbol ?? \"\",\n };\n};\n\nconst normalizeSdkIntentGas = (\n gas: any,\n chainId?: number\n): SwapIntentDestination[\"gas\"] => ({\n amount: normalizeSdkIntentAmount(gas?.amount),\n value: normalizeSdkIntentString(gas?.value),\n token: normalizeSdkIntentToken(gas?.token ?? gas, chainId),\n});\n\nconst normalizeSdkIntentSource = (\n source: any\n): SwapIntentSource | undefined => {\n const chain = normalizeSdkIntentChain(source?.chain);\n if (!chain) return undefined;\n return {\n ...source,\n amount: normalizeSdkIntentAmount(source?.amount),\n chain,\n token: normalizeSdkIntentToken(source?.token, chain.id),\n value: normalizeSdkIntentString(source?.value),\n };\n};\n\nconst normalizeSdkIntentDestination = (\n destination: any\n): SwapIntentDestination | undefined => {\n const chain = normalizeSdkIntentChain(destination?.chain);\n if (!chain) return undefined;\n return {\n ...destination,\n amount: normalizeSdkIntentAmount(destination?.amount),\n chain,\n gas: normalizeSdkIntentGas(destination?.gas, chain.id),\n token: normalizeSdkIntentToken(destination?.token, chain.id),\n value: normalizeSdkIntentString(destination?.value),\n };\n};\n\nconst normalizeSwapIntentData = (intent: any): SwapIntentData | null => {\n const destination = normalizeSdkIntentDestination(intent?.destination);\n if (!destination) return null;\n\n return {\n ...intent,\n destination,\n sources: Array.isArray(intent?.sources)\n ? intent.sources\n .map(normalizeSdkIntentSource)\n .filter(\n (source: SwapIntentSource | undefined): source is SwapIntentSource =>\n Boolean(source)\n )\n : [],\n };\n};\n\nconst normalizeSwapAndExecuteRequirementIntent = (\n intent: any\n): SwapIntentData | null => {\n const requirement = intent?.executeRequirement ?? intent?.executionRequirement;\n if (!requirement) return null;\n const destination = normalizeSdkIntentDestination({\n amount: requirement?.token?.amount,\n chain: requirement?.chain,\n gas: requirement?.gas,\n token: requirement?.token,\n value: requirement?.token?.value,\n });\n if (!destination) return null;\n\n return {\n ...intent,\n bridgeProvider: normalizeBridgeProvider(intent?.bridgeProvider),\n destination,\n feesAndBuffer: intent?.feesAndBuffer,\n sources: [],\n };\n};\n\nconst normalizeRenderableSwapIntentData = (\n rawIntent: any,\n bridgeProvider?: BridgeProvider\n): SwapIntentData | null => {\n const direct = normalizeSwapIntentData(rawIntent);\n const normalizedIntent = direct\n ? null\n : normalizeSwapIntentData(rawIntent?.normalizedIntent);\n const nestedSwap =\n direct || normalizedIntent ? null : normalizeSwapIntentData(rawIntent?.swap);\n const requirement = direct || normalizedIntent || nestedSwap\n ? null\n : normalizeSwapAndExecuteRequirementIntent(rawIntent);\n const normalized = direct ?? normalizedIntent ?? nestedSwap ?? requirement;\n if (!normalized) return null;\n\n return bridgeProvider === undefined\n ? normalized\n : { ...normalized, bridgeProvider };\n};\n\nconst logSwapPlanSteps = (\n eventType: \"plan_preview\" | \"plan_confirmed\",\n stepList: Array,\n rawSteps: unknown\n) => {\n console.log(`[NexusWidget SDK][swap] ${eventType} step list`, {\n count: stepList.length,\n eventType,\n rawSteps,\n steps: stepList.map((step, index) => summarizeSdkProgressStep(step, index)),\n });\n};\n\nconst logSwapPlanProgress = (\n event: any,\n step: SwapStepType | BridgeStepType,\n eventName: string,\n completed: boolean\n) => {\n console.log(\"[NexusWidget SDK][swap] plan_progress\", {\n completed,\n eventName,\n eventType: getSdkEventType(event),\n normalizedStep: summarizeSdkProgressStep(step),\n rawEvent: event,\n rawStep: event?.step,\n state: event?.state,\n stepType: event?.stepType,\n });\n};\n\nconst getFailureMessageForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n mode: NexusWidgetMode,\n autoRefundAvailable = false\n) => {\n if (autoRefundAvailable) {\n return \"Swap Failed. Refund Initiated\";\n }\n\n const type = getProgressStepType(step);\n if (\n type.includes(\"CREATE_PERMIT_FOR_SOURCE_SWAP\") ||\n type.includes(\"CREATE_PERMIT_EOA_TO_EPHEMERAL\") ||\n type.includes(\"EOA_EXECUTE_CALL\") ||\n type.includes(\"SOURCE_SWAP\") ||\n type.includes(\"COLLECTION\")\n ) {\n return \"Collection Failed\";\n }\n if (type.includes(\"DESTINATION_SWAP\") || type.includes(\"FULFIL\")) {\n return \"Destination Swap Failed\";\n }\n if (\n type.includes(\"TRANSACTION\") ||\n type.includes(\"APPROVAL\") ||\n type.includes(\"DEPOSIT\")\n ) {\n return mode === \"send\"\n ? \"Send failed. Funds are in your wallet\"\n : mode === \"deposit\"\n ? \"Deposit failed. Funds are in your wallet\"\n : \"Swap Failed\";\n }\n if (\n type.includes(\"SWAP\") ||\n type.includes(\"BRIDGE\") ||\n type.includes(\"INTENT\") ||\n type.includes(\"DETERMINING\")\n ) {\n return \"Swap Failed\";\n }\n return mode === \"send\"\n ? \"Send failed. Funds are in your wallet\"\n : mode === \"deposit\"\n ? \"Deposit failed. Funds are in your wallet\"\n : \"Swap Failed\";\n};\n\nconst getBridgeTokenSymbolForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined\n) => {\n const rawStep = step as any;\n return (\n getNonEmptyString(\n rawStep?.bridgeToken?.symbol,\n rawStep?.data?.bridgeToken?.symbol,\n rawStep?.bridgeTokenSymbol,\n rawStep?.data?.bridgeTokenSymbol,\n rawStep?.swaps?.[0]?.input?.symbol,\n rawStep?.data?.swaps?.[0]?.input?.symbol,\n rawStep?.input?.symbol,\n rawStep?.data?.input?.symbol,\n rawStep?.asset?.symbol,\n rawStep?.data?.asset?.symbol\n ) ?? \"USDC\"\n );\n};\n\nconst getFailureDescriptionForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n autoRefundAvailable = false\n) => {\n if (autoRefundAvailable) return undefined;\n const type = getProgressStepType(step);\n if (!type.includes(\"DESTINATION_SWAP\")) return undefined;\n const bridgeTokenSymbol = getBridgeTokenSymbolForProgressStep(step);\n return `${bridgeTokenSymbol} has been bridged and you have those funds in your wallet.`;\n};\n\nconst getSourceRows = (entry: SwapHistoryEntry): HistorySourceRow[] => {\n const sources = entry.intentData?.sources ?? [];\n const displayDestinationSourceRow = getDisplayDestinationSourceRow(entry);\n if (sources.length > 0) {\n const sourceRows = sources.map((source, index) => {\n const fallback = entry.fromTokens.find(\n (token) =>\n token.chainId === source.chain.id &&\n (token.contractAddress?.toLowerCase() ===\n source.token.contractAddress?.toLowerCase() ||\n token.symbol === source.token.symbol)\n );\n\n return {\n key: `${source.chain.id}-${source.token.contractAddress}-${index}`,\n chainId: source.chain.id,\n contractAddress: source.token.contractAddress,\n tokenLogo: source.token.logo || fallback?.logo,\n chainLogo: source.chain.logo || fallback?.chainLogo,\n symbol: source.token.symbol,\n chainName: getShortChainName(source.chain.id, source.chain.name),\n amount: source.amount,\n value: source.value,\n };\n });\n\n return mergeHistorySourceRows(\n displayDestinationSourceRow\n ? [displayDestinationSourceRow, ...sourceRows]\n : sourceRows\n );\n }\n\n const fallbackRows = entry.fromTokens.map((token, index) => ({\n key: `${token.chainId}-${token.contractAddress}-${index}`,\n chainId: token.chainId,\n contractAddress: token.contractAddress,\n tokenLogo: token.logo,\n chainLogo: token.chainLogo,\n symbol: token.symbol,\n chainName: getShortChainName(token.chainId, token.chainName),\n amount: token.userAmount || \"0\",\n value: token.balanceInFiat,\n }));\n\n return mergeHistorySourceRows(\n displayDestinationSourceRow\n ? [displayDestinationSourceRow, ...fallbackRows]\n : fallbackRows\n );\n};\n\nfunction SourceRowsList({\n entry,\n maxHeight = 236,\n borderTopFirst = true,\n scrollAfterRows = 4,\n}: {\n entry: SwapHistoryEntry;\n maxHeight?: number;\n borderTopFirst?: boolean;\n scrollAfterRows?: number;\n}) {\n const rows = getSourceRows(entry);\n const shouldScroll = rows.length > scrollAfterRows;\n const scrollRef = useRef(null);\n\n return (\n
\n \n {rows.map((row, index) => (\n 0 ? \"1px solid #E8E8E7\" : \"none\",\n display: \"flex\",\n justifyContent: \"space-between\",\n minHeight: \"64px\",\n padding: \"10px 20px\",\n }}\n >\n \n \n
\n \n {row.symbol}\n \n \n on {row.chainName || \"Unknown chain\"}\n \n
\n
\n \n \n {formatTokenDisplay(row.amount)} {row.symbol}\n \n \n {formatUsdDisplay(row.value)}\n \n \n \n ))}\n \n {shouldScroll && (\n \n scrollRef.current?.scrollBy({ top: 72, behavior: \"smooth\" })\n }\n style={{\n alignItems: \"center\",\n background: \"#FFFFFE\",\n border: \"1px solid #E8E8E7\",\n borderRadius: \"999px\",\n bottom: \"6px\",\n boxShadow: \"0 2px 8px rgba(22,22,21,0.08)\",\n display: \"flex\",\n height: \"22px\",\n justifyContent: \"center\",\n left: \"50%\",\n padding: 0,\n position: \"absolute\",\n transform: \"translateX(-50%)\",\n width: \"22px\",\n }}\n type=\"button\"\n >\n \n \n )}\n \n );\n}\n\nfunction SwapReceiptPanel({\n entry,\n onDone,\n}: {\n entry: SwapHistoryEntry;\n onDone: () => void;\n}) {\n const [showSourceDetails, setShowSourceDetails] = useState(false);\n const destination = entry.intentData?.destination;\n const isFailed = entry.status === \"failed\";\n const isTimeout = entry.status === \"timeout\";\n const isDeposit = entry.mode === \"deposit\";\n const isSend = entry.mode === \"send\";\n const isRecipientTransfer = isSend || Boolean(entry.recipientAddress);\n const tokenSymbol = destination?.token.symbol || entry.toToken?.symbol || \"\";\n const chainName = getShortChainName(\n destination?.chain.id ?? entry.toToken?.chainId,\n destination?.chain.name || entry.toToken?.chainName || \"\"\n );\n const depositVenue =\n entry.opportunity?.title || entry.opportunity?.protocol || chainName;\n const amount = destination?.amount || \"\";\n const destinationLogo =\n destination?.token.logo ||\n entry.toToken?.logo ||\n entry.opportunity?.tokenLogo ||\n (isDeposit ? entry.opportunity?.logo : undefined);\n const requestedExactOutAmount =\n (isDeposit || isSend) && entry.requestedToAmount\n ? entry.requestedToAmount\n : undefined;\n const requestedExactOutValue =\n (isDeposit || isSend) && entry.requestedToValue\n ? entry.requestedToValue\n : undefined;\n const value = requestedExactOutValue || destination?.value;\n const displayAmount = requestedExactOutAmount || amount;\n const showIntentExplorer = hasValidIntentExplorer(entry);\n const intentLabel = entry.intentId\n ? `Intent #${entry.intentId}`\n : \"View Explorer\";\n const sourceRows = getSourceRows(entry);\n const sourceCount = sourceRows.length;\n const sourceTotalUsd = sourceRows.reduce(\n (sum, source) => sum.plus(parseDecimalLoose(source.value) ?? 0),\n new Decimal(0)\n );\n const defaultSwapFailureHeadline = entry.autoRefundAvailable\n ? \"Swap Failed. Refund Initiated\"\n : \"Swap Failed\";\n const entryFailureMessage =\n entry.status === \"timeout\" ? TIMEOUT_LABEL : entry.failureMessage;\n const storedFailureMessage =\n !entry.autoRefundAvailable && entryFailureMessage?.includes(\"Refund\")\n ? undefined\n : entryFailureMessage;\n const failureHeadline =\n storedFailureMessage ||\n (isDeposit\n ? \"Deposit failed. Funds are in your wallet\"\n : isRecipientTransfer\n ? \"Send failed. Funds are in your wallet\"\n : defaultSwapFailureHeadline);\n const failureDescription = isFailed ? entry.failureDescription : undefined;\n const timeoutHeadline = TIMEOUT_LABEL;\n const timeoutDescription = isTimeout\n ? entry.failureDescription ||\n \"This transaction is still pending. Check the intent explorer for the latest status.\"\n : undefined;\n const receiptLocation = isDeposit ? depositVenue : chainName;\n const receiptSummary = receiptLocation ? `on ${receiptLocation}` : \"\";\n\n return (\n \n \n \n \n \n {isFailed ? \"x\" : isTimeout ? \"!\" : \"✓\"}\n \n \n
\n {isTimeout\n ? timeoutHeadline\n : isFailed\n ? failureHeadline\n : isDeposit\n ? \"You deposited\"\n : isRecipientTransfer\n ? \"You sent\"\n : \"You received\"}\n
\n {(failureDescription || timeoutDescription) && (\n \n {failureDescription || timeoutDescription}\n \n )}\n \n {displayAmount ? formatTokenDisplay(displayAmount) : \"--\"}\n \n {tokenSymbol}\n \n \n
\n ≈ {formatUsdDisplay(value)}\n
\n {receiptSummary && (\n \n {receiptSummary}\n \n )}\n \n\n \n \n \n {isDeposit || isSend ? \"You Paid\" : \"You Swapped\"}\n \n \n \n {formatUsdDisplay(sourceTotalUsd)}\n \n setShowSourceDetails((current) => !current)}\n style={{\n alignItems: \"center\",\n background: \"transparent\",\n border: \"none\",\n color: \"var(--foreground-brand)\",\n cursor: \"pointer\",\n display: \"inline-flex\",\n fontFamily: uiFont,\n fontSize: \"12px\",\n gap: \"4px\",\n padding: 0,\n }}\n type=\"button\"\n >\n {showSourceDetails\n ? \"Hide Details\"\n : `${sourceCount} asset${sourceCount === 1 ? \"\" : \"s\"}`}\n \n \n \n \n \n
\n \n
\n \n {isRecipientTransfer && entry.recipientAddress && (\n \n \n Recipient\n \n \n \n )}\n {showIntentExplorer && (\n \n \n Intent Explorer\n \n \n {intentLabel} ↗\n \n \n )}\n {entry.finalExplorerUrl && (\n \n \n Final Transaction\n \n \n View Explorer ↗\n \n \n )}\n \n \n Total Fees\n \n \n {formatUsdDisplay(entry.feeUsd)}\n \n \n \n\n \n Done\n \n \n );\n}\n\nconst getRelativeTime = (time: number, now: number) => {\n const seconds = Math.max(1, Math.floor((now - time) / 1000));\n if (seconds < 60) return `${seconds} second${seconds === 1 ? \"\" : \"s\"} ago`;\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes} minute${minutes === 1 ? \"\" : \"s\"} ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours} hour${hours === 1 ? \"\" : \"s\"} ago`;\n const days = Math.floor(hours / 24);\n return `${days} day${days === 1 ? \"\" : \"s\"} ago`;\n};\n\nfunction HistoryStatusPill({ status }: { status: SwapHistoryStatus }) {\n const config =\n status === \"fulfilled\"\n ? { label: \"Fulfilled\", bg: \"#E8F6EF\", fg: \"#168A47\" }\n : status === \"pending\"\n ? { label: \"Pending\", bg: \"#FFF3DE\", fg: \"#B7791F\" }\n : status === \"timeout\"\n ? { label: TIMEOUT_LABEL, bg: \"#FFF3DE\", fg: \"#B7791F\" }\n : status === \"refund-initiated\"\n ? { label: \"Refund Initiated\", bg: \"#FFF3DE\", fg: \"#B7791F\" }\n : { label: \"Failed\", bg: \"#FFE6EA\", fg: \"#E92C2C\" };\n\n return (\n \n {config.label}\n \n );\n}\n\nfunction SwapHistoryPanel({\n entries,\n now,\n}: {\n entries: SwapHistoryEntry[];\n now: number;\n}) {\n if (entries.length === 0) {\n return (\n \n \n \n ↻\n \n \n \n No transactions yet\n \n \n Your transaction history will appear here once you make your first\n swap, deposit, or send.\n \n \n );\n }\n\n const sortedEntries = sortSwapHistoryEntries(entries);\n const shouldScroll = sortedEntries.length > 5;\n\n return (\n \n {sortedEntries.map((entry) => {\n const destination = entry.intentData?.destination;\n const destinationLogo =\n destination?.token.logo ||\n entry.toToken?.logo ||\n entry.opportunity?.tokenLogo ||\n \"\";\n const destinationChainLogo =\n destination?.chain.logo || entry.toToken?.chainLogo || \"\";\n const destinationChainName = getShortChainName(\n destination?.chain.id ?? entry.toToken?.chainId,\n destination?.chain.name || entry.toToken?.chainName || \"\"\n );\n const destinationSymbol =\n destination?.token.symbol || entry.toToken?.symbol || \"\";\n const destinationValue =\n (entry.mode === \"deposit\" || entry.mode === \"send\") &&\n entry.requestedToValue\n ? entry.requestedToValue\n : destination?.value;\n const destinationAmount =\n (entry.mode === \"deposit\" || entry.mode === \"send\") &&\n entry.requestedToAmount\n ? entry.requestedToAmount\n : destination?.amount || \"\";\n const canShowRefund =\n entry.status === \"failed\" && Boolean(entry.autoRefundAvailable);\n const status = canShowRefund ? \"refund-initiated\" : entry.status;\n const sourceRows = getSourceRows(entry);\n const historyExplorerUrl = getHistoryExplorerUrl(entry);\n\n return (\n \n \n \n \n
\n \n {destinationAmount\n ? formatTokenDisplay(destinationAmount)\n : \"--\"}\n \n {destinationSymbol}\n \n
\n \n ≈ {formatUsdDisplay(destinationValue)}\n \n \n \n \n \n \n {getRelativeTime(entry.createdAt ?? entry.startedAt, now)}\n \n \n \n\n {canShowRefund && (\n \n \n Refund Initiated\n \n \n )}\n\n \n \n {sourceRows.length > 0 && (\n \n )}\n \n →\n \n \n \n {historyExplorerUrl && (\n \n \n View\n \n \n \n \n \n \n )}\n \n \n );\n })}\n \n );\n}\n\n// ---------------------------------------------------------------------------\n// NexusWidget\n// ---------------------------------------------------------------------------\n\nexport function NexusWidget(props: NexusWidgetProps) {\n return (\n \n
\n Something went wrong\n
\n \n An unexpected error occurred. Please refresh the page or try\n resetting the widget.\n \n window.location.reload()}\n style={{\n backgroundColor: \"var(--foreground-brand)\",\n border: \"none\",\n borderRadius: \"8px\",\n color: \"#FFFFFE\",\n cursor: \"pointer\",\n fontSize: \"15px\",\n fontWeight: 500,\n padding: \"8px 16px\",\n transition: \"background-color 0.15s ease-out\",\n }}\n type=\"button\"\n >\n Reload Page\n \n \n }\n >\n \n \n );\n}\n\nfunction NexusWidgetInner({\n config: rawConfig,\n embed = true,\n className,\n connectedAddress,\n open: controlledOpen,\n onOpenChange,\n defaultOpen = false,\n onComplete,\n onConnectClick,\n onStart,\n onError,\n onReceiveAssetChange,\n onClose,\n onConnectWallet,\n}: NexusWidgetProps) {\n const {\n nexusSDK,\n bridgableBalance,\n swapBalance,\n swapBalanceLoading,\n getFiatValue,\n resolveTokenUsdRate,\n swapSupportedChainsAndTokens,\n supportedChainsAndTokens,\n fetchSwapBalance,\n handleInit,\n swapIntent: providerSwapIntent,\n network,\n loading: nexusLoading,\n } = useNexus();\n const appConfig = useMemo(\n () => ({\n nexusNetwork: network ?? \"mainnet\",\n }),\n [network]\n );\n const isSwapBalancePending =\n Boolean(nexusSDK) && (swapBalanceLoading || swapBalance === null);\n\n const normalizedWidgetConfig = useMemo(\n () => normalizeNexusWidgetConfig(rawConfig),\n [rawConfig]\n );\n const config = normalizedWidgetConfig.config;\n const activeMode = normalizedWidgetConfig.activeMode;\n const amountInputConfig = normalizedWidgetConfig.amountInput;\n const appearanceConfig = normalizedWidgetConfig.appearance;\n const configuredDepositOptions = normalizedWidgetConfig.depositOptions;\n const isConfiguredAmountFixed = normalizedWidgetConfig.isAmountFixed;\n const isConfiguredRecipientLocked =\n normalizedWidgetConfig.isRecipientLocked;\n const primaryColor = normalizeNexusWidgetPrimaryColor(\n appearanceConfig?.primaryColor\n );\n const primaryButtonBackground =\n primaryColor ?? NEXUS_WIDGET_DEFAULT_PRIMARY_COLOR;\n const primaryButtonForeground = getReadableTextColor(\n primaryButtonBackground\n );\n const theme = useMemo(\n () => ({\n ...nexusWidgetTheme,\n colors: {\n ...nexusWidgetTheme.colors,\n primary: \"var(--foreground-brand)\",\n primaryText: \"var(--foreground-brand)\",\n },\n }),\n [primaryColor]\n );\n const configuredDeposit = getConfiguredDeposit(config);\n const configuredDepositIdentity = getDepositConfigIdentity(configuredDeposit);\n const configuredDestinationTokenOptions = useMemo(() => {\n const destination = rawConfig.destination;\n const destinationChainId = getConfiguredDestinationChainId(destination);\n const destinationTokens =\n rawConfig.mode === \"deposit\"\n ? configuredDepositOptions.map(toSwapTokenOptionFromDepositConfig)\n : destination?.tokens && destinationChainId\n ? destination.tokens\n .map((token) =>\n toSwapTokenOptionFromConfiguredDestinationToken(\n token,\n destinationChainId\n )\n )\n .filter((token): token is SwapTokenOption => Boolean(token))\n : [];\n\n const tokensByKey = new Map();\n for (const token of destinationTokens) {\n tokensByKey.set(getTokenSelectionKey(token), token);\n }\n return Array.from(tokensByKey.values());\n }, [configuredDepositOptions, rawConfig.destination]);\n if (activeMode === \"deposit\" && !configuredDeposit) {\n throw new Error(\n \"NexusWidget deposit mode requires destination.chain, at least one destination token, depositAddress, and executeDeposit.\"\n );\n }\n const showCloseButton = !embed && Boolean(onClose);\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const isControlledOpen = controlledOpen !== undefined;\n const isModalOpen = isControlledOpen ? controlledOpen : internalOpen;\n\n // Preload receive tokens once SDK is available\n useEffect(() => {\n if (nexusSDK) {\n console.log(\n \"[preloadReceiveTokens] Calling preloadReceiveTokens from NexusWidget useEffect (nexusSDK available)\"\n );\n preloadReceiveTokens();\n }\n }, [nexusSDK]);\n\n const {\n address: accountAddress,\n connector,\n status: walletStatus,\n } = useAccount();\n const {\n connectors,\n connectAsync,\n isPending: isWalletConnectPending,\n } = useConnect();\n const { data: walletClient } = useWalletClient();\n const { data: connectorClient } = useConnectorClient();\n const publicClient = usePublicClient();\n const walletClientAddress = walletClient?.account?.address;\n const ownerAddress =\n connectedAddress &&\n isAddress(connectedAddress) &&\n connectedAddress.toLowerCase() !== zeroAddress\n ? connectedAddress\n : accountAddress &&\n isAddress(accountAddress) &&\n accountAddress.toLowerCase() !== zeroAddress\n ? accountAddress\n : walletClientAddress &&\n isAddress(walletClientAddress) &&\n walletClientAddress.toLowerCase() !== zeroAddress\n ? walletClientAddress\n : undefined;\n const getEffectiveWalletProvider = useCallback(\n async (activeConnector = connector) => {\n let connectorProvider: unknown;\n try {\n connectorProvider = await activeConnector?.getProvider();\n } catch {\n connectorProvider = undefined;\n }\n\n const connectorClientProvider = connectorClient\n ? {\n request: (args: unknown) => connectorClient.request(args as any),\n }\n : undefined;\n const walletClientProvider = walletClient\n ? {\n request: (args: unknown) => walletClient.request(args as any),\n }\n : undefined;\n const windowProvider =\n typeof window !== \"undefined\"\n ? (window as Window & { ethereum?: EthereumProvider }).ethereum\n : undefined;\n const effectiveProvider =\n connectorProvider &&\n typeof (connectorProvider as EthereumProvider).request === \"function\"\n ? (connectorProvider as EthereumProvider)\n : (connectorClientProvider ?? walletClientProvider ?? windowProvider);\n\n return effectiveProvider &&\n typeof effectiveProvider.request === \"function\"\n ? (effectiveProvider as EthereumProvider)\n : undefined;\n },\n [connector, connectorClient, walletClient]\n );\n const historyStorageKey = getSwapHistoryStorageKey(ownerAddress);\n\n useEffect(() => {\n if (walletStatus !== \"connected\" || !ownerAddress) {\n return;\n }\n\n let cancelled = false;\n void (async () => {\n const effectiveProvider = await getEffectiveWalletProvider();\n if (cancelled || !effectiveProvider) {\n return;\n }\n await handleInit(effectiveProvider, ownerAddress);\n })().catch((error) => {\n if (!cancelled) {\n console.error(\"Unable to initialize Nexus for wallet address:\", error);\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [getEffectiveWalletProvider, handleInit, ownerAddress, walletStatus]);\n\n // Global form state\n const [amount, setAmount] = useState(\"\");\n const [recipientAddress, setRecipientAddress] = useState(\"\");\n const [editingAssetIndex, setEditingAssetIndex] = useState(\n null\n );\n const [txError, setTxError] = useState(null);\n const [walletActionPending, setWalletActionPending] = useState(false);\n const defaultRecipientAddress = ownerAddress ?? \"\";\n const effectiveRecipientAddress =\n activeMode === \"swap\"\n ? recipientAddress || defaultRecipientAddress\n : recipientAddress;\n const hasSameOwnerSendRecipient =\n activeMode === \"send\" &&\n Boolean(\n ownerAddress &&\n recipientAddress &&\n isAddress(recipientAddress) &&\n recipientAddress.toLowerCase() === ownerAddress.toLowerCase()\n );\n const hasCustomSwapRecipient =\n activeMode === \"swap\" &&\n Boolean(\n recipientAddress &&\n (!defaultRecipientAddress ||\n recipientAddress.toLowerCase() !==\n defaultRecipientAddress.toLowerCase())\n );\n const transferRecipientAddress =\n activeMode === \"send\"\n ? recipientAddress\n : hasCustomSwapRecipient\n ? recipientAddress\n : undefined;\n\n // Swap-specific\n const [swapType, setSwapType] = useState(\"exactIn\");\n const [swapStep, setSwapStep] = useState(\"idle\");\n const [depositFundingStep, setDepositFundingStep] =\n useState(\"method\");\n const [depositOnrampSessionState, setDepositOnrampSessionState] = useState<\n string | null\n >(null);\n const drawerCloseTimerRef = useRef | null>(\n null\n );\n const terminalBalanceRefreshTimerRef = useRef | null>(null);\n const [closingDrawerStep, setClosingDrawerStep] = useState(\n null\n );\n const rootContentRef = useRef(null);\n const [rootContentHeight, setRootContentHeight] = useState(\n null\n );\n const rootContentHeightRef = useRef(null);\n const [hasMeasuredRootContent, setHasMeasuredRootContent] = useState(false);\n const [shouldAnimateRootHeight, setShouldAnimateRootHeight] = useState(false);\n const [isPreviewTransitioning, setIsPreviewTransitioning] = useState(false);\n const rootHeightTransitionTimerRef = useRef | null>(null);\n const [fromTokens, setFromTokens] = useState([]);\n const [sourceSelectionTouched, setSourceSelectionTouched] = useState(false);\n const [sourceSelectionRevision, setSourceSelectionRevision] = useState(0);\n const [\n sourceSelectionRequiredUsdDisplay,\n setSourceSelectionRequiredUsdDisplay,\n ] = useState(undefined);\n const [sourcePickerDraftTokens, setSourcePickerDraftTokens] = useState<\n SwapTokenOption[] | null\n >(null);\n const sourcePickerDraftTokensRef = useRef(null);\n const sourcePickerDraftDepositFilterRef = useRef(\"all\");\n const sourcePickerDraftTouchedRef = useRef(false);\n const sourcePickerDraftModeRef = useRef<\"all\" | \"selected\">(\"all\");\n const [exactOutQuoteSourceMode, setExactOutQuoteSourceMode] = useState<\n \"all\" | \"selected\"\n >(\"all\");\n const exactOutQuoteSourceModeRef = useRef<\"all\" | \"selected\">(\"all\");\n const [toToken, setToToken] = useState(\n undefined\n );\n const [fromTokensQuoteKey, setFromTokensQuoteKey] = useState(\"\");\n\n useEffect(() => {\n setDepositFundingStep(activeMode === \"deposit\" ? \"method\" : \"wallet\");\n }, [activeMode]);\n\n useEffect(() => {\n const key = getSourceTokensQuoteKey(\n activeMode === \"swap\" && swapType === \"exactIn\"\n ? getReadyExactInSourceTokens(fromTokens)\n : fromTokens\n );\n setFromTokensQuoteKey(key);\n }, [activeMode, swapType, fromTokens]);\n const toTokenQuoteKey = getTokenQuoteKey(toToken);\n const appliedTokenPrefillRef = useRef(null);\n\n useEffect(() => {\n if (!toToken?.chainId || !toToken.contractAddress) return;\n\n let active = true;\n const selectedTokenKey = getTokenSelectionKey(toToken);\n const applyLoadedReceiveToken = () => {\n if (!active) return;\n const loadedToken = getCachedReceiveTokenMatch(toToken);\n if (!loadedToken) return;\n\n setToToken((current) => {\n if (!current || getTokenSelectionKey(current) !== selectedTokenKey) {\n return current;\n }\n\n const chainMeta = current.chainId\n ? CHAIN_METADATA[current.chainId]\n : undefined;\n const next = {\n ...current,\n chainLogo:\n loadedToken.chainLogo || current.chainLogo || chainMeta?.logo,\n chainName: getShortChainName(\n current.chainId,\n loadedToken.chainName || current.chainName || chainMeta?.name\n ),\n decimals: loadedToken.decimals ?? current.decimals,\n logo: loadedToken.logo || current.logo,\n name: loadedToken.name || current.name,\n priceUSD: loadedToken.priceUSD ?? current.priceUSD,\n symbol: loadedToken.symbol || current.symbol,\n };\n\n if (\n current.decimals === next.decimals &&\n current.chainLogo === next.chainLogo &&\n current.chainName === next.chainName &&\n current.logo === next.logo &&\n current.name === next.name &&\n current.priceUSD === next.priceUSD &&\n current.symbol === next.symbol\n ) {\n return current;\n }\n\n return next;\n });\n };\n\n applyLoadedReceiveToken();\n const receiveTokensPromise = preloadReceiveTokens();\n receiveTokensPromise?.then(applyLoadedReceiveToken).catch((error) => {\n if (active) {\n console.warn(\"Unable to refresh receive token metadata\", error);\n }\n });\n\n return () => {\n active = false;\n };\n }, [\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n const setExactOutQuoteSourceModeValue = useCallback(\n (mode: \"all\" | \"selected\") => {\n exactOutQuoteSourceModeRef.current = mode;\n setExactOutQuoteSourceMode(mode);\n },\n []\n );\n\n useEffect(() => {\n if (!nexusSDK) return;\n void fetchSwapBalance();\n }, [fetchSwapBalance, nexusSDK]);\n\n useEffect(() => {\n setSourceSelectionTouched(false);\n setExactOutQuoteSourceModeValue(\"all\");\n }, [activeMode, setExactOutQuoteSourceModeValue]);\n\n useEffect(() => {\n if (activeMode !== \"swap\") return;\n\n setRecipientAddress((current) => {\n if (!defaultRecipientAddress) return current ? \"\" : current;\n return current.toLowerCase() === defaultRecipientAddress.toLowerCase()\n ? current\n : defaultRecipientAddress;\n });\n }, [activeMode, defaultRecipientAddress]);\n\n const {\n steps,\n seed,\n onStepsList,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n const [progressEvents, setProgressEvents] = useState(\n []\n );\n const progressEventsRef = useRef([]);\n const swapStepsListRef = useRef([]);\n const [failedProgressStep, setFailedProgressStep] = useState<\n SwapStepType | BridgeStepType | null\n >(null);\n const [explorerUrls, setExplorerUrls] = useState<{\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n }>({ sourceExplorerUrl: null, destinationExplorerUrl: null });\n const swapRunIdRef = useRef(0);\n\n const widgetSessionIdRef = useRef(\n typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`\n );\n const widgetAttemptIdRef = useRef(null);\n const widgetOpenedTsRef = useRef(Date.now());\n const previewViewedTsRef = useRef(null);\n const previewConfirmedTsRef = useRef(null);\n const attemptCountRef = useRef(0);\n const fundsMovedRef = useRef(false);\n const intentUrlRef = useRef(null);\n const hadSimulationSuccessRef = useRef(false);\n const hadPreviewViewedRef = useRef(false);\n const widgetOpenedFiredRef = useRef(false);\n const reachedTerminalRef = useRef(false);\n const lastIntentSourceTokensRef = useRef([]);\n const lastAutoIntentSourceTokensRef = useRef([]);\n const immediateQuoteAfterSourceEditRef = useRef(false);\n const amountEnteredLastValueRef = useRef(\"\");\n const lastInputMethodRef = useRef<\n | \"typed\"\n | \"percent_20\"\n | \"percent_25\"\n | \"percent_50\"\n | \"percent_75\"\n | \"percent_max\"\n >(\"typed\");\n const prevSourceTouchedRef = useRef(false);\n const previousAutoSourceCountRef = useRef(0);\n const analyticsRef = useRef<{\n track: (event: string, properties?: Record) => void;\n } | null>(null);\n const selectedOpportunityRef = useRef(\n undefined\n );\n\n const newAttemptId = useCallback(() => {\n return typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`;\n }, []);\n\n const rotateAttempt = useCallback(() => {\n widgetAttemptIdRef.current = newAttemptId();\n previewViewedTsRef.current = null;\n previewConfirmedTsRef.current = null;\n fundsMovedRef.current = false;\n intentUrlRef.current = null;\n hadSimulationSuccessRef.current = false;\n hadPreviewViewedRef.current = false;\n reachedTerminalRef.current = false;\n }, [newAttemptId]);\n const [intentToAmount, setIntentToAmount] = useState(\n undefined\n );\n const [intentFeeUsd, setIntentFeeUsd] = useState(\n undefined\n );\n const [intentLoading, setIntentLoading] = useState(false);\n const [quoteRefreshing, setQuoteRefreshing] = useState(false);\n const [receiveMaxCalculating, setReceiveMaxCalculating] = useState(false);\n const [maxCalculationPercent, setMaxCalculationPercent] = useState<\n number | null\n >(null);\n const intentDestinationUsdRateCacheRef = useRef<\n Record\n >({});\n const intentSymbolUsdRateCacheRef = useRef<\n Record\n >({});\n const predictiveQuoteCacheRef = useRef<\n Record\n >({});\n const predictiveQuoteRunRef = useRef(0);\n const latestIntentFetchRunIdRef = useRef(0);\n const intentFetchTimingRef = useRef(null);\n const intentUiRenderLoggedRunIdRef = useRef(null);\n const [intentFetchTiming, setIntentFetchTiming] =\n useState(null);\n const [predictiveQuote, setPredictiveQuote] =\n useState(null);\n const maxPercentRunRef = useRef(0);\n const [previewQuoteRefreshing, setPreviewQuoteRefreshing] = useState(false);\n const [quoteRefreshProgress, setQuoteRefreshProgress] = useState(0);\n const [quoteRefreshSecondsRemaining, setQuoteRefreshSecondsRemaining] =\n useState(0);\n const [intentData, setIntentData] = useState(null);\n const [swapQuoteIssue, setSwapQuoteIssue] = useState(\n null\n );\n const [receiveAmountIssue, setReceiveAmountIssue] =\n useState(null);\n const receiveAmountIssueRef = useRef(null);\n const receiveAmountIssueKeyRef = useRef(\"\");\n const [transferExplorerUrl, setTransferExplorerUrl] = useState(\n null\n );\n const swapStepRef = useRef(swapStep);\n const syncingIntentSourcesRef = useRef(false);\n const lastSwapIntentRefreshAtRef = useRef(0);\n const [destinationBalance, setDestinationBalance] = useState(\n null\n );\n const [swapHistory, setSwapHistory] = useState(() =>\n readSwapHistoryFromStorage(historyStorageKey)\n );\n const [currentSwapId, setCurrentSwapId] = useState(null);\n const [historyNow, setHistoryNow] = useState(() => Date.now());\n const currentSwapIdRef = useRef(null);\n const currentSwapStartedAtRef = useRef(0);\n const historyStorageKeyRef = useRef(historyStorageKey);\n const skipNextHistoryPersistRef = useRef(false);\n const explorerUrlsRef = useRef<{\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n }>({ sourceExplorerUrl: null, destinationExplorerUrl: null });\n const activeQuoteInputKeyRef = useRef(\"\");\n\n // Ref to store swap intent hook allow/deny callbacks\n const swapIntentRef = useRef<{\n intent?: SwapIntentData;\n allow: () => void;\n deny: () => void;\n refresh: () => Promise;\n runId?: number;\n quoteInputKey?: string;\n } | null>(null);\n\n const startIntentFetchTiming = useCallback(\n ({\n background,\n operation,\n quoteInputKey,\n runId,\n }: {\n background: boolean;\n operation: string;\n quoteInputKey: string;\n runId: number;\n }) => {\n const startTimeMs = Date.now();\n const timing: IntentFetchTiming = {\n background,\n operation,\n quoteInputKey,\n runId,\n startTimeMs,\n walletAddress: ownerAddress ?? null,\n };\n\n latestIntentFetchRunIdRef.current = runId;\n intentFetchTimingRef.current = timing;\n intentUiRenderLoggedRunIdRef.current = null;\n setIntentFetchTiming(timing);\n console.log(\"[NexusWidget] intent fetch start\", {\n runId,\n walletAddress: timing.walletAddress,\n operation,\n quoteInputKey,\n background,\n dateNow: startTimeMs,\n date: new Date(startTimeMs),\n });\n return timing;\n },\n [ownerAddress]\n );\n\n const finishIntentFetchTiming = useCallback(\n (runId: number, status: \"resolved\" | \"failed\" = \"resolved\") => {\n const timing = intentFetchTimingRef.current;\n if (!timing || timing.runId !== runId) return;\n if (timing.endTimeMs !== undefined) return;\n\n const endTimeMs = Date.now();\n if (latestIntentFetchRunIdRef.current !== runId) {\n return;\n }\n\n const fetchDurationMs = endTimeMs - timing.startTimeMs;\n const completedTiming: IntentFetchTiming = {\n ...timing,\n endTimeMs,\n fetchDurationMs,\n };\n intentFetchTimingRef.current = completedTiming;\n setIntentFetchTiming(completedTiming);\n console.log(\"[NexusWidget] intent fetch end\", {\n runId,\n walletAddress: timing.walletAddress,\n operation: timing.operation,\n quoteInputKey: timing.quoteInputKey,\n status,\n dateNow: endTimeMs,\n date: new Date(endTimeMs),\n });\n console.log(\"[NexusWidget] intent fetch total\", {\n runId,\n walletAddress: timing.walletAddress,\n operation: timing.operation,\n quoteInputKey: timing.quoteInputKey,\n totalTimeSeconds: fetchDurationMs / 1000,\n elapsedMs: fetchDurationMs,\n dateNow: endTimeMs,\n });\n },\n []\n );\n\n useEffect(() => {\n swapStepRef.current = swapStep;\n }, [swapStep]);\n\n useEffect(() => {\n return () => {\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n }\n if (terminalBalanceRefreshTimerRef.current) {\n clearTimeout(terminalBalanceRefreshTimerRef.current);\n }\n if (rootHeightTransitionTimerRef.current) {\n clearTimeout(rootHeightTransitionTimerRef.current);\n }\n };\n }, []);\n\n const isQuoteEditLocked = useCallback(\n () => swapStepRef.current === \"choose-swap-asset\",\n []\n );\n\n const getQuoteRequestDelay = useCallback(() => {\n if (immediateQuoteAfterSourceEditRef.current) {\n immediateQuoteAfterSourceEditRef.current = false;\n return 0;\n }\n return EXACT_OUT_INPUT_DEBOUNCE_MS;\n }, []);\n\n const startRootHeightTransition = useCallback(() => {\n setShouldAnimateRootHeight(true);\n if (rootHeightTransitionTimerRef.current) {\n clearTimeout(rootHeightTransitionTimerRef.current);\n }\n rootHeightTransitionTimerRef.current = setTimeout(() => {\n setShouldAnimateRootHeight(false);\n rootHeightTransitionTimerRef.current = null;\n }, ROOT_HEIGHT_TRANSITION_MS);\n }, []);\n\n const closeDrawerToIdle = useCallback(() => {\n const isDrawerStep =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n swapStep === \"enter-recipient\";\n\n if (!isDrawerStep) {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n return;\n }\n\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n }\n\n setClosingDrawerStep(swapStep);\n drawerCloseTimerRef.current = setTimeout(() => {\n if (\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\"\n ) {\n startRootHeightTransition();\n }\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n setClosingDrawerStep(null);\n drawerCloseTimerRef.current = null;\n }, DRAWER_CLOSE_MS);\n }, [startRootHeightTransition, swapStep]);\n\n const openDrawerStep = useCallback((nextStep: SwapStep) => {\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n drawerCloseTimerRef.current = null;\n }\n if (\n nextStep === \"choose-swap-asset\" ||\n nextStep === \"choose-receive-asset\"\n ) {\n startRootHeightTransition();\n }\n setClosingDrawerStep(null);\n swapStepRef.current = nextStep;\n setSwapStep(nextStep);\n }, [startRootHeightTransition]);\n\n const syncRootContentHeight = useCallback((animate = false) => {\n const element = rootContentRef.current;\n if (!element) return;\n\n const nextHeight = Math.ceil(\n Math.max(element.getBoundingClientRect().height, element.scrollHeight)\n );\n if (nextHeight <= 0) return;\n\n if (rootContentHeightRef.current === nextHeight) {\n setHasMeasuredRootContent(true);\n return;\n }\n\n rootContentHeightRef.current = nextHeight;\n setShouldAnimateRootHeight(animate);\n if (rootHeightTransitionTimerRef.current) {\n clearTimeout(rootHeightTransitionTimerRef.current);\n rootHeightTransitionTimerRef.current = null;\n }\n if (animate) {\n rootHeightTransitionTimerRef.current = setTimeout(() => {\n setShouldAnimateRootHeight(false);\n rootHeightTransitionTimerRef.current = null;\n }, ROOT_HEIGHT_TRANSITION_MS);\n }\n setRootContentHeight(nextHeight);\n setHasMeasuredRootContent(true);\n }, []);\n\n useLayoutEffect(() => {\n syncRootContentHeight(true);\n\n const element = rootContentRef.current;\n if (!element || typeof ResizeObserver === \"undefined\") return;\n\n let frame = 0;\n const observer = new ResizeObserver(() => {\n if (frame) {\n window.cancelAnimationFrame(frame);\n }\n frame = window.requestAnimationFrame(() => {\n syncRootContentHeight(false);\n });\n });\n\n observer.observe(element);\n\n return () => {\n if (frame) {\n window.cancelAnimationFrame(frame);\n }\n observer.disconnect();\n };\n }, [activeMode, swapStep, syncRootContentHeight]);\n\n useEffect(() => {\n currentSwapIdRef.current = currentSwapId;\n }, [currentSwapId]);\n\n useEffect(() => {\n if (historyStorageKeyRef.current === historyStorageKey) return;\n historyStorageKeyRef.current = historyStorageKey;\n skipNextHistoryPersistRef.current = true;\n setSwapHistory(readSwapHistoryFromStorage(historyStorageKey));\n }, [historyStorageKey]);\n\n useEffect(() => {\n if (skipNextHistoryPersistRef.current) {\n skipNextHistoryPersistRef.current = false;\n return;\n }\n\n writeSwapHistoryToStorage(historyStorageKey, swapHistory);\n }, [historyStorageKey, swapHistory]);\n\n useEffect(() => {\n if (swapStep !== \"history\") return;\n const timer = window.setInterval(() => setHistoryNow(Date.now()), 30000);\n return () => window.clearInterval(timer);\n }, [swapStep]);\n\n const normalizeAddress = (value?: string | null) =>\n (value ?? \"\").toLowerCase();\n\n const getTokenLookupAddress = (value?: string | null) => {\n if (!value) return \"\";\n return isNativeTokenAddress(value) ? zeroAddress : value.toLowerCase();\n };\n\n const findBalanceTokenLogo = (\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId) return \"\";\n const lookupAddress = getTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n let symbolMatchLogo = \"\";\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== chainId) continue;\n const breakdownAddress = getTokenLookupAddress(\n breakdown.contractAddress\n );\n const addressMatches =\n lookupAddress && breakdownAddress === lookupAddress;\n const symbolMatches =\n lookupSymbol &&\n [breakdown.symbol, asset.symbol]\n .filter(Boolean)\n .some((candidate) => candidate.toUpperCase() === lookupSymbol);\n const logo = getLogoFromMetadata(breakdown) || getLogoFromMetadata(asset);\n\n if (addressMatches && logo) return logo;\n if (!symbolMatchLogo && symbolMatches && logo) {\n symbolMatchLogo = logo;\n }\n }\n }\n\n return symbolMatchLogo;\n };\n\n const findSupportedTokenLogo = (\n chains: any[] | null | undefined,\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId) return \"\";\n const chain = chains?.find(\n (item: any) => Number(item?.id ?? item?.chainId) === chainId\n );\n const tokens = chain?.tokens ?? chain?.assets ?? [];\n const lookupAddress = getTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n\n const addressMatch =\n lookupAddress &&\n tokens.find((token: any) => {\n const tokenAddress = getTokenLookupAddress(\n token?.contractAddress ?? token?.address ?? token?.tokenAddress\n );\n return tokenAddress === lookupAddress;\n });\n const symbolMatch =\n lookupSymbol &&\n tokens.find((token: any) => {\n const tokenSymbol = (token?.symbol ?? token?.tokenSymbol ?? \"\")\n .toString()\n .toUpperCase();\n return tokenSymbol === lookupSymbol;\n });\n\n return getLogoFromMetadata(addressMatch) || getLogoFromMetadata(symbolMatch);\n };\n\n const resolveTokenLogo = (\n token:\n | {\n address?: string;\n contractAddress?: string;\n logo?: string;\n symbol?: string;\n tokenAddress?: string;\n tokenSymbol?: string;\n }\n | undefined,\n chainId?: number\n ) => {\n const explicitLogo = getLogoFromMetadata(token);\n if (explicitLogo) return explicitLogo;\n\n const contractAddress =\n token?.contractAddress ?? token?.address ?? token?.tokenAddress;\n const symbol = token?.symbol ?? token?.tokenSymbol;\n const citreaToken = findCitreaReceiveToken({\n address: contractAddress,\n chainId,\n symbol,\n });\n const tokenMeta = symbol\n ? TOKEN_METADATA[symbol as keyof typeof TOKEN_METADATA]\n : undefined;\n\n return (\n findBalanceTokenLogo(chainId, contractAddress, symbol) ||\n findSupportedTokenLogo(\n swapSupportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol\n ) ||\n findSupportedTokenLogo(\n supportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol\n ) ||\n citreaToken?.logo ||\n tokenMeta?.logo ||\n (contractAddress && isNativeTokenAddress(contractAddress)\n ? CHAIN_METADATA[chainId ?? 0]?.logo\n : \"\") ||\n \"\"\n );\n };\n\n const enrichIntentToken = <\n T extends {\n address?: string;\n contractAddress?: string;\n logo?: string;\n symbol?: string;\n tokenAddress?: string;\n tokenSymbol?: string;\n },\n >(\n token: T,\n chainId?: number\n ): T => {\n const logo = resolveTokenLogo(token, chainId);\n return logo && logo !== token.logo ? { ...token, logo } : token;\n };\n\n const enrichTokenOptionLogo = (\n token?: SwapTokenOption\n ): SwapTokenOption | undefined => {\n if (!token) return undefined;\n\n const chainMeta = token.chainId ? CHAIN_METADATA[token.chainId] : undefined;\n const logo = resolveTokenLogo(token, token.chainId);\n const next = {\n ...token,\n chainLogo: token.chainLogo || chainMeta?.logo,\n logo: token.logo || logo,\n };\n\n if (next.chainLogo === token.chainLogo && next.logo === token.logo) {\n return token;\n }\n\n return next;\n };\n\n const enrichSwapIntentTokenMetadata = (\n intent: SwapIntentData\n ): SwapIntentData => ({\n ...intent,\n destination: {\n ...intent.destination,\n token: enrichIntentToken(\n intent.destination.token,\n intent.destination.chain.id\n ),\n },\n sources: (intent.sources ?? []).map((source) => ({\n ...source,\n token: enrichIntentToken(source.token, source.chain.id),\n })),\n });\n\n const buildIntentSourceToken = (\n source: SwapIntentData[\"sources\"][number]\n ): SwapTokenOption => {\n let matchedAsset: any;\n let matchedBreakdown: any;\n const sourceAddress = normalizeAddress(source.token.contractAddress);\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const addressMatches =\n normalizeAddress(breakdown.contractAddress) === sourceAddress;\n const symbolMatches =\n breakdown.symbol === source.token.symbol ||\n asset.symbol === source.token.symbol;\n if (\n breakdown.chain?.id === source.chain.id &&\n (addressMatches || symbolMatches)\n ) {\n matchedAsset = asset;\n matchedBreakdown = breakdown;\n break;\n }\n }\n if (matchedBreakdown) break;\n }\n\n const chainMeta = CHAIN_METADATA[source.chain.id];\n const sourceValue = Number((source as any).value ?? 0);\n const isNativeSource = isNativeTokenAddress(source.token.contractAddress);\n const nativeCurrency = chainMeta?.nativeCurrency;\n const sourceSymbol =\n isNativeSource && (!source.token.symbol || !matchedAsset?.logo)\n ? nativeCurrency?.symbol || source.token.symbol\n : source.token.symbol || nativeCurrency?.symbol || \"\";\n const sourceDecimals =\n isNativeSource && nativeCurrency?.decimals !== undefined\n ? nativeCurrency.decimals\n : source.token.decimals;\n const sourceLogo = resolveTokenLogo(source.token, source.chain.id);\n\n return {\n contractAddress: source.token.contractAddress,\n symbol: sourceSymbol,\n name: sourceSymbol,\n logo: sourceLogo ?? \"\",\n decimals: sourceDecimals,\n balance: matchedBreakdown?.balance\n ? `${matchedBreakdown.balance} ${sourceSymbol}`\n : `${source.amount} ${sourceSymbol}`,\n balanceInFiat:\n matchedBreakdown?.balanceInFiat != null\n ? `$${Number(matchedBreakdown.balanceInFiat).toFixed(2)}`\n : Number.isFinite(sourceValue)\n ? `$${sourceValue.toFixed(2)}`\n : \"$0.00\",\n chainId: source.chain.id,\n chainName: getShortChainName(\n source.chain.id,\n chainMeta?.name ?? source.chain.name\n ),\n chainLogo: chainMeta?.logo ?? source.chain.logo,\n userAmount: source.amount,\n userAmountUsd: Number.isFinite(sourceValue) ? source.value : undefined,\n userAmountMode: \"token\",\n };\n };\n\n const clearPendingSwapIntent = (\n clearQuote = true,\n options: { keepQuoteRefreshing?: boolean } = {}\n ) => {\n swapRunIdRef.current += 1;\n swapIntentRef.current?.deny();\n swapIntentRef.current = null;\n setIntentLoading(false);\n setTxError(null);\n if (!options.keepQuoteRefreshing) {\n setQuoteRefreshing(false);\n }\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setSwapQuoteIssue(null);\n resetProgressEvents();\n if (swapStepsListRef.current.length > 0 || steps.length > 0) {\n swapStepsListRef.current = [];\n resetSteps();\n } else {\n swapStepsListRef.current = [];\n }\n if (clearQuote) {\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n if (!options.keepQuoteRefreshing) {\n setPredictiveQuote(null);\n }\n }\n };\n\n const clearSelectedSources = () => {\n setFromTokens((current) => (current.length === 0 ? current : []));\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setExactOutQuoteSourceModeValue(\"all\");\n };\n\n const resetExactOutSourcesToAuto = () => {\n setFromTokens((current) => (current.length === 0 ? current : []));\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setExactOutQuoteSourceModeValue(\"all\");\n setSourceSelectionRevision((current) => current + 1);\n };\n\n const getSourceAmountInput = (tokens: SwapTokenOption[]) => {\n const total = tokens.reduce(\n (sum, token) => sum + Number(token.userAmount || 0),\n 0\n );\n return total > 0 ? String(total) : \"\";\n };\n\n const parseFiatNumber = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = normalizeDecimalInputText(value);\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n };\n\n const minimumSourceUsd = new Decimal(1);\n const hasMinimumSourceUsdValue = (value: unknown) =>\n (parseFiatNumber(value) ?? new Decimal(0)).gte(minimumSourceUsd);\n const hasMinimumSourceUsdBalance = (\n token: Pick\n ) => hasMinimumSourceUsdValue(token.balanceInFiat);\n const filterMinimumSourceUsdTokens = (tokens: SwapTokenOption[]) =>\n tokens.filter(hasMinimumSourceUsdBalance);\n\n const getTokenUsdRateCacheKeyFromParts = (\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId || !symbol) return \"\";\n return [\n chainId,\n (contractAddress || zeroAddress).toLowerCase(),\n symbol.toUpperCase(),\n ].join(\":\");\n };\n\n const getTokenUsdRateCacheKey = (\n token?: Pick\n ) =>\n getTokenUsdRateCacheKeyFromParts(\n token?.chainId,\n token?.contractAddress,\n token?.symbol\n );\n\n const getSymbolUsdRateCacheKey = (symbol?: string) =>\n symbol ? symbol.trim().toUpperCase() : \"\";\n\n const getCachedIntentUsdRate = (\n token?: Pick\n ) => {\n const tokenKey = getTokenUsdRateCacheKey(token);\n const cached = tokenKey\n ? intentDestinationUsdRateCacheRef.current[tokenKey]\n : undefined;\n const rate = parseFiatNumber(cached?.rate);\n return rate && rate.gt(0) ? rate : undefined;\n };\n\n const cacheDestinationUsdRateFromIntent = (\n intent?: SwapIntentData | null\n ) => {\n const destination = intent?.destination;\n const amount = parseFiatNumber(destination?.amount);\n const value = parseFiatNumber(destination?.value);\n const chainId = destination?.chain?.id;\n const symbol = destination?.token?.symbol;\n\n if (\n !amount ||\n !value ||\n amount.lte(0) ||\n value.lte(0) ||\n !chainId ||\n !symbol\n ) {\n return;\n }\n\n const rate = value.div(amount);\n if (!rate.isFinite() || rate.lte(0)) return;\n\n const cached: CachedIntentUsdRate = {\n amount: amount.toFixed(),\n rate: rate.toDecimalPlaces(18).toFixed(),\n updatedAt: Date.now(),\n value: value.toFixed(),\n };\n const tokenKey = getTokenUsdRateCacheKeyFromParts(\n chainId,\n destination?.token?.contractAddress,\n symbol\n );\n if (tokenKey) {\n intentDestinationUsdRateCacheRef.current[tokenKey] = cached;\n }\n\n const symbolKey = getSymbolUsdRateCacheKey(symbol);\n if (symbolKey) {\n intentSymbolUsdRateCacheRef.current[symbolKey] = cached;\n }\n };\n\n const getSwapBalanceTotalUsd = () =>\n (swapBalance ?? []).reduce((sum, asset) => {\n const breakdown = asset.breakdown ?? [];\n if (breakdown.length > 0) {\n return sum.plus(\n breakdown.reduce((breakdownSum, item) => {\n const value = parseFiatNumber(item.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd)\n ? breakdownSum.plus(value)\n : breakdownSum;\n }, new Decimal(0))\n );\n }\n\n const value = parseFiatNumber(asset.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd) ? sum.plus(value) : sum;\n }, new Decimal(0));\n\n const getTokenUsdRate = (token: SwapTokenOption) => {\n const tokenBalance = parseFiatNumber(token.balance) ?? new Decimal(0);\n const fiatBalance = parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n if (tokenBalance.gt(0) && fiatBalance.gt(0)) {\n return fiatBalance.div(tokenBalance);\n }\n\n const priceUsd = parseFiatNumber(token.priceUSD);\n if (priceUsd && priceUsd.gt(0)) {\n return priceUsd;\n }\n\n const fallbackRate = getFiatValue(1, token.symbol);\n if (Number.isFinite(fallbackRate) && fallbackRate > 0) {\n return new Decimal(fallbackRate);\n }\n\n return getCachedIntentUsdRate(token) ?? new Decimal(0);\n };\n const getUsdRateForSymbol = (symbol?: string) => {\n if (!symbol) return new Decimal(0);\n const fiat = getFiatValue(1, symbol);\n if (Number.isFinite(fiat) && fiat > 0) {\n return new Decimal(fiat);\n }\n\n const cached =\n intentSymbolUsdRateCacheRef.current[getSymbolUsdRateCacheKey(symbol)];\n const rate = parseFiatNumber(cached?.rate);\n return rate && rate.gt(0) ? rate : new Decimal(0);\n };\n\n const getFeeTokenLookupAddress = (value?: string | null) => {\n const lower = (value ?? \"\").toLowerCase();\n if (!lower || lower === \"0x\" || isNativeTokenAddress(lower)) {\n return zeroAddress;\n }\n return lower;\n };\n\n const getTokenUsdRateFromBalances = (\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId) return undefined;\n const lookupAddress = getFeeTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== chainId) continue;\n const breakdownAddress = getFeeTokenLookupAddress(\n breakdown.contractAddress\n );\n const addressMatches =\n lookupAddress && breakdownAddress === lookupAddress;\n const symbolMatches =\n lookupSymbol &&\n [breakdown.symbol, asset.symbol]\n .filter(Boolean)\n .some((candidate) => candidate.toUpperCase() === lookupSymbol);\n if (!addressMatches && !symbolMatches) continue;\n\n const balance = parseFiatNumber(breakdown.balance);\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n if (balance && fiatBalance && balance.gt(0) && fiatBalance.gt(0)) {\n return fiatBalance.div(balance);\n }\n }\n }\n\n return undefined;\n };\n\n const getTokenUsdRateFromSupportedChains = (\n chains: any[] | null | undefined,\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId) return undefined;\n const chain = chains?.find(\n (item: any) => Number(item?.id ?? item?.chainId) === chainId\n );\n const tokens = chain?.tokens ?? chain?.assets ?? [];\n const lookupAddress = getFeeTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n const matchedToken = tokens.find((token: any) => {\n const tokenAddress = getFeeTokenLookupAddress(\n token?.contractAddress ?? token?.address ?? token?.tokenAddress\n );\n const addressMatches = lookupAddress && tokenAddress === lookupAddress;\n const tokenSymbol = (token?.symbol ?? token?.tokenSymbol ?? \"\")\n .toString()\n .toUpperCase();\n const symbolMatches = lookupSymbol && tokenSymbol === lookupSymbol;\n return addressMatches || symbolMatches;\n });\n const priceUsd = parseFiatNumber(\n matchedToken?.priceUSD ??\n matchedToken?.priceUsd ??\n matchedToken?.usdPrice ??\n matchedToken?.price\n );\n return priceUsd && priceUsd.gt(0) ? priceUsd : undefined;\n };\n\n const getIntentDestinationGasUsdValue = (\n intent?: SwapIntentData | null\n ) => {\n const gas = intent?.destination?.gas;\n const explicitValue = parseFiatNumber(gas?.value);\n if (explicitValue && explicitValue.gt(0)) return explicitValue;\n\n const amount = parseFiatNumber(gas?.amount);\n if (!amount || amount.lte(0)) return undefined;\n\n const chainId = intent?.destination?.chain?.id;\n const contractAddress = gas?.token?.contractAddress;\n const symbol = gas?.token?.symbol;\n const rate =\n getTokenUsdRateFromBalances(chainId, contractAddress, symbol) ??\n getTokenUsdRateFromSupportedChains(\n swapSupportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol\n ) ??\n getTokenUsdRateFromSupportedChains(\n supportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol\n ) ??\n getUsdRateForSymbol(symbol);\n\n return rate.gt(0) ? amount.mul(rate) : undefined;\n };\n\n const getCachedDestinationUsdRate = (token?: SwapTokenOption) => {\n return getCachedIntentUsdRate(token);\n };\n\n const resolveUsdRateForSymbol = async (symbol?: string) => {\n if (!symbol) return new Decimal(0);\n\n const localRate = getUsdRateForSymbol(symbol);\n if (localRate.gt(0)) return localRate;\n\n try {\n const resolvedRate = await resolveTokenUsdRate(symbol);\n return resolvedRate && resolvedRate > 0\n ? new Decimal(resolvedRate)\n : new Decimal(0);\n } catch {\n return new Decimal(0);\n }\n };\n\n const getTokenUsdValue = (\n token: SwapTokenOption,\n fallbackAmount?: string\n ) => {\n const amountNumber =\n parseFiatNumber(token.userAmount || fallbackAmount) ?? new Decimal(0);\n if (amountNumber.lte(0)) return new Decimal(0);\n const quotedUsd = parseFiatNumber(token.userAmountUsd);\n if (quotedUsd && quotedUsd.gte(0)) return quotedUsd;\n if (token.userAmountMode === \"usd\") return amountNumber;\n\n const rate = getTokenUsdRate(token);\n return rate.gt(0) ? amountNumber.mul(rate) : new Decimal(0);\n };\n\n const getTokenBalanceAmount = (token: SwapTokenOption) =>\n parseFiatNumber(token.balance) ?? new Decimal(0);\n\n const getTokenBalanceUsd = (token: SwapTokenOption) =>\n parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n\n const getSdkExactOutSourcePriority = (token: SwapTokenOption) => {\n const isSameChain =\n Boolean(token.chainId && toToken?.chainId) &&\n token.chainId === toToken?.chainId;\n const isSameToken =\n Boolean(token.contractAddress && toToken?.contractAddress) &&\n ((isNativeTokenAddress(token.contractAddress) &&\n isNativeTokenAddress(toToken?.contractAddress)) ||\n token.contractAddress.toLowerCase() ===\n toToken?.contractAddress.toLowerCase());\n const isStable = isSdkExactOutStableSymbol(token.symbol);\n const isGas = isNativeTokenAddress(token.contractAddress);\n const isEthereum = token.chainId === ETHEREUM_MAINNET_CHAIN_ID;\n\n if (isSameChain) {\n if (isSameToken) return 1;\n if (isStable) return 2;\n if (isGas) return 3;\n return 4;\n }\n\n if (isEthereum) {\n if (isSameToken) return 8;\n if (isStable) return 9;\n if (isGas) return 10;\n return 11;\n }\n\n if (isSameToken) return 5;\n if (isStable) return 6;\n return 7;\n };\n\n const sortExactOutSourcesBySdkPriority = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const priorityDiff =\n getSdkExactOutSourcePriority(a) - getSdkExactOutSourcePriority(b);\n if (priorityDiff !== 0) return priorityDiff;\n\n const usdDiff = getTokenBalanceUsd(b).cmp(getTokenBalanceUsd(a));\n if (usdDiff !== 0) return usdDiff;\n\n return `${a.symbol} ${a.chainName ?? \"\"}`.localeCompare(\n `${b.symbol} ${b.chainName ?? \"\"}`\n );\n });\n\n const getTokenAmountForUsd = (token: SwapTokenOption, usdAmount: Decimal) => {\n const rate = getTokenUsdRate(token);\n if (rate.lte(0) || usdAmount.lte(0)) return new Decimal(0);\n return usdAmount.div(rate);\n };\n\n const getUsdForTokenAmount = (\n token: SwapTokenOption,\n tokenAmount: Decimal\n ) => {\n const rate = getTokenUsdRate(token);\n if (rate.lte(0) || tokenAmount.lte(0)) return new Decimal(0);\n return tokenAmount.mul(rate);\n };\n\n const getExactOutDestinationBalanceCoverage = ({\n requestedAmount,\n requestedUsd,\n producedAmount,\n producedUsd,\n token = toToken,\n }: {\n requestedAmount?: Decimal;\n requestedUsd?: Decimal;\n producedAmount?: Decimal;\n producedUsd?: Decimal;\n token?: SwapTokenOption;\n }) => {\n if (\n (activeMode !== \"deposit\" && activeMode !== \"send\") ||\n !token ||\n !requestedAmount ||\n requestedAmount.lte(0)\n ) {\n return null;\n }\n\n const balanceAmount =\n parseFiatNumber(destinationBalance) ??\n parseFiatNumber(token.balance) ??\n new Decimal(0);\n if (balanceAmount.lte(0)) return null;\n\n const externalAmount =\n producedAmount && producedAmount.gt(0) ? producedAmount : new Decimal(0);\n const uncoveredAmount = Decimal.max(\n requestedAmount.minus(externalAmount),\n new Decimal(0)\n );\n const coveredAmount = Decimal.min(balanceAmount, uncoveredAmount);\n if (coveredAmount.lte(0)) return null;\n\n const requestedRate =\n requestedUsd && requestedUsd.gt(0)\n ? requestedUsd.div(requestedAmount)\n : undefined;\n const producedRate =\n producedUsd && producedUsd.gt(0) && producedAmount && producedAmount.gt(0)\n ? producedUsd.div(producedAmount)\n : undefined;\n const fallbackRate = getTokenUsdRate(token);\n const usdRate =\n requestedRate && requestedRate.gt(0)\n ? requestedRate\n : producedRate && producedRate.gt(0)\n ? producedRate\n : fallbackRate.gt(0)\n ? fallbackRate\n : undefined;\n\n return {\n amount: coveredAmount,\n usd: usdRate ? coveredAmount.mul(usdRate) : undefined,\n };\n };\n\n const buildDestinationBalanceDisplayToken = (\n coverage: ReturnType,\n token?: SwapTokenOption\n ): SwapTokenOption | null => {\n if (!coverage || !token || coverage.amount.lte(0)) return null;\n\n const amount = coverage.amount\n .toDecimalPlaces(Math.max(0, token.decimals ?? 18), Decimal.ROUND_DOWN)\n .toFixed();\n const usd = coverage.usd?.toDecimalPlaces(6, Decimal.ROUND_DOWN).toFixed();\n const balanceUsd = coverage.usd\n ? `$${coverage.usd.toDecimalPlaces(2, Decimal.ROUND_DOWN).toFixed()}`\n : token.balanceInFiat || \"$0.00\";\n\n return {\n ...token,\n balance: `${amount} ${token.symbol}`,\n balanceInFiat: balanceUsd,\n userAmount: amount,\n userAmountMode: \"token\",\n userAmountUsd: usd,\n };\n };\n\n const cacheSymbolUsdRate = (symbol: string | undefined, rate: Decimal) => {\n const symbolKey = getSymbolUsdRateCacheKey(symbol);\n if (!symbolKey || rate.lte(0)) return;\n\n intentSymbolUsdRateCacheRef.current[symbolKey] = {\n amount: \"1\",\n rate: rate.toDecimalPlaces(18).toFixed(),\n updatedAt: Date.now(),\n value: rate.toFixed(),\n };\n };\n\n const getPredictiveDestinationKey = (token?: SwapTokenOption) => {\n const tokenKey = getTokenUsdRateCacheKey(token);\n return tokenKey ? `destination:${tokenKey}` : \"\";\n };\n\n const getPredictiveSourceKey = (token: SwapTokenOption) =>\n [\n token.chainId ?? \"unknown\",\n (token.contractAddress || zeroAddress).toLowerCase(),\n token.symbol.toUpperCase(),\n ].join(\":\");\n\n const getPredictiveQuoteCacheKey = (\n mode = activeMode,\n type = swapType,\n destination = toToken,\n sources = fromTokens\n ) => {\n const destinationKey = getPredictiveDestinationKey(destination);\n if (!destinationKey) return \"\";\n if (mode !== \"swap\" || type !== \"exactIn\") {\n return `exactOut:${destinationKey}`;\n }\n\n const sourceKey = getExpandedSourceTokens(sources)\n .map(getPredictiveSourceKey)\n .sort()\n .join(\"+\");\n return sourceKey ? `exactIn:${sourceKey}->${destinationKey}` : \"\";\n };\n\n const getPredictiveDisplayAmount = (\n amount: Decimal,\n token?: Pick\n ) => {\n const decimals = Math.min(\n PREDICTIVE_QUOTE_DISPLAY_DECIMALS,\n Math.max(0, token?.decimals ?? 18)\n );\n return amount.toDecimalPlaces(decimals, Decimal.ROUND_DOWN).toFixed();\n };\n\n const resolveUsdRateForToken = async (token?: SwapTokenOption) => {\n if (!token?.symbol) return new Decimal(0);\n\n const localRate = getTokenUsdRate(token);\n if (localRate.gt(0)) return localRate;\n\n const resolvedRate = await resolveUsdRateForSymbol(token.symbol);\n if (resolvedRate.gt(0)) {\n cacheSymbolUsdRate(token.symbol, resolvedRate);\n }\n return resolvedRate;\n };\n\n const getDestinationReceiveLimitUsd = (token?: SwapTokenOption) => {\n if (!token?.chainId) return undefined;\n const limit = DESTINATION_RECEIVE_LIMIT_USD_BY_CHAIN_ID[token.chainId];\n return limit ? new Decimal(limit) : undefined;\n };\n\n const getImmediateDestinationReceiveUsdRate = (token?: SwapTokenOption) => {\n const priceUsd = parseFiatNumber(token?.priceUSD);\n if (priceUsd && priceUsd.gt(0)) return priceUsd;\n\n const cachedRate = getCachedDestinationUsdRate(token);\n if (cachedRate && cachedRate.gt(0)) return cachedRate;\n\n if (!token) return undefined;\n const localRate = getTokenUsdRate(token);\n return localRate.gt(0) ? localRate : undefined;\n };\n\n const getExactInSourceUsdForReceiveLimit = (\n sourceTokens: SwapTokenOption[],\n inputAmount: string\n ) => {\n if (sourceTokens.length === 0) return undefined;\n let hasPositiveSourceAmount = false;\n let totalUsd = new Decimal(0);\n\n for (const token of sourceTokens) {\n const fallbackAmount =\n sourceTokens.length === 1 ? inputAmount : undefined;\n const sourceAmount = parseFiatNumber(token.userAmount || fallbackAmount);\n if (!sourceAmount || sourceAmount.lte(0)) continue;\n\n hasPositiveSourceAmount = true;\n const sourceUsd = getTokenUsdValue(token, fallbackAmount);\n if (sourceUsd.lte(0)) return undefined;\n totalUsd = totalUsd.plus(sourceUsd);\n }\n\n return hasPositiveSourceAmount ? totalUsd : undefined;\n };\n\n const buildReceiveAmountIssue = ({\n destinationRate,\n destinationToken = toToken,\n inputAmount = amount,\n mode = activeMode,\n sourceTokens = fromTokens,\n type = swapType,\n }: {\n destinationRate?: Decimal;\n destinationToken?: SwapTokenOption;\n inputAmount?: string;\n mode?: NexusWidgetMode;\n sourceTokens?: SwapTokenOption[];\n type?: SwapType;\n } = {}): ReceiveAmountIssue | null => {\n const limit = getDestinationReceiveLimitUsd(destinationToken);\n if (!limit || !destinationToken) return null;\n\n const parsedAmount = parseFiatNumber(inputAmount);\n if (!parsedAmount || parsedAmount.lte(0)) return null;\n if (mode === \"swap\" && type === \"exactIn\" && sourceTokens.length === 0) {\n return null;\n }\n\n const chainName = getShortChainName(\n destinationToken.chainId,\n destinationToken.chainName\n );\n const resolvedDestinationRate =\n destinationRate ??\n getImmediateDestinationReceiveUsdRate(destinationToken);\n\n if (!resolvedDestinationRate || resolvedDestinationRate.lte(0)) {\n return {\n ctaLabel: \"Price unavailable\",\n message: `Unable to price ${destinationToken.symbol} on ${chainName}. Select another receive token.`,\n type: \"unpricedReceiveToken\",\n };\n }\n\n let receiveUsd: Decimal | undefined;\n if (mode === \"swap\" && type === \"exactIn\") {\n receiveUsd = getExactInSourceUsdForReceiveLimit(\n sourceTokens,\n inputAmount\n );\n if (!receiveUsd || receiveUsd.lte(0)) {\n return {\n ctaLabel: \"Price unavailable\",\n message: `Unable to price selected assets for ${chainName}'s receive limit.`,\n type: \"unpricedReceiveToken\",\n };\n }\n } else if (mode === \"deposit\" && depositAmountMode === \"usd\") {\n receiveUsd = parsedAmount;\n } else {\n receiveUsd = parsedAmount.mul(resolvedDestinationRate);\n }\n\n if (receiveUsd.gt(limit)) {\n return {\n ctaLabel: \"Receive limit exceeded\",\n message: `Maximum receive amount on ${chainName} is ${formatUsdDisplay(limit)}.`,\n type: \"receiveLimitExceeded\",\n };\n }\n\n return null;\n };\n\n const buildConfiguredAmountIssue = (\n inputAmount = amount\n ): ReceiveAmountIssue | null => {\n if (!amountInputConfig || !inputAmount) return null;\n const parsedAmount = parseFiatNumber(inputAmount);\n if (!parsedAmount || parsedAmount.lte(0)) return null;\n\n const minAmount = parseFiatNumber(amountInputConfig.min);\n if (minAmount && parsedAmount.lt(minAmount)) {\n return {\n ctaLabel: \"Below minimum\",\n message: `Minimum amount is ${minAmount.toFixed()}.`,\n type: \"configuredAmountLimit\",\n };\n }\n\n const maxAmount = parseFiatNumber(amountInputConfig.max);\n if (maxAmount && parsedAmount.gt(maxAmount)) {\n return {\n ctaLabel: \"Above maximum\",\n message: `Maximum amount is ${maxAmount.toFixed()}.`,\n type: \"configuredAmountLimit\",\n };\n }\n\n return null;\n };\n\n const clearPreviewForBlockingAmountIssue = () => {\n clearPendingSwapIntent(true);\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setTxError(null);\n setSwapQuoteIssue(null);\n };\n\n const applyReceiveAmountIssue = (issue: ReceiveAmountIssue | null) => {\n const key = issue ? `${issue.type}:${issue.message}` : \"\";\n receiveAmountIssueRef.current = issue;\n if (receiveAmountIssueKeyRef.current !== key) {\n receiveAmountIssueKeyRef.current = key;\n setReceiveAmountIssue(issue);\n }\n if (!issue) return;\n\n clearPendingSwapIntent(true);\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setTxError(null);\n };\n\n const getPredictiveExactInSourceTokens = () => {\n const expanded = getExpandedSourceTokens(fromTokens);\n if (expanded.length === 0) return [];\n\n return expanded\n .map((token) => {\n const userAmount =\n token.userAmount ||\n (expanded.length === 1 && hasPositiveDecimalInput(amount)\n ? amount\n : \"\");\n return { ...token, userAmount };\n })\n .filter((token) => hasPositiveDecimalInput(token.userAmount));\n };\n\n const sortUnifiedSourceTokens = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const fiatDiff = getTokenBalanceUsd(b).cmp(getTokenBalanceUsd(a));\n if (fiatDiff !== 0) return fiatDiff;\n return getTokenBalanceAmount(b).cmp(getTokenBalanceAmount(a));\n });\n\n const allocateUnifiedExactInToken = (\n token: SwapTokenOption,\n fallbackAmount?: string\n ) => {\n if (!token.isUnified || !token.sourceTokens?.length) return [token];\n\n const rawAmount =\n parseFiatNumber(token.userAmount || fallbackAmount) ?? new Decimal(0);\n if (rawAmount.lte(0)) return [];\n\n const sortedSources = sortUnifiedSourceTokens(token.sourceTokens).filter(\n (source) =>\n source.chainId &&\n source.contractAddress &&\n getTokenBalanceAmount(source).gt(0) &&\n hasMinimumSourceUsdBalance(source)\n );\n const allocated: SwapTokenOption[] = [];\n\n if (token.userAmountMode === \"usd\") {\n let remainingUsd = rawAmount;\n\n for (const source of sortedSources) {\n if (remainingUsd.lte(0)) break;\n\n const availableUsd = getTokenBalanceUsd(source);\n if (availableUsd.lte(0)) continue;\n\n const targetUsd = Decimal.min(remainingUsd, availableUsd);\n const tokenAmount = getTokenAmountForUsd(\n source,\n targetUsd\n ).toDecimalPlaces(\n Math.max(0, source.decimals || 18),\n Decimal.ROUND_DOWN\n );\n if (tokenAmount.lte(0)) continue;\n\n const actualUsd = getUsdForTokenAmount(source, tokenAmount);\n allocated.push({\n ...source,\n userAmount: tokenAmount.toFixed(),\n userAmountMode: \"token\",\n userAmountUsd: actualUsd\n .toDecimalPlaces(6, Decimal.ROUND_DOWN)\n .toFixed(),\n });\n remainingUsd = remainingUsd.minus(targetUsd);\n }\n\n return allocated;\n }\n\n let remainingTokenAmount = rawAmount;\n\n for (const source of sortedSources) {\n if (remainingTokenAmount.lte(0)) break;\n\n const availableTokenAmount = getTokenBalanceAmount(source);\n if (availableTokenAmount.lte(0)) continue;\n\n const tokenAmount = Decimal.min(\n remainingTokenAmount,\n availableTokenAmount\n ).toDecimalPlaces(Math.max(0, source.decimals || 18), Decimal.ROUND_DOWN);\n if (tokenAmount.lte(0)) continue;\n\n const actualUsd = getUsdForTokenAmount(source, tokenAmount);\n allocated.push({\n ...source,\n userAmount: tokenAmount.toFixed(),\n userAmountMode: \"token\",\n userAmountUsd: actualUsd\n .toDecimalPlaces(6, Decimal.ROUND_DOWN)\n .toFixed(),\n });\n remainingTokenAmount = remainingTokenAmount.minus(tokenAmount);\n }\n\n return allocated;\n };\n\n const getExactInSourceTokens = (\n tokens: SwapTokenOption[],\n fallbackAmount?: string\n ) =>\n tokens\n .flatMap((token) =>\n token.isUnified\n ? allocateUnifiedExactInToken(token, fallbackAmount)\n : [token]\n )\n .filter(hasMinimumSourceUsdBalance);\n\n const hasPositiveDecimalInput = (value: unknown) =>\n Boolean(parseFiatNumber(value)?.gt(0));\n\n const hasPositiveSourceSpend = (token: SwapTokenOption) =>\n hasPositiveDecimalInput(token.userAmount) ||\n hasPositiveDecimalInput(token.userAmountUsd);\n\n const getReadyExactInSourceTokens = (tokens: SwapTokenOption[]) =>\n getExactInSourceTokens(tokens).filter(\n (token) =>\n Boolean(token.chainId && token.contractAddress) &&\n hasPositiveDecimalInput(token.userAmount)\n );\n\n const hasReadyExactInSwapInput = (\n tokens: SwapTokenOption[],\n destination?: SwapTokenOption\n ) =>\n Boolean(\n destination?.chainId &&\n destination.contractAddress &&\n getReadyExactInSourceTokens(tokens).length > 0\n );\n\n const getExpandedSourceTokens = (tokens: SwapTokenOption[]) => {\n const expanded = tokens.flatMap((token) =>\n token.isUnified && token.sourceTokens?.length\n ? token.sourceTokens\n : [token]\n );\n const seen = new Set();\n return expanded.filter((token) => {\n if (!token.chainId || !token.contractAddress) return false;\n if (\n !isSwapSupportedBySdkChainList(\n token.chainId,\n swapSupportedChainsAndTokens\n )\n ) {\n return false;\n }\n const key = `${token.chainId}-${token.contractAddress.toLowerCase()}`;\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n });\n };\n\n const getNativeGasBalanceForChain = (chainId: number) => {\n const nativeSymbol =\n CHAIN_METADATA[chainId]?.nativeCurrency?.symbol?.toUpperCase();\n let balance = new Decimal(0);\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== chainId) continue;\n const breakdownSymbol = (\n breakdown.symbol ??\n asset.symbol ??\n \"\"\n ).toUpperCase();\n const assetSymbol = (asset.symbol ?? \"\").toUpperCase();\n const isNativeBalance =\n isNativeTokenAddress(breakdown.contractAddress) ||\n Boolean(\n nativeSymbol &&\n (breakdownSymbol === nativeSymbol || assetSymbol === nativeSymbol)\n );\n\n if (!isNativeBalance) continue;\n balance = balance.plus(\n parseFiatNumber(breakdown.balance) ?? new Decimal(0)\n );\n }\n }\n\n return balance;\n };\n\n const hasGasForSource = (token: SwapTokenOption) => {\n if (!token.chainId || !token.contractAddress) return false;\n const tokenBalance = parseFiatNumber(token.balance) ?? new Decimal(0);\n if (tokenBalance.lte(0)) return false;\n if (isNativeTokenAddress(token.contractAddress)) return true;\n return getNativeGasBalanceForChain(token.chainId).gt(0);\n };\n\n const getGasCapableBalanceSourceTokens = () => {\n const tokens: SwapTokenOption[] = [];\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const chainId = breakdown.chain?.id;\n const contractAddress = breakdown.contractAddress;\n const balance = parseFiatNumber(breakdown.balance) ?? new Decimal(0);\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n if (\n !chainId ||\n !contractAddress ||\n balance.lte(0) ||\n !fiatBalance ||\n fiatBalance.lt(minimumSourceUsd)\n )\n continue;\n\n const chainMeta = CHAIN_METADATA[chainId];\n const symbol = breakdown.symbol ?? asset.symbol;\n tokens.push({\n chainId,\n chainLogo: chainMeta?.logo ?? breakdown.chain?.logo,\n chainName: getShortChainName(\n chainId,\n chainMeta?.name ?? breakdown.chain?.name\n ),\n contractAddress,\n decimals: breakdown.decimals ?? asset.decimals ?? 18,\n logo: asset.logo ?? \"\",\n name: symbol,\n symbol,\n balance: `${breakdown.balance} ${symbol}`,\n balanceInFiat:\n fiatBalance !== undefined\n ? `$${fiatBalance.toDecimalPlaces(2).toFixed()}`\n : \"$0.00\",\n });\n }\n }\n\n return getExpandedSourceTokens(tokens).filter(hasGasForSource);\n };\n\n const getMinimumBalanceSourceTokens = () =>\n filterMinimumSourceUsdTokens(\n getExpandedSourceTokens(\n swapBalance\n ? deriveTokenOptions(swapBalance, swapSupportedChainsAndTokens)\n : []\n )\n );\n const getHeldDestinationTokenOption = () => {\n if (!toToken?.chainId || !toToken.contractAddress) return undefined;\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const chainId = breakdown.chain?.id;\n if (chainId !== toToken.chainId) continue;\n\n const breakdownAddress = breakdown.contractAddress;\n const addressMatches =\n breakdownAddress &&\n (breakdownAddress.toLowerCase() ===\n toToken.contractAddress.toLowerCase() ||\n (isNativeTokenAddress(breakdownAddress) &&\n isNativeTokenAddress(toToken.contractAddress)));\n const symbolMatches =\n (breakdown.symbol ?? asset.symbol ?? \"\").toUpperCase() ===\n toToken.symbol.toUpperCase();\n\n if (!addressMatches && !symbolMatches) continue;\n\n const balanceAmount = parseFiatNumber(breakdown.balance);\n if (!balanceAmount || balanceAmount.lte(0)) continue;\n\n const chainMeta = CHAIN_METADATA[chainId];\n const symbol = breakdown.symbol ?? asset.symbol ?? toToken.symbol;\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n\n return {\n chainId,\n chainLogo:\n chainMeta?.logo ?? breakdown.chain?.logo ?? toToken.chainLogo,\n chainName: getShortChainName(\n chainId,\n chainMeta?.name ?? breakdown.chain?.name ?? toToken.chainName\n ),\n contractAddress: breakdown.contractAddress ?? toToken.contractAddress,\n decimals:\n breakdown.decimals ?? asset.decimals ?? toToken.decimals ?? 18,\n logo: asset.logo ?? toToken.logo,\n name: symbol,\n symbol,\n balance: `${breakdown.balance} ${symbol}`,\n balanceInFiat:\n fiatBalance !== undefined\n ? `$${fiatBalance.toDecimalPlaces(2).toFixed()}`\n : \"$0.00\",\n } satisfies SwapTokenOption;\n }\n }\n\n return undefined;\n };\n\n const getDepositDestinationForSourceSelection = () => {\n const destination =\n activeMode === \"deposit\" ? selectedOpportunity : toToken;\n const chainId = destination?.chainId;\n const tokenAddress =\n activeMode === \"deposit\"\n ? selectedOpportunity?.tokenAddress\n : toToken?.contractAddress;\n const tokenSymbol =\n activeMode === \"deposit\"\n ? selectedOpportunity?.tokenSymbol\n : toToken?.symbol;\n\n if (!chainId || !tokenAddress || !tokenSymbol) return undefined;\n\n return {\n chainId,\n tokenAddress: tokenAddress as `0x${string}`,\n tokenSymbol,\n };\n };\n const getDestinationSourceIdForDeposit = () => {\n const destination = getDepositDestinationForSourceSelection();\n return destination\n ? getDepositSourceId(destination.tokenAddress, destination.chainId)\n : undefined;\n };\n const getDepositSourceTargetUsd = () => {\n if (activeMode !== \"deposit\") return undefined;\n const requestedUsd = depositUsdDecimal;\n if (!requestedUsd || requestedUsd.lte(0)) return undefined;\n\n const coverage = getExactOutDestinationBalanceCoverage({\n requestedAmount: depositTokenAmountForQuote,\n requestedUsd,\n token: toToken,\n });\n return Decimal.max(\n requestedUsd.minus(coverage?.usd ?? new Decimal(0)),\n new Decimal(0)\n );\n };\n\n const getDepositSourceIdsFromTokens = (tokens: SwapTokenOption[]) =>\n getExpandedSourceTokens(tokens)\n .filter((token) => token.chainId && token.contractAddress)\n .map((token) =>\n getDepositSourceId(token.contractAddress, token.chainId!)\n );\n\n const getDepositTokenOptionsBySourceId = () => {\n const map = new Map();\n const sourceTokens = [\n ...(swapBalance\n ? deriveTokenOptions(swapBalance, swapSupportedChainsAndTokens)\n : []),\n ...fromTokens,\n ];\n\n for (const token of getExpandedSourceTokens(sourceTokens)) {\n if (!token.chainId || !token.contractAddress) continue;\n const id = getDepositSourceId(token.contractAddress, token.chainId);\n if (!map.has(id)) {\n map.set(id, {\n ...token,\n userAmount: \"\",\n });\n }\n }\n\n return map;\n };\n\n const getDepositSourceTokensForIds = (sourceIds: string[]) => {\n const tokenBySourceId = getDepositTokenOptionsBySourceId();\n return sourceIds\n .map((sourceId) => tokenBySourceId.get(sourceId))\n .filter((token): token is SwapTokenOption => Boolean(token))\n .map((token) => ({ ...token, userAmount: \"\" }));\n };\n\n const getResolvedDepositSourceSelection = (options?: {\n filter?: DepositSourceFilter;\n selectedTokens?: SwapTokenOption[];\n isManualSelection?: boolean;\n targetAmountUsd?: Decimal;\n }) => {\n const destination = getDepositDestinationForSourceSelection();\n if (!destination) {\n return { sourcePoolIds: [], selectedSourceIds: [], fromSources: [] };\n }\n\n const manualSelection =\n options?.isManualSelection ?? sourceSelectionTouched;\n const selectedTokensForResolution = options?.selectedTokens ?? fromTokens;\n const selectedSourceIds = getDepositSourceIdsFromTokens(\n selectedTokensForResolution\n );\n const destinationSourceId = getDestinationSourceIdForDeposit();\n const targetAmountUsd =\n options?.targetAmountUsd ??\n (activeMode === \"deposit\"\n ? getDepositSourceTargetUsd()\n : activeMode === \"send\"\n ? new Decimal(sendAmountUsd || 0)\n : undefined);\n\n return resolveDepositSourceSelection({\n swapBalance,\n destination,\n filter: manualSelection\n ? \"custom\"\n : (options?.filter ?? depositSourceFilter),\n selectedSourceIds,\n isManualSelection: manualSelection,\n minimumBalanceUsd: minimumSourceUsd.toNumber(),\n targetAmountUsd: targetAmountUsd?.toNumber(),\n excludedSourceIds: destinationSourceId ? [destinationSourceId] : [],\n });\n };\n\n const getExactOutSourceTokens = (\n mode: \"all\" | \"selected\" = exactOutQuoteSourceModeRef.current,\n targetAmountUsd?: Decimal\n ) => {\n if (activeMode === \"deposit\") {\n const selection = getResolvedDepositSourceSelection({ targetAmountUsd });\n return getDepositSourceTokensForIds(\n mode === \"all\" ? selection.sourcePoolIds : selection.selectedSourceIds\n );\n }\n\n if (activeMode === \"send\" && mode === \"selected\" && fromTokens.length > 0) {\n return filterMinimumSourceUsdTokens(\n getExpandedSourceTokens(fromTokens)\n ).filter(hasGasForSource);\n }\n\n return getGasCapableBalanceSourceTokens();\n };\n\n const buildExplicitSourcesPayload = (tokens: SwapTokenOption[]) => {\n const eligibleTokens = filterMinimumSourceUsdTokens(tokens).filter(\n (token) => token.chainId && token.contractAddress\n );\n return {\n sources: eligibleTokens.map((token) => ({\n chainId: token.chainId!,\n tokenAddress: token.contractAddress as `0x${string}`,\n })),\n };\n };\n\n const getSdkSourceKey = (source: {\n chainId: number;\n tokenAddress: `0x${string}`;\n }) => {\n const normalizedAddress = isNativeTokenAddress(source.tokenAddress)\n ? zeroAddress\n : source.tokenAddress.toLowerCase();\n return `${source.chainId}:${normalizedAddress}`;\n };\n\n const dedupeSdkSources = (\n sources: Array<\n | {\n chainId: number;\n tokenAddress: `0x${string}`;\n }\n | undefined\n >\n ) => {\n const seen = new Set();\n return sources.filter(\n (\n source\n ): source is {\n chainId: number;\n tokenAddress: `0x${string}`;\n } => {\n if (!source) return false;\n const key = getSdkSourceKey(source);\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n }\n );\n };\n\n const getHeldDestinationTokenSource = () => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n if (\n !heldDestinationToken?.chainId ||\n !heldDestinationToken.contractAddress\n ) {\n return undefined;\n }\n\n return {\n chainId: heldDestinationToken.chainId,\n tokenAddress: heldDestinationToken.contractAddress as `0x${string}`,\n };\n };\n\n const shouldSendExactOutSourceAllowlist = () => {\n if (activeMode === \"deposit\") {\n return sourceSelectionTouched || depositSourceFilter !== \"all\";\n }\n\n if (activeMode === \"send\") {\n return exactOutQuoteSourceModeRef.current === \"selected\";\n }\n\n return true;\n };\n\n const buildExactOutSourcesPayload = (tokens: SwapTokenOption[]) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n return buildExplicitSourcesPayload(tokens);\n }\n\n const sourceTokens = shouldSendExactOutSourceAllowlist()\n ? tokens\n : getExactOutSourceTokens(\"all\");\n const explicitSources = buildExplicitSourcesPayload(sourceTokens).sources;\n const sources = dedupeSdkSources([\n ...explicitSources,\n getHeldDestinationTokenSource(),\n ]);\n\n return sources.length > 0 ? { sources } : {};\n };\n\n const buildPredictiveExactOutSources = async (requiredSourceUsd: Decimal) => {\n if (requiredSourceUsd.lte(0)) return [];\n\n const destinationKey = getTokenSelectionKey(toToken);\n const candidates = sortExactOutSourcesBySdkPriority(\n getExactOutSourceTokens(\n exactOutQuoteSourceModeRef.current,\n requiredSourceUsd\n )\n .filter((token) => getTokenSelectionKey(token) !== destinationKey)\n .filter(hasMinimumSourceUsdBalance)\n );\n const sources: SwapTokenOption[] = [];\n let remainingUsd = requiredSourceUsd;\n\n for (const token of candidates) {\n if (remainingUsd.lte(0)) break;\n\n const availableUsd = getTokenBalanceUsd(token);\n if (availableUsd.lt(minimumSourceUsd)) continue;\n\n const rate = await resolveUsdRateForToken(token);\n if (rate.lte(0)) continue;\n\n const targetUsd = Decimal.min(remainingUsd, availableUsd);\n const tokenAmount = targetUsd\n .div(rate)\n .toDecimalPlaces(Math.max(0, token.decimals || 18), Decimal.ROUND_DOWN);\n if (tokenAmount.lte(0)) continue;\n\n sources.push({\n ...token,\n userAmount: tokenAmount.toFixed(),\n userAmountMode: \"token\",\n userAmountUsd: targetUsd\n .toDecimalPlaces(6, Decimal.ROUND_DOWN)\n .toFixed(),\n });\n remainingUsd = remainingUsd.minus(targetUsd);\n }\n\n return remainingUsd.gt(0.01) ? [] : sources;\n };\n\n const getErrorText = (error: unknown) => {\n const err = error as any;\n const parts = [\n err?.message,\n typeof error === \"string\" ? error : undefined,\n err?.code,\n ];\n\n try {\n if (err?.data) parts.push(JSON.stringify(err.data));\n } catch {\n // Ignore non-serializable SDK error metadata.\n }\n\n return parts.filter(Boolean).join(\" \");\n };\n\n const isInsufficientSourcesError = (error: unknown) => {\n const err = error as any;\n const message = getErrorText(error).toLowerCase();\n\n return (\n err?.code === ERROR_CODES.INSUFFICIENT_BALANCE ||\n message.includes(\"insufficient balance\") ||\n message.includes(\"sources are not enough\") ||\n (message.includes(\"source\") && message.includes(\"not enough\"))\n );\n };\n\n const isViemInvalidDecimalError = (error: unknown) =>\n getErrorText(error).toLowerCase().includes(\"not a valid decimal number\");\n\n const parseLabeledErrorDecimal = (text: string, label: string) => {\n const match = text.match(\n new RegExp(`${label}\\\\s*:\\\\s*\\\\$?\\\\s*([0-9][0-9,]*(?:\\\\.[0-9]+)?)`, \"i\")\n );\n return match ? parseFiatNumber(match[1]) : undefined;\n };\n\n const getExactOutRequestedUsd = () => {\n const amountNumber = parseFiatNumber(amount);\n if (!amountNumber || amountNumber.lte(0) || !toToken?.symbol) {\n return undefined;\n }\n\n if (activeMode === \"deposit\" && depositAmountMode === \"usd\") {\n return amountNumber;\n }\n\n const fiatValue = getFiatValue(amountNumber.toNumber(), toToken.symbol);\n return Number.isFinite(fiatValue) && fiatValue > 0\n ? new Decimal(fiatValue)\n : undefined;\n };\n\n const getExactOutAvailableSourceUsd = (\n sourceTokensOverride?: SwapTokenOption[]\n ): Decimal => {\n const sumTokensWithDestinationCredit = (\n tokens: SwapTokenOption[]\n ): Decimal => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n const heldDestinationKey = getTokenSelectionKey(heldDestinationToken);\n let hasHeldDestinationToken = false;\n\n const sourceTotal = tokens.reduce((sum, token) => {\n const value = parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n const isHeldDestinationToken =\n heldDestinationKey &&\n getTokenSelectionKey(token) === heldDestinationKey;\n if (isHeldDestinationToken) {\n hasHeldDestinationToken = true;\n }\n\n return value.gt(0) &&\n (isHeldDestinationToken || value.gte(minimumSourceUsd))\n ? sum.plus(value)\n : sum;\n }, new Decimal(0));\n\n if (!heldDestinationToken || hasHeldDestinationToken) {\n return sourceTotal;\n }\n\n const heldDestinationUsd =\n parseFiatNumber(heldDestinationToken.balanceInFiat) ?? new Decimal(0);\n return heldDestinationUsd.gt(0)\n ? sourceTotal.plus(heldDestinationUsd)\n : sourceTotal;\n };\n\n if (\n sourceTokensOverride ||\n exactOutQuoteSourceModeRef.current === \"selected\"\n ) {\n const selectedSourceTokens = sourceTokensOverride ?? fromTokens;\n return sumTokensWithDestinationCredit(selectedSourceTokens);\n }\n\n return getExactOutTotalSourceBalanceUsd();\n };\n const getExactOutTotalSourceBalanceUsd = (): Decimal => {\n const allSourceTotal: Decimal = getExactOutAvailableSourceUsd(\n getMinimumBalanceSourceTokens()\n );\n\n return allSourceTotal.gt(0) ? allSourceTotal : getSwapBalanceTotalUsd();\n };\n\n const getExactOutIntentSourceUsd = () => {\n const intentSourceUsd = (intentData?.sources ?? []).reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0)\n );\n return intentSourceUsd.gt(0) ? intentSourceUsd : undefined;\n };\n\n const getExactOutHeldDestinationUsd = () => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n const value = parseFiatNumber(heldDestinationToken?.balanceInFiat);\n return value && value.gt(0) ? value : new Decimal(0);\n };\n\n const exactOutIntentIncludesHeldDestination = () => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n const heldDestinationKey = getTokenSelectionKey(heldDestinationToken);\n if (!heldDestinationKey) return false;\n\n return (intentData?.sources ?? []).some((source) => {\n const chainId = source.chain?.id;\n const tokenAddress = source.token?.contractAddress;\n if (!chainId || !tokenAddress) return false;\n\n return (\n getTokenSelectionKey({\n chainId,\n contractAddress: tokenAddress,\n } as SwapTokenOption) === heldDestinationKey\n );\n });\n };\n\n const getExactOutRequiredSourceUsd = () => {\n const intentSourceUsd = getExactOutIntentSourceUsd();\n if (intentSourceUsd) return intentSourceUsd;\n return undefined;\n };\n\n const getExactOutRequiredFundingUsd = () => {\n const sourceRequiredUsd = getExactOutRequiredSourceUsd();\n if (sourceRequiredUsd) {\n const destinationCreditUsd = exactOutIntentIncludesHeldDestination()\n ? new Decimal(0)\n : getExactOutHeldDestinationUsd();\n return sourceRequiredUsd.plus(destinationCreditUsd);\n }\n\n return getExactOutRequestedUsd();\n };\n\n const getExactInSourceDeficitUsd = () => {\n if (swapType !== \"exactIn\" || fromTokens.length === 0) return undefined;\n\n return fromTokens.reduce((sum, token) => {\n const requestedAmount = parseFiatNumber(token.userAmount);\n if (!requestedAmount || requestedAmount.lte(0)) return sum;\n\n if (token.userAmountMode === \"usd\") {\n const availableUsd = parseFiatNumber(token.balanceInFiat);\n if (!availableUsd || requestedAmount.lte(availableUsd)) return sum;\n return sum.plus(requestedAmount.minus(availableUsd));\n }\n\n const availableTokenAmount = parseFiatNumber(token.balance);\n if (!availableTokenAmount || requestedAmount.lte(availableTokenAmount)) {\n return sum;\n }\n\n const missingTokenAmount = requestedAmount.minus(availableTokenAmount);\n const fiatBalance = parseFiatNumber(token.balanceInFiat);\n if (fiatBalance && availableTokenAmount.gt(0)) {\n return sum.plus(\n missingTokenAmount.mul(fiatBalance.div(availableTokenAmount))\n );\n }\n\n return sum;\n }, new Decimal(0));\n };\n\n const buildInsufficientSourcesIssue = (error: unknown): SwapQuoteIssue => {\n const errorText = getErrorText(error);\n const details =\n (error as any)?.data?.details ?? (error as any)?.details ?? {};\n const requiredFromError =\n parseFiatNumber(\n details.requiredUsd ??\n details.requiredUSD ??\n details.requiredAmountUsd ??\n details.requiredAmount ??\n details.required\n ) ?? parseLabeledErrorDecimal(errorText, \"required\");\n const availableFromError =\n parseFiatNumber(\n details.availableUsd ??\n details.availableUSD ??\n details.availableAmountUsd ??\n details.availableAmount ??\n details.available\n ) ?? parseLabeledErrorDecimal(errorText, \"available\");\n const requestedUsd = getExactOutRequiredFundingUsd();\n const availableUsd = getExactOutAvailableSourceUsd();\n const exactInSourceDeficitUsd = getExactInSourceDeficitUsd();\n\n let missingUsd =\n exactInSourceDeficitUsd && exactInSourceDeficitUsd.gt(0)\n ? exactInSourceDeficitUsd\n : requiredFromError && availableFromError\n ? requiredFromError.minus(availableFromError)\n : undefined;\n\n if (\n requestedUsd &&\n (!missingUsd || missingUsd.lte(0) || missingUsd.gt(requestedUsd.mul(5)))\n ) {\n missingUsd = requestedUsd.minus(availableUsd);\n }\n\n if (missingUsd && missingUsd.gt(0)) {\n const formattedMissing =\n missingUsd.gt(0) && missingUsd.lt(0.01)\n ? \"<$0.01\"\n : formatUsdDisplay(missingUsd);\n\n return {\n type: \"insufficientSources\",\n missingUsd: missingUsd.toDecimalPlaces(2).toFixed(),\n message: `Need ${formattedMissing} more across your assets`,\n };\n }\n\n return {\n type: \"insufficientSources\",\n message: \"Add more source balance across your assets\",\n };\n };\n\n const buildExactOutSourceBalanceIssue = (\n sourceTokensOverride?: SwapTokenOption[]\n ): SwapQuoteIssue | null => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return null;\n const requestedUsd = getExactOutRequiredFundingUsd();\n const availableUsd = getExactOutAvailableSourceUsd(sourceTokensOverride);\n if (!requestedUsd || requestedUsd.lte(0) || !availableUsd) return null;\n\n const missingUsd = requestedUsd.minus(availableUsd);\n if (missingUsd.lte(0.01)) return null;\n\n const formattedMissing =\n missingUsd.gt(0) && missingUsd.lt(0.01)\n ? \"<$0.01\"\n : formatUsdDisplay(missingUsd);\n\n return {\n type: \"insufficientSources\",\n missingUsd: missingUsd.toDecimalPlaces(2).toFixed(),\n message: `Need ${formattedMissing} more across your assets`,\n };\n };\n\n const isNativeTokenAddress = (address?: string) =>\n !address ||\n address.toLowerCase() === \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\" ||\n address.toLowerCase() === \"0x0000000000000000000000000000000000000000\";\n\n const formatReadableTokenAmount = (rawAmount: bigint, decimals: number) =>\n new Decimal(rawAmount.toString())\n .div(new Decimal(10).pow(decimals))\n .toFixed();\n\n const formatReadableTokenBalanceAmount = (\n rawAmount: bigint,\n decimals: number\n ) =>\n new Decimal(rawAmount.toString())\n .div(new Decimal(10).pow(decimals))\n .toDecimalPlaces(6)\n .toFixed();\n\n const trimDecimalString = (value: string) =>\n value.replace(/(\\.\\d*?)0+$/, \"$1\").replace(/\\.$/, \"\");\n\n const buildSourceTokenSnapshotMap = useCallback(\n (balances: UserAsset[] | null | undefined) => {\n const snapshots = new Map();\n\n for (const asset of balances ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const chainId = breakdown.chain?.id;\n const contractAddress = breakdown.contractAddress;\n const symbol = breakdown.symbol ?? asset.symbol;\n\n if (\n !chainId ||\n !contractAddress ||\n !symbol ||\n !isSwapSupportedBySdkChainList(\n chainId,\n swapSupportedChainsAndTokens\n )\n ) {\n continue;\n }\n\n const chainMeta = CHAIN_METADATA[chainId];\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n const snapshot: SwapTokenOption = {\n balance: `${breakdown.balance ?? \"0\"} ${symbol}`,\n balanceInFiat: fiatBalance\n ? formatUsdDisplay(fiatBalance)\n : \"$0.00\",\n chainId,\n chainLogo: chainMeta?.logo ?? breakdown.chain?.logo,\n chainName: getShortChainName(\n chainId,\n chainMeta?.name ?? breakdown.chain?.name\n ),\n contractAddress,\n decimals: breakdown.decimals ?? asset.decimals ?? 18,\n logo: asset.logo ?? \"\",\n name: symbol,\n symbol,\n };\n snapshots.set(getTokenSelectionKey(snapshot), snapshot);\n }\n }\n\n return snapshots;\n },\n [swapSupportedChainsAndTokens]\n );\n\n const patchSourceTokensWithBalances = useCallback(\n (tokens: SwapTokenOption[], balances: UserAsset[]) => {\n const snapshots = buildSourceTokenSnapshotMap(balances);\n\n const updateToken = (token: SwapTokenOption): SwapTokenOption => {\n const preservedAmounts = {\n userAmount: token.userAmount,\n userAmountMode: token.userAmountMode,\n userAmountUsd: token.userAmountUsd,\n };\n\n if (token.isUnified) {\n const sourceTokens = (token.sourceTokens ?? []).map(updateToken);\n const totalBalance = sourceTokens.reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber(source.balance) ?? new Decimal(0)),\n new Decimal(0)\n );\n const totalFiat = sourceTokens.reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber(source.balanceInFiat) ?? new Decimal(0)),\n new Decimal(0)\n );\n\n return {\n ...token,\n ...preservedAmounts,\n balance: totalBalance.toDecimalPlaces(8).toFixed(),\n balanceInFiat: formatUsdDisplay(totalFiat),\n sourceTokens,\n };\n }\n\n const snapshot = snapshots.get(getTokenSelectionKey(token));\n if (!snapshot) {\n return {\n ...token,\n ...preservedAmounts,\n balance: `0 ${token.symbol}`,\n balanceInFiat: \"$0.00\",\n chainLogo:\n token.chainLogo ??\n (token.chainId ? CHAIN_METADATA[token.chainId]?.logo : undefined),\n chainName: getShortChainName(token.chainId, token.chainName),\n };\n }\n\n return {\n ...token,\n ...snapshot,\n ...preservedAmounts,\n };\n };\n\n return tokens.map(updateToken);\n },\n [buildSourceTokenSnapshotMap]\n );\n\n const refreshSelectedSourceBalances = useCallback(async () => {\n const refreshedBalance = await fetchSwapBalance();\n const balances = refreshedBalance ?? swapBalance;\n if (!balances) return;\n\n setFromTokens((current) =>\n current.length === 0\n ? current\n : patchSourceTokensWithBalances(current, balances)\n );\n setSourceSelectionRevision((current) => current + 1);\n }, [fetchSwapBalance, patchSourceTokensWithBalances, swapBalance]);\n\n const receiveMaxSafetyMultiplier = new Decimal(\"0.9\");\n const currentSwapEntry =\n currentSwapId !== null\n ? swapHistory.find((entry) => entry.id === currentSwapId)\n : undefined;\n\n const scheduleTerminalBalanceRefresh = () => {\n if (terminalBalanceRefreshTimerRef.current) {\n clearTimeout(terminalBalanceRefreshTimerRef.current);\n }\n\n terminalBalanceRefreshTimerRef.current = setTimeout(() => {\n terminalBalanceRefreshTimerRef.current = null;\n void fetchSwapBalance();\n }, BALANCE_REFRESH_AFTER_TERMINAL_MS);\n };\n\n const patchSwapHistoryEntry = (\n id: string | null | undefined,\n patch: Partial\n ) => {\n if (!id) return;\n setSwapHistory((prev) =>\n sortSwapHistoryEntries(\n prev.map((entry) => (entry.id === id ? { ...entry, ...patch } : entry))\n )\n );\n };\n\n const patchCurrentSwapHistoryEntry = (patch: Partial) => {\n patchSwapHistoryEntry(currentSwapIdRef.current, patch);\n };\n\n const patchCurrentIntentExplorerUrl = (url?: string | null) => {\n if (!isHttpUrl(url)) return;\n if (intentUrlRef.current === url) return;\n\n intentUrlRef.current = url;\n const intentId = extractIntentIdFromUrl(url);\n patchCurrentSwapHistoryEntry({\n intentExplorerUrl: url,\n ...(intentId ? { intentId } : {}),\n });\n };\n\n const resetExplorerUrls = () => {\n const next = { sourceExplorerUrl: null, destinationExplorerUrl: null };\n explorerUrlsRef.current = next;\n setExplorerUrls(next);\n };\n\n const mergeExplorerUrls = (\n patch: Partial<{\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n }>\n ) => {\n const next = { ...explorerUrlsRef.current, ...patch };\n explorerUrlsRef.current = next;\n setExplorerUrls(next);\n patchCurrentSwapHistoryEntry({\n sourceExplorerUrl: next.sourceExplorerUrl,\n finalExplorerUrl: next.destinationExplorerUrl ?? next.sourceExplorerUrl,\n });\n };\n\n const resetProgressEvents = () => {\n progressEventsRef.current = [];\n setProgressEvents((current) => (current.length === 0 ? current : []));\n setFailedProgressStep((current) => (current === null ? current : null));\n };\n\n const appendProgressEvent = (\n name: string,\n step: SwapStepType | BridgeStepType | undefined,\n defaultCompleted: boolean,\n event?: unknown\n ) => {\n if (!step) return;\n const completed =\n typeof (step as any).completed === \"boolean\"\n ? Boolean((step as any).completed)\n : defaultCompleted;\n\n setProgressEvents((prev) => {\n const next = [\n ...prev,\n {\n id: `${Date.now()}-${prev.length}-${(step as any).typeID ?? (step as any).type ?? name}`,\n name,\n completed,\n event,\n step,\n },\n ];\n progressEventsRef.current = next;\n return next;\n });\n };\n\n const appendProgressListEvent = (\n name: string,\n stepList: Array\n ) => {\n if (stepList.length === 0) return;\n\n setProgressEvents((prev) => {\n const next = [\n ...prev,\n {\n id: `${Date.now()}-${prev.length}-${name}`,\n name,\n completed: false,\n step: stepList[0],\n steps: stepList,\n },\n ];\n progressEventsRef.current = next;\n return next;\n });\n };\n\n const startSwapHistoryEntry = () => {\n const id = `${Date.now()}-${swapRunIdRef.current}`;\n const now = Date.now();\n const resolvedToToken =\n toToken && destinationBalance\n ? { ...toToken, balance: destinationBalance }\n : toToken;\n const entry: SwapHistoryEntry = {\n id,\n mode: activeMode,\n status: \"pending\",\n createdAt: now,\n startedAt: now,\n intentData,\n fromTokens,\n toToken: resolvedToToken,\n requestedToAmount:\n activeMode === \"deposit\" || activeMode === \"send\"\n ? previewDestinationAmount\n : undefined,\n requestedToValue:\n activeMode === \"deposit\" || activeMode === \"send\"\n ? previewToAmountUsd\n : undefined,\n recipientAddress: transferRecipientAddress,\n opportunity: selectedOpportunity,\n feeUsd: intentFeeUsd,\n sourceExplorerUrl: null,\n finalExplorerUrl: null,\n intentExplorerUrl: isHttpUrl(intentUrlRef.current)\n ? intentUrlRef.current\n : null,\n intentId: extractIntentIdFromUrl(intentUrlRef.current),\n autoRefundAvailable: false,\n };\n\n currentSwapStartedAtRef.current = 0;\n currentSwapIdRef.current = id;\n setCurrentSwapId(id);\n setSwapHistory((prev) => sortSwapHistoryEntries([entry, ...prev]));\n return id;\n };\n\n const finishCurrentSwapHistoryEntry = (\n status: \"fulfilled\" | \"failed\" | \"timeout\",\n patch: Partial = {}\n ) => {\n const now = Date.now();\n const startedAt = currentSwapStartedAtRef.current || now;\n patchSwapHistoryEntry(currentSwapIdRef.current, {\n status,\n endedAt: now,\n durationSeconds: Math.max(1, Math.round((now - startedAt) / 1000)),\n sourceExplorerUrl: explorerUrlsRef.current.sourceExplorerUrl,\n finalExplorerUrl:\n explorerUrlsRef.current.destinationExplorerUrl ??\n explorerUrlsRef.current.sourceExplorerUrl,\n ...patch,\n });\n scheduleTerminalBalanceRefresh();\n };\n\n const markSwapExecutionStarted = () => {\n if (currentSwapStartedAtRef.current > 0) return;\n const now = Date.now();\n currentSwapStartedAtRef.current = now;\n patchCurrentSwapHistoryEntry({ startedAt: now });\n };\n\n const enterSkippedSwapProgress = () => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n\n const shouldInitializeProgress = swapStepRef.current !== \"progress\";\n if (!currentSwapIdRef.current) {\n onStart?.();\n startSwapHistoryEntry();\n }\n\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setPreviewQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setSwapQuoteIssue(null);\n\n if (shouldInitializeProgress) {\n resetProgressEvents();\n swapStepsListRef.current = [];\n resetSteps();\n swapStepRef.current = \"progress\";\n setSwapStep(\"progress\");\n }\n };\n\n const cachePredictiveBaselineFromIntent = (intent: SwapIntentData) => {\n const destinationAmount = parseFiatNumber(intent.destination?.amount);\n const destinationValue = parseFiatNumber(intent.destination?.value);\n const sourceUsd = (intent.sources ?? []).reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0)\n );\n\n if (!destinationAmount || destinationAmount.lte(0)) return;\n\n const destinationUsdRate =\n destinationValue && destinationValue.gt(0)\n ? destinationValue.div(destinationAmount)\n : getUsdRateForSymbol(intent.destination?.token?.symbol);\n if (destinationUsdRate.lte(0)) return;\n\n cacheSymbolUsdRate(intent.destination?.token?.symbol, destinationUsdRate);\n\n const key = getPredictiveQuoteCacheKey();\n if (!key) return;\n\n const baseline: PredictiveQuoteBaseline = {\n destinationUsdRate: destinationUsdRate.toDecimalPlaces(18).toFixed(),\n updatedAt: Date.now(),\n };\n\n if (activeMode === \"swap\" && swapType === \"exactIn\" && sourceUsd.gt(0)) {\n baseline.exactInDestinationAmountPerSourceUsd = destinationAmount\n .div(sourceUsd)\n .toDecimalPlaces(18)\n .toFixed();\n }\n\n const resolvedDestinationValue =\n destinationValue && destinationValue.gt(0)\n ? destinationValue\n : destinationAmount.mul(destinationUsdRate);\n if (\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n resolvedDestinationValue.gt(0) &&\n sourceUsd.gt(0)\n ) {\n baseline.exactOutSourceUsdPerDestinationUsd = sourceUsd\n .div(resolvedDestinationValue)\n .toDecimalPlaces(18)\n .toFixed();\n }\n\n predictiveQuoteCacheRef.current[key] = baseline;\n };\n\n const applySwapIntent = useCallback(\n (intent: SwapIntentData) => {\n const sortedIntent = {\n ...intent,\n sources: sortIntentSourcesByUsdDesc(intent.sources ?? []),\n };\n const enrichedIntent = enrichSwapIntentTokenMetadata(sortedIntent);\n const sortedIntentSourceTokens = sortSwapTokensByUsdDesc(\n (enrichedIntent.sources ?? []).map(buildIntentSourceToken)\n );\n\n lastSwapIntentRefreshAtRef.current = Date.now();\n lastIntentSourceTokensRef.current = sortedIntentSourceTokens;\n if (\n !sourceSelectionTouched &&\n (activeMode === \"send\" ||\n (activeMode === \"deposit\" && swapType === \"exactOut\"))\n ) {\n lastAutoIntentSourceTokensRef.current = sortedIntentSourceTokens;\n }\n cacheDestinationUsdRateFromIntent(enrichedIntent);\n cachePredictiveBaselineFromIntent(enrichedIntent);\n setIntentData(enrichedIntent);\n setIntentToAmount(enrichedIntent.destination?.amount || undefined);\n setSwapQuoteIssue(null);\n\n if (\n !sourceSelectionTouched &&\n swapStepRef.current !== \"choose-swap-asset\" &&\n (activeMode === \"send\" ||\n (activeMode === \"deposit\" && swapType === \"exactOut\"))\n ) {\n syncingIntentSourcesRef.current = true;\n setFromTokens(sortedIntentSourceTokens);\n }\n\n try {\n const bridgeFees = enrichedIntent.feesAndBuffer?.bridge;\n const bridgeFeeData =\n bridgeFees && typeof bridgeFees === \"object\" ? bridgeFees : undefined;\n const collectionFee = parseFiatNumber(bridgeFeeData?.collection);\n const fulfilmentFee = parseFiatNumber(bridgeFeeData?.fulfilment);\n const executionGasFee =\n parseFiatNumber(bridgeFeeData?.caGas) ??\n (collectionFee !== undefined || fulfilmentFee !== undefined\n ? (collectionFee ?? new Decimal(0)).plus(\n fulfilmentFee ?? new Decimal(0)\n )\n : undefined);\n const bridgeGasSuppliedFee = parseFiatNumber(\n bridgeFeeData?.gasSupplied\n );\n const destinationGasSuppliedFee =\n getIntentDestinationGasUsdValue(enrichedIntent);\n const gasSuppliedFee =\n bridgeGasSuppliedFee ?? destinationGasSuppliedFee;\n const bridgeComponentsTotal = bridgeFeeData\n ? [\n executionGasFee,\n parseFiatNumber(bridgeFeeData.protocol),\n parseFiatNumber(bridgeFeeData.solver),\n gasSuppliedFee,\n ].reduce(\n (sum, value) => sum.plus(value ?? new Decimal(0)),\n new Decimal(0)\n )\n : undefined;\n const rawBridgeTotal =\n typeof bridgeFees === \"string\"\n ? parseFiatNumber(bridgeFees)\n : parseFiatNumber(bridgeFeeData?.total);\n const bridgeTotal =\n rawBridgeTotal &&\n !bridgeGasSuppliedFee &&\n destinationGasSuppliedFee &&\n destinationGasSuppliedFee.gt(0)\n ? rawBridgeTotal.plus(destinationGasSuppliedFee)\n : (rawBridgeTotal ??\n (bridgeComponentsTotal && bridgeComponentsTotal.gt(0)\n ? bridgeComponentsTotal\n : destinationGasSuppliedFee));\n\n if (bridgeTotal !== undefined) {\n setIntentFeeUsd(\n bridgeTotal.gt(0) ? bridgeTotal.toDecimalPlaces(6).toFixed() : \"0\"\n );\n } else {\n setIntentFeeUsd(undefined);\n }\n } catch (err) {\n console.warn(\"Could not resolve bridge fee total\", err);\n setIntentFeeUsd(undefined);\n }\n },\n [\n activeMode,\n fromTokens,\n getFiatValue,\n sourceSelectionTouched,\n swapType,\n swapBalance,\n swapSupportedChainsAndTokens,\n supportedChainsAndTokens,\n toToken,\n ]\n );\n\n const handleSwapIntentCallback = useCallback(\n (data: any, runId: number, quoteInputKey: string) => {\n const { intent, allow, deny, refresh } = data;\n const bridgeProvider = normalizeBridgeProvider(\n data?.bridgeProvider ??\n intent?.bridgeProvider ??\n intent?.normalizedIntent?.bridgeProvider ??\n intent?.swap?.bridgeProvider\n );\n const intentWithBridgeProvider = normalizeRenderableSwapIntentData(\n intent,\n bridgeProvider\n );\n logSdkIntentEvent(\"onIntent\", data, {\n bridgeProvider,\n currentRunId: swapRunIdRef.current,\n normalizedIntent: intentWithBridgeProvider,\n isCurrentRun: swapRunIdRef.current === runId,\n quoteInputKey,\n runId,\n });\n if (swapRunIdRef.current !== runId) {\n logSdkIntentEvent(\"ignored stale onIntent\", data, {\n currentRunId: swapRunIdRef.current,\n quoteInputKey,\n runId,\n });\n deny();\n return;\n }\n if (quoteInputKey && activeQuoteInputKeyRef.current !== quoteInputKey) {\n logSdkIntentEvent(\"ignored stale quote input onIntent\", data, {\n currentQuoteInputKey: activeQuoteInputKeyRef.current,\n quoteInputKey,\n runId,\n });\n deny();\n return;\n }\n if (!intentWithBridgeProvider) {\n console.warn(\"[NexusWidget SDK][intent] Unsupported intent shape\", {\n intent,\n raw: data,\n });\n finishIntentFetchTiming(runId, \"failed\");\n deny();\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setTxError(\"Quote unavailable\");\n return;\n }\n const resolvedQuoteInputKey = quoteInputKey;\n const normalizedRefresh =\n typeof refresh === \"function\"\n ? async (...args: unknown[]) => {\n const refreshed = await refresh(...args);\n const refreshedBridgeProvider = normalizeBridgeProvider(\n refreshed?.bridgeProvider ??\n refreshed?.normalizedIntent?.bridgeProvider ??\n refreshed?.swap?.bridgeProvider ??\n bridgeProvider\n );\n return (\n normalizeRenderableSwapIntentData(\n refreshed,\n refreshedBridgeProvider\n ) ?? refreshed\n );\n }\n : refresh;\n finishIntentFetchTiming(runId);\n providerSwapIntent.current = {\n intent: intentWithBridgeProvider as any,\n allow,\n deny,\n refresh: normalizedRefresh,\n };\n swapIntentRef.current = {\n intent: intentWithBridgeProvider,\n allow,\n deny,\n refresh: normalizedRefresh,\n runId,\n quoteInputKey: resolvedQuoteInputKey,\n };\n flushSync(() => {\n applySwapIntent(intentWithBridgeProvider);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n });\n },\n [applySwapIntent, finishIntentFetchTiming, providerSwapIntent]\n );\n\n // Deposit-specific\n const [selectedOpportunity, setSelectedOpportunity] = useState<\n NexusWidgetDepositOpportunityConfig | undefined\n >(() => (activeMode === \"deposit\" ? configuredDeposit : undefined));\n const selectedOpportunityIdentity =\n getDepositConfigIdentity(selectedOpportunity);\n const [depositAmountMode, setDepositAmountMode] = useState<\"token\" | \"usd\">(\n \"token\"\n );\n const [depositSourceFilter, setDepositSourceFilter] =\n useState(\"all\");\n\n useEffect(() => {\n const immediateIssue = buildReceiveAmountIssue();\n applyReceiveAmountIssue(immediateIssue);\n\n if (!toToken || !getDestinationReceiveLimitUsd(toToken)) return;\n if (!parseFiatNumber(amount)?.gt(0)) return;\n if (getImmediateDestinationReceiveUsdRate(toToken)?.gt(0)) return;\n\n let cancelled = false;\n void resolveUsdRateForToken(toToken)\n .then((resolvedRate) => {\n if (cancelled) return;\n const issue = buildReceiveAmountIssue({\n destinationRate: resolvedRate.gt(0) ? resolvedRate : undefined,\n });\n applyReceiveAmountIssue(issue);\n })\n .catch(() => {\n if (!cancelled) {\n applyReceiveAmountIssue(buildReceiveAmountIssue());\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [\n activeMode,\n amount,\n depositAmountMode,\n fromTokens,\n swapType,\n toToken?.chainId,\n toToken?.chainName,\n toToken?.contractAddress,\n toToken?.priceUSD,\n toToken?.symbol,\n ]);\n\n const trackDeposit = useCallback(\n (event: string, props?: Record) => {\n const analytics = nexusSDK?.analytics;\n if (!analytics) return;\n analytics.track(event, {\n widgetSessionId: widgetSessionIdRef.current,\n widgetAttemptId: widgetAttemptIdRef.current,\n opportunityProtocol: selectedOpportunity?.protocol ?? null,\n destinationChainId: selectedOpportunity?.chainId ?? null,\n destinationToken: selectedOpportunity?.tokenSymbol ?? null,\n ...props,\n });\n },\n [nexusSDK, selectedOpportunity]\n );\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (!nexusSDK?.analytics) return;\n if (widgetOpenedFiredRef.current) return;\n widgetOpenedFiredRef.current = true;\n widgetOpenedTsRef.current = Date.now();\n rotateAttempt();\n trackDeposit(\"deposit_widget_opened\", {\n embed: Boolean(embed),\n depositConfigured: Boolean(configuredDeposit),\n prefillAmountPresent: Boolean(config.prefill?.amount),\n });\n }, [\n activeMode,\n nexusSDK,\n embed,\n configuredDeposit,\n config.prefill,\n rotateAttempt,\n trackDeposit,\n ]);\n\n useEffect(() => {\n analyticsRef.current = nexusSDK?.analytics ?? null;\n }, [nexusSDK]);\n\n useEffect(() => {\n selectedOpportunityRef.current = selectedOpportunity;\n }, [selectedOpportunity]);\n\n useEffect(() => {\n return () => {\n if (!widgetOpenedFiredRef.current) return;\n const analytics = analyticsRef.current;\n if (!analytics) return;\n const opp = selectedOpportunityRef.current;\n analytics.track(\"deposit_widget_closed\", {\n widgetSessionId: widgetSessionIdRef.current,\n widgetAttemptId: widgetAttemptIdRef.current,\n opportunityProtocol: opp?.protocol ?? null,\n lastStep: swapStepRef.current,\n reachedTerminal: reachedTerminalRef.current,\n hadSimulationSuccess: hadSimulationSuccessRef.current,\n hadPreviewViewed: hadPreviewViewedRef.current,\n timeInWidgetMs: Date.now() - widgetOpenedTsRef.current,\n });\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const toTokenFromOpportunity = (\n opp: NexusWidgetDepositOpportunityMetadata\n ): SwapTokenOption => {\n const citreaToken = findCitreaReceiveToken({\n address: opp.tokenAddress,\n chainId: opp.chainId,\n symbol: opp.tokenSymbol,\n });\n const chainTokens = supportedChainsAndTokens?.find(\n (chain) => chain.id === opp.chainId\n )?.tokens;\n const matchedToken = chainTokens?.find(\n (token) =>\n token.contractAddress.toLowerCase() ===\n opp.tokenAddress.toLowerCase() || token.symbol === opp.tokenSymbol\n );\n const tokenSymbol =\n citreaToken?.symbol ?? matchedToken?.symbol ?? opp.tokenSymbol;\n const tokenMeta =\n TOKEN_METADATA[tokenSymbol as keyof typeof TOKEN_METADATA];\n\n return {\n chainId: opp.chainId,\n contractAddress: citreaToken?.contractAddress ?? opp.tokenAddress,\n symbol: tokenSymbol,\n name: matchedToken?.name || citreaToken?.name || tokenSymbol,\n balance: \"0\",\n balanceInFiat: \"$0.00\",\n decimals:\n matchedToken?.decimals ??\n citreaToken?.decimals ??\n opp.tokenDecimals ??\n tokenMeta?.decimals ??\n 18,\n logo:\n opp.tokenLogo ||\n matchedToken?.logo ||\n citreaToken?.logo ||\n tokenMeta?.logo,\n chainName: getShortChainName(\n opp.chainId,\n CHAIN_METADATA[opp.chainId]?.name ?? citreaToken?.chainName\n ),\n chainLogo: CHAIN_METADATA[opp.chainId]?.logo ?? citreaToken?.chainLogo,\n };\n };\n\n const getDestinationBalanceFromSwapBalances = (token?: SwapTokenOption) => {\n if (!token?.chainId || !token.contractAddress) return null;\n\n const targetAddress = token.contractAddress.toLowerCase();\n const targetSymbol = token.symbol.toUpperCase();\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== token.chainId) continue;\n\n const breakdownAddress = breakdown.contractAddress?.toLowerCase();\n const addressMatches =\n (breakdownAddress && breakdownAddress === targetAddress) ||\n (isNativeTokenAddress(breakdownAddress) &&\n isNativeTokenAddress(targetAddress));\n const symbolMatches =\n (breakdown.symbol ?? asset.symbol ?? \"\").toUpperCase() ===\n targetSymbol;\n\n if (!addressMatches && !symbolMatches) continue;\n\n const balance = parseFiatNumber(breakdown.balance);\n if (!balance) return null;\n\n return `${balance.toDecimalPlaces(6).toFixed()} ${token.symbol}`;\n }\n }\n\n return null;\n };\n\n const resolvePrefillToken = useCallback(\n (pair?: RuntimeDestinationPair) => {\n if (!pair?.token || !pair.chain) return undefined;\n\n const normalizeAddress = (address?: string) => {\n if (!address) return \"\";\n return isNativeTokenAddress(address)\n ? zeroAddress\n : address.toLowerCase();\n };\n const targetAddress = normalizeAddress(pair.token);\n\n const balanceToken = deriveTokenOptions(\n swapBalance ?? [],\n swapSupportedChainsAndTokens\n ).find(\n (token) =>\n token.chainId === pair.chain &&\n normalizeAddress(token.contractAddress) === targetAddress\n );\n if (balanceToken) return balanceToken;\n\n const chain = supportedChainsAndTokens?.find(\n (item) => item.id === pair.chain\n );\n const matchedToken = chain?.tokens?.find(\n (token) => normalizeAddress(token.contractAddress) === targetAddress\n );\n const citreaToken = findCitreaReceiveToken({\n address: pair.token,\n chainId: pair.chain,\n });\n const tokenAddressSymbol = Object.entries(\n TOKEN_CONTRACT_ADDRESSES as Record>\n ).find(\n ([, addresses]) =>\n normalizeAddress(addresses[pair.chain]) === targetAddress\n )?.[0];\n const chainMeta = CHAIN_METADATA[pair.chain];\n const isNativePrefill = isNativeTokenAddress(pair.token);\n const tokenSymbol =\n matchedToken?.symbol ??\n citreaToken?.symbol ??\n pair.symbol ??\n tokenAddressSymbol ??\n (isNativePrefill ? chainMeta?.nativeCurrency?.symbol : undefined) ??\n \"Token\";\n const tokenMeta =\n TOKEN_METADATA[tokenSymbol as keyof typeof TOKEN_METADATA];\n\n if (\n !chain &&\n !matchedToken &&\n !citreaToken &&\n !tokenAddressSymbol &&\n !pair.symbol &&\n !isNativePrefill\n ) {\n return undefined;\n }\n\n return {\n chainId: pair.chain,\n contractAddress: citreaToken?.contractAddress ?? pair.token,\n symbol: tokenSymbol,\n name: matchedToken?.name || citreaToken?.name || tokenSymbol,\n balance: `0 ${tokenSymbol}`,\n balanceInFiat: \"$0.00\",\n decimals:\n matchedToken?.decimals ??\n citreaToken?.decimals ??\n pair.decimals ??\n tokenMeta?.decimals ??\n (isNativePrefill ? chainMeta?.nativeCurrency?.decimals : undefined) ??\n 18,\n logo:\n matchedToken?.logo ||\n citreaToken?.logo ||\n pair.logo ||\n tokenMeta?.logo,\n chainName: getShortChainName(\n pair.chain,\n chain?.name ?? chainMeta?.name ?? citreaToken?.chainName\n ),\n chainLogo: chainMeta?.logo ?? chain?.logo ?? citreaToken?.chainLogo,\n } satisfies SwapTokenOption;\n },\n [supportedChainsAndTokens, swapBalance]\n );\n\n useEffect(() => {\n if (activeMode !== \"swap\") return;\n\n const sourcePrefill = config.prefill?.source;\n const destinationPrefill = config.prefill?.destination;\n if (!sourcePrefill && !destinationPrefill) return;\n\n const prefillKey = [\n sourcePrefill\n ? `source:${sourcePrefill.chain}:${sourcePrefill.token.toLowerCase()}`\n : \"\",\n destinationPrefill\n ? `destination:${destinationPrefill.chain}:${destinationPrefill.token.toLowerCase()}`\n : \"\",\n config.prefill?.amount ? `amount:${config.prefill.amount}` : \"\",\n ].join(\"|\");\n\n if (appliedTokenPrefillRef.current === prefillKey) return;\n\n const sourceToken = resolvePrefillToken(sourcePrefill);\n const destinationToken = resolvePrefillToken(destinationPrefill);\n\n if (sourcePrefill && !sourceToken) return;\n if (destinationPrefill && !destinationToken) return;\n\n if (sourceToken) {\n setFromTokens((current) => {\n const nextSourceToken = {\n ...sourceToken,\n userAmount: config.prefill?.amount ?? \"\",\n };\n const currentSourceToken = current[0];\n if (\n current.length === 1 &&\n isSameTokenSelection(currentSourceToken, nextSourceToken) &&\n currentSourceToken.userAmount === nextSourceToken.userAmount\n ) {\n return current;\n }\n return [nextSourceToken];\n });\n setSourceSelectionTouched(true);\n }\n if (destinationToken) {\n setToToken((current) =>\n isSameTokenSelection(current, destinationToken)\n ? current\n : destinationToken\n );\n }\n setSwapType(\"exactIn\");\n appliedTokenPrefillRef.current = prefillKey;\n }, [\n activeMode,\n config.prefill?.amount,\n config.prefill?.destination?.chain,\n config.prefill?.destination?.decimals,\n config.prefill?.destination?.logo,\n config.prefill?.destination?.symbol,\n config.prefill?.destination?.token,\n config.prefill?.source?.chain,\n config.prefill?.source?.decimals,\n config.prefill?.source?.logo,\n config.prefill?.source?.symbol,\n config.prefill?.source?.token,\n resolvePrefillToken,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"send\") return;\n\n const sendPrefill =\n config.prefill?.token && config.prefill?.chain\n ? {\n token: config.prefill.token,\n chain: config.prefill.chain,\n }\n : config.prefill?.destination;\n if (!sendPrefill) return;\n\n const prefillKey = `send:${sendPrefill.chain}:${sendPrefill.token.toLowerCase()}`;\n if (appliedTokenPrefillRef.current === prefillKey) return;\n\n const token = resolvePrefillToken(sendPrefill);\n if (!token) return;\n\n setToToken(token);\n setSwapType(\"exactOut\");\n appliedTokenPrefillRef.current = prefillKey;\n }, [\n activeMode,\n config.prefill?.chain,\n config.prefill?.destination?.chain,\n config.prefill?.destination?.decimals,\n config.prefill?.destination?.logo,\n config.prefill?.destination?.symbol,\n config.prefill?.destination?.token,\n config.prefill?.token,\n resolvePrefillToken,\n ]);\n\n useEffect(() => {\n if (config.prefill?.amount) setAmount(config.prefill.amount);\n if (config.prefill?.recipient)\n setRecipientAddress(config.prefill.recipient);\n }, [config.prefill?.amount, config.prefill?.recipient]);\n\n useEffect(() => {\n setDestinationBalance(null);\n\n const balanceToken =\n toToken ??\n (activeMode === \"deposit\" && selectedOpportunity\n ? toTokenFromOpportunity(selectedOpportunity)\n : undefined);\n\n if (!balanceToken?.chainId || !ownerAddress) return;\n\n const swapBalanceValue =\n getDestinationBalanceFromSwapBalances(balanceToken);\n if (swapBalanceValue) {\n setDestinationBalance(swapBalanceValue);\n }\n }, [\n activeMode,\n ownerAddress,\n selectedOpportunity?.chainId,\n selectedOpportunity?.tokenAddress,\n selectedOpportunity?.tokenLogo,\n selectedOpportunity?.tokenSymbol,\n swapBalance,\n toToken?.chainId,\n toToken?.chainName,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || !configuredDeposit) return;\n setSelectedOpportunity((current) =>\n isSameDepositConfig(current, configuredDeposit)\n ? current\n : configuredDeposit\n );\n setSwapType(\"exactOut\");\n setToToken((current) => {\n const next = {\n ...toTokenFromOpportunity(configuredDeposit),\n balance: current?.balance ?? \"0\",\n balanceInFiat: current?.balanceInFiat ?? \"$0.00\",\n };\n if (\n current &&\n current.chainId === next.chainId &&\n current.contractAddress.toLowerCase() ===\n next.contractAddress.toLowerCase() &&\n current.symbol === next.symbol &&\n current.decimals === next.decimals &&\n current.logo === next.logo &&\n current.chainLogo === next.chainLogo &&\n current.chainName === next.chainName &&\n current.balance === next.balance &&\n current.balanceInFiat === next.balanceInFiat\n ) {\n return current;\n }\n return next;\n });\n }, [\n activeMode,\n configuredDepositIdentity,\n configuredDeposit?.chainId,\n configuredDeposit?.tokenAddress,\n configuredDeposit?.tokenLogo,\n configuredDeposit?.tokenSymbol,\n supportedChainsAndTokens,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || !selectedOpportunity) return;\n setToToken((current) => {\n const next = {\n ...toTokenFromOpportunity(selectedOpportunity),\n balance: current?.balance ?? \"0\",\n balanceInFiat: current?.balanceInFiat ?? \"$0.00\",\n };\n if (\n current &&\n current.chainId === next.chainId &&\n current.contractAddress.toLowerCase() ===\n next.contractAddress.toLowerCase() &&\n current.symbol === next.symbol &&\n current.decimals === next.decimals &&\n current.logo === next.logo &&\n current.chainLogo === next.chainLogo &&\n current.chainName === next.chainName &&\n current.balance === next.balance &&\n current.balanceInFiat === next.balanceInFiat\n ) {\n return current;\n }\n return next;\n });\n }, [activeMode, selectedOpportunity, supportedChainsAndTokens]);\n\n useEffect(() => {\n if (activeMode !== \"send\") return;\n setSwapType(\"exactOut\");\n }, [activeMode]);\n\n useEffect(() => {\n if (activeMode === \"swap\" && swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n }, [activeMode, swapType]);\n\n useEffect(() => {\n if (!toToken?.symbol) return;\n if (getFiatValue(1, toToken.symbol) > 0) return;\n\n let cancelled = false;\n void resolveTokenUsdRate(toToken.symbol).catch((error) => {\n if (!cancelled) {\n console.warn(\"Unable to resolve Nexus Widget token USD rate\", {\n symbol: toToken.symbol,\n error,\n });\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [activeMode, getFiatValue, resolveTokenUsdRate, toToken?.symbol]);\n\n // Balance helpers\n const activeBalanceArray = swapBalance;\n const selectedToken = config.prefill?.token ?? \"USDC\";\n const currentAsset =\n activeBalanceArray?.find((a) => a.symbol === selectedToken) ||\n activeBalanceArray?.[0];\n const maxBalance = currentAsset?.balance\n ? String(currentAsset.balance)\n : undefined;\n const usdValue = getFiatValue(\n Number(amount) || 0,\n currentAsset?.symbol || \"USDC\"\n );\n const getDepositTokenUsdRate = () => {\n if (selectedOpportunity?.tokenSymbol) {\n const fiat = getFiatValue(1, selectedOpportunity.tokenSymbol);\n if (Number.isFinite(fiat) && fiat > 0) {\n return new Decimal(fiat);\n }\n }\n\n if (toToken) {\n const tokenRate = getTokenUsdRate(toToken);\n if (tokenRate.gt(0)) return tokenRate;\n }\n\n return getCachedDestinationUsdRate(toToken) ?? new Decimal(0);\n };\n const getDepositTokenAmountForQuote = () => {\n const parsedAmount = parseFiatNumber(amount) ?? new Decimal(0);\n if (parsedAmount.lte(0)) return undefined;\n if (depositAmountMode === \"token\") return parsedAmount;\n\n const rate = getDepositTokenUsdRate();\n if (rate.lte(0)) return undefined;\n return parsedAmount.div(rate);\n };\n const depositTokenAmountForQuote = getDepositTokenAmountForQuote();\n const depositQuoteAmountKey = depositTokenAmountForQuote?.toFixed() ?? \"\";\n const depositUsdDecimal =\n depositAmountMode === \"usd\"\n ? (parseFiatNumber(amount) ?? new Decimal(0))\n : depositTokenAmountForQuote\n ? depositTokenAmountForQuote.mul(getDepositTokenUsdRate())\n : new Decimal(0);\n const depositUsdDisplay = depositUsdDecimal.toDecimalPlaces(2).toFixed();\n const depositTokenDisplay =\n depositTokenAmountForQuote\n ?.toDecimalPlaces(\n getCappedTokenDisplayDecimals(toToken?.decimals),\n Decimal.ROUND_DOWN\n )\n .toFixed() ?? \"0\";\n const isExactOutMode = activeMode === \"deposit\" || activeMode === \"send\";\n const getActiveTotalBalanceUsd = () =>\n isExactOutMode\n ? getExactOutTotalSourceBalanceUsd()\n : getSwapBalanceTotalUsd();\n const getTotalBalancePercentUsdAmount = (pct: number) => {\n const ratio =\n isExactOutMode && pct === 100\n ? receiveMaxSafetyMultiplier\n : new Decimal(pct).div(100);\n return getActiveTotalBalanceUsd().mul(ratio);\n };\n const getExactOutDestinationUsdRate = (token: SwapTokenOption) => {\n if (activeMode === \"deposit\") {\n return getDepositTokenUsdRate();\n }\n return getTokenUsdRate(token);\n };\n const getExactOutPercentAmountFromBalance = (\n token: SwapTokenOption,\n pct: number,\n preferUsd: boolean\n ) => {\n const usdAmount = getTotalBalancePercentUsdAmount(pct);\n if (usdAmount.lte(0)) return undefined;\n\n if (preferUsd) {\n return {\n amount: usdAmount.toDecimalPlaces(2, Decimal.ROUND_DOWN).toFixed(),\n mode: \"usd\" as const,\n };\n }\n\n const rate = getExactOutDestinationUsdRate(token);\n if (rate.lte(0)) return undefined;\n\n return {\n amount: usdAmount\n .div(rate)\n .toDecimalPlaces(\n getCappedTokenDisplayDecimals(token.decimals),\n Decimal.ROUND_DOWN\n )\n .toFixed(),\n mode: \"token\" as const,\n };\n };\n const depositSourceTargetUsdKey =\n activeMode === \"deposit\"\n ? (getDepositSourceTargetUsd()?.toFixed() ?? \"\")\n : \"\";\n const normalizedQuoteAmountKey = parseFiatNumber(amount)?.toFixed() ?? \"\";\n const quoteRecipientKey =\n activeMode === \"swap\"\n ? effectiveRecipientAddress\n : activeMode === \"send\"\n ? recipientAddress\n : \"\";\n const activeQuoteInputKey = [\n activeMode,\n swapType,\n normalizedQuoteAmountKey,\n toTokenQuoteKey,\n quoteRecipientKey.toLowerCase(),\n activeMode === \"swap\" ? fromTokensQuoteKey : \"\",\n activeMode === \"deposit\"\n ? [\n depositAmountMode,\n depositQuoteAmountKey,\n selectedOpportunityIdentity,\n depositSourceTargetUsdKey,\n depositSourceFilter,\n sourceSelectionTouched ? \"manual\" : \"auto\",\n sourceSelectionRevision,\n exactOutQuoteSourceMode,\n ].join(\":\")\n : \"\",\n activeMode === \"send\"\n ? [\n sourceSelectionTouched ? \"manual\" : \"auto\",\n sourceSelectionRevision,\n exactOutQuoteSourceMode,\n ].join(\":\")\n : \"\",\n ].join(\"|\");\n\n const exactOutAutoSourceInputKey = [\n activeMode,\n normalizedQuoteAmountKey,\n toTokenQuoteKey,\n quoteRecipientKey.toLowerCase(),\n activeMode === \"deposit\"\n ? [\n depositAmountMode,\n depositQuoteAmountKey,\n selectedOpportunityIdentity,\n depositSourceTargetUsdKey,\n ].join(\":\")\n : \"\",\n ].join(\"|\");\n\n activeQuoteInputKeyRef.current = activeQuoteInputKey;\n\n useEffect(() => {\n lastAutoIntentSourceTokensRef.current = [];\n }, [exactOutAutoSourceInputKey]);\n\n useEffect(() => {\n setTxError(null);\n }, [activeQuoteInputKey]);\n const hasCurrentQuoteIntent = Boolean(\n intentData &&\n swapIntentRef.current &&\n swapIntentRef.current.runId === swapRunIdRef.current &&\n swapIntentRef.current.quoteInputKey === activeQuoteInputKey\n );\n\n useEffect(() => {\n if (swapStep !== \"idle\") return;\n if (!hasCurrentQuoteIntent) return;\n if (sourceSelectionTouched) return;\n if (\n activeMode !== \"send\" &&\n !(activeMode === \"deposit\" && swapType === \"exactOut\")\n ) {\n return;\n }\n\n const nextTokens = lastIntentSourceTokensRef.current;\n if (nextTokens.length === 0) return;\n\n setFromTokens((current) => {\n if (\n getSourceTokenListSyncKey(current) ===\n getSourceTokenListSyncKey(nextTokens)\n ) {\n return current;\n }\n\n syncingIntentSourcesRef.current = true;\n return nextTokens;\n });\n }, [\n activeMode,\n hasCurrentQuoteIntent,\n sourceSelectionTouched,\n swapStep,\n swapType,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (!nexusSDK?.analytics) return;\n const parsed = parseFiatNumber(amount);\n if (!parsed || parsed.lte(0)) return;\n if (amount === amountEnteredLastValueRef.current) return;\n const timeout = setTimeout(() => {\n amountEnteredLastValueRef.current = amount;\n trackDeposit(\"deposit_amount_entered\", {\n amountToken: depositTokenDisplay,\n amountUsd: Number(depositUsdDisplay) || 0,\n inputMethod: lastInputMethodRef.current,\n });\n lastInputMethodRef.current = \"typed\";\n }, 500);\n return () => clearTimeout(timeout);\n }, [\n amount,\n activeMode,\n nexusSDK,\n depositTokenDisplay,\n depositUsdDisplay,\n trackDeposit,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (intentData) hadSimulationSuccessRef.current = true;\n }, [intentData, activeMode]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (sourceSelectionTouched) return;\n previousAutoSourceCountRef.current = (intentData?.sources ?? []).length;\n }, [intentData, activeMode, sourceSelectionTouched]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n const prev = prevSourceTouchedRef.current;\n const curr = sourceSelectionTouched;\n if (prev === curr) return;\n prevSourceTouchedRef.current = curr;\n if (!prev && curr) {\n trackDeposit(\"deposit_source_selection_changed\", {\n sourceCount: fromTokens.length,\n sourceChainIds: fromTokens.map((t) => t.chainId).filter(Boolean),\n sourceTokenSymbols: fromTokens.map((t) => t.symbol).filter(Boolean),\n previousSourceCount: previousAutoSourceCountRef.current,\n });\n } else if (prev && !curr) {\n trackDeposit(\"deposit_source_selection_reverted_to_auto\", {\n previousSourceCount: fromTokens.length,\n });\n }\n }, [sourceSelectionTouched, activeMode, fromTokens, trackDeposit]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (swapStep !== \"preview-intent\") return;\n if (intentLoading) return;\n if (!intentData) return;\n if (hadPreviewViewedRef.current) return;\n hadPreviewViewedRef.current = true;\n previewViewedTsRef.current = Date.now();\n trackDeposit(\"deposit_preview_viewed\", {\n totalFeeUsd: Number(intentFeeUsd) || 0,\n toAmountUsd: Number(depositUsdDisplay) || 0,\n sourceCount: (intentData?.sources ?? []).length,\n });\n }, [\n swapStep,\n intentLoading,\n intentData,\n activeMode,\n intentFeeUsd,\n depositUsdDisplay,\n trackDeposit,\n ]);\n const requiredDestinationTokenAmount =\n activeMode === \"deposit\"\n ? depositTokenAmountForQuote\n : activeMode === \"send\"\n ? parseFiatNumber(amount)\n : undefined;\n const canRefreshExactOutQuote = () =>\n activeMode === \"deposit\"\n ? Boolean(\n hasPositiveDecimalInput(amount) &&\n toToken &&\n selectedOpportunity &&\n depositTokenAmountForQuote &&\n depositTokenAmountForQuote.gt(0)\n )\n : activeMode === \"send\"\n ? Boolean(hasPositiveDecimalInput(amount) && toToken)\n : false;\n const invalidateExactOutQuoteForRefresh = (options?: {\n sourceTokens?: SwapTokenOption[];\n }) => {\n immediateQuoteAfterSourceEditRef.current = true;\n const receiveIssue = buildReceiveAmountIssue({\n sourceTokens: options?.sourceTokens,\n });\n applyReceiveAmountIssue(receiveIssue);\n const configuredIssue = buildConfiguredAmountIssue();\n if (configuredIssue) {\n clearPreviewForBlockingAmountIssue();\n }\n const sourceBalanceIssue = buildExactOutSourceBalanceIssue(\n options?.sourceTokens\n );\n if (sourceBalanceIssue) {\n clearPreviewForBlockingAmountIssue();\n setSwapQuoteIssue(sourceBalanceIssue);\n }\n const shouldLoadQuote = Boolean(\n !receiveIssue &&\n !configuredIssue &&\n !sourceBalanceIssue &&\n nexusSDK &&\n canRefreshExactOutQuote()\n );\n if (!receiveIssue && !configuredIssue && !sourceBalanceIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n setTxError(null);\n setSwapQuoteIssue(null);\n }\n return shouldLoadQuote;\n };\n\n useEffect(() => {\n if (\n activeMode !== \"swap\" ||\n swapStep !== \"idle\" ||\n swapType !== \"exactIn\"\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n\n const sources = getPredictiveExactInSourceTokens();\n const key = getPredictiveQuoteCacheKey();\n if (!toToken || sources.length === 0 || !key) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n\n const runId = ++predictiveQuoteRunRef.current;\n let cancelled = false;\n\n void (async () => {\n const baseline = predictiveQuoteCacheRef.current[key];\n const cachedDestinationRate = parseFiatNumber(\n baseline?.destinationUsdRate\n );\n const destinationRate =\n cachedDestinationRate && cachedDestinationRate.gt(0)\n ? cachedDestinationRate\n : await resolveUsdRateForToken(toToken);\n\n if (cancelled || runId !== predictiveQuoteRunRef.current) return;\n if (destinationRate.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n\n let sourceUsd = new Decimal(0);\n for (const source of sources) {\n const sourceAmount =\n parseFiatNumber(source.userAmount) ?? new Decimal(0);\n if (sourceAmount.lte(0)) continue;\n\n if (source.userAmountMode === \"usd\") {\n sourceUsd = sourceUsd.plus(sourceAmount);\n continue;\n }\n\n const sourceRate = await resolveUsdRateForToken(source);\n if (cancelled || runId !== predictiveQuoteRunRef.current) return;\n if (sourceRate.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n sourceUsd = sourceUsd.plus(sourceAmount.mul(sourceRate));\n }\n\n if (sourceUsd.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n\n const cachedAmountPerSourceUsd = parseFiatNumber(\n baseline?.exactInDestinationAmountPerSourceUsd\n );\n const predictedDestinationAmount =\n cachedAmountPerSourceUsd && cachedAmountPerSourceUsd.gt(0)\n ? sourceUsd.mul(cachedAmountPerSourceUsd)\n : sourceUsd\n .mul(BASIS_POINTS - PREDICTIVE_EXACT_IN_DISCOUNT_BPS)\n .div(BASIS_POINTS)\n .div(destinationRate);\n const predictedDestinationUsd =\n cachedAmountPerSourceUsd && cachedAmountPerSourceUsd.gt(0)\n ? predictedDestinationAmount.mul(destinationRate)\n : sourceUsd\n .mul(BASIS_POINTS - PREDICTIVE_EXACT_IN_DISCOUNT_BPS)\n .div(BASIS_POINTS);\n\n if (\n cancelled ||\n runId !== predictiveQuoteRunRef.current ||\n predictedDestinationAmount.lte(0)\n ) {\n return;\n }\n\n setPredictiveQuote({\n key,\n mode: \"exactIn\",\n toAmount: getPredictiveDisplayAmount(\n predictedDestinationAmount,\n toToken\n ),\n toUsd: predictedDestinationUsd.toDecimalPlaces(6).toFixed(),\n });\n })();\n\n return () => {\n cancelled = true;\n };\n }, [\n activeMode,\n amount,\n fromTokens,\n swapStep,\n swapType,\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n useEffect(() => {\n if (\n (activeMode !== \"deposit\" && activeMode !== \"send\") ||\n swapStep !== \"idle\" ||\n swapType !== \"exactOut\" ||\n !nexusSDK\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current\n );\n return;\n }\n\n const parsedAmount = parseFiatNumber(amount);\n const key = getPredictiveQuoteCacheKey();\n if (\n !toToken ||\n !parsedAmount ||\n parsedAmount.lte(0) ||\n !key ||\n (activeMode === \"deposit\" && !selectedOpportunity)\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current\n );\n return;\n }\n\n const runId = ++predictiveQuoteRunRef.current;\n let cancelled = false;\n\n void (async () => {\n const baseline = predictiveQuoteCacheRef.current[key];\n const cachedDestinationRate = parseFiatNumber(\n baseline?.destinationUsdRate\n );\n const destinationRate =\n cachedDestinationRate && cachedDestinationRate.gt(0)\n ? cachedDestinationRate\n : await resolveUsdRateForToken(toToken);\n\n if (cancelled || runId !== predictiveQuoteRunRef.current) return;\n if (destinationRate.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current\n );\n return;\n }\n\n const destinationAmount =\n activeMode === \"deposit\" && depositAmountMode === \"usd\"\n ? parsedAmount.div(destinationRate)\n : parsedAmount;\n const destinationUsd =\n activeMode === \"deposit\" && depositAmountMode === \"usd\"\n ? parsedAmount\n : destinationAmount.mul(destinationRate);\n const destinationCoverage = getExactOutDestinationBalanceCoverage({\n requestedAmount: destinationAmount,\n requestedUsd: destinationUsd,\n token: toToken,\n });\n const destinationUsdNeedingSources = Decimal.max(\n destinationUsd.minus(destinationCoverage?.usd ?? new Decimal(0)),\n new Decimal(0)\n );\n const cachedSourceUsdRatio = parseFiatNumber(\n baseline?.exactOutSourceUsdPerDestinationUsd\n );\n const requiredSourceUsd = getPredictiveExactOutSourceTargetUsd(\n destinationUsdNeedingSources,\n cachedSourceUsdRatio\n );\n const sources = requiredSourceUsd.gt(0)\n ? await buildPredictiveExactOutSources(requiredSourceUsd)\n : [];\n\n if (\n cancelled ||\n runId !== predictiveQuoteRunRef.current ||\n (requiredSourceUsd.gt(0) && sources.length === 0)\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current\n );\n return;\n }\n\n setPredictiveQuote({\n key,\n mode: \"exactOut\",\n sources,\n toAmount: getPredictiveDisplayAmount(destinationAmount, toToken),\n toUsd: destinationUsd.toDecimalPlaces(6).toFixed(),\n });\n })();\n\n return () => {\n cancelled = true;\n };\n }, [\n activeMode,\n amount,\n depositAmountMode,\n destinationBalance,\n fromTokensQuoteKey,\n nexusSDK,\n selectedOpportunityIdentity,\n sourceSelectionRevision,\n swapBalance,\n swapStep,\n swapType,\n toToken?.balance,\n toToken?.balanceInFiat,\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n const resolvedDepositSourceTokens = useMemo(() => {\n if (activeMode !== \"deposit\" || !swapBalance) return [];\n const selection = getResolvedDepositSourceSelection();\n return getDepositSourceTokensForIds(selection.selectedSourceIds);\n }, [\n activeMode,\n depositSourceFilter,\n depositQuoteAmountKey,\n depositSourceTargetUsdKey,\n depositUsdDecimal.toFixed(),\n fromTokensQuoteKey,\n selectedOpportunity?.chainId,\n selectedOpportunity?.tokenAddress,\n selectedOpportunity?.tokenSymbol,\n sourceSelectionRevision,\n sourceSelectionTouched,\n swapBalance,\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.symbol,\n ]);\n const lockedDestinationSourceTokens = useMemo(() => {\n if (\n (activeMode !== \"deposit\" && activeMode !== \"send\") ||\n !toToken?.chainId ||\n !requiredDestinationTokenAmount ||\n requiredDestinationTokenAmount.lte(0)\n ) {\n return [];\n }\n\n const heldDestinationToken = getHeldDestinationTokenOption();\n return heldDestinationToken ? [heldDestinationToken] : [];\n }, [\n activeMode,\n requiredDestinationTokenAmount?.toFixed(),\n swapBalance,\n toToken?.chainId,\n toToken?.chainLogo,\n toToken?.chainName,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.logo,\n toToken?.symbol,\n ]);\n\n const setSourcePickerDraftSelection = useCallback(\n (tokens: SwapTokenOption[]) => {\n const nextTokens = tokens.map((token) => ({\n ...token,\n userAmount: \"\",\n }));\n sourcePickerDraftTokensRef.current = nextTokens;\n setSourcePickerDraftTokens(nextTokens);\n },\n []\n );\n\n const getAutoExactOutSourceTokensForPicker = useCallback(() => {\n const autoIntentTokens = lastAutoIntentSourceTokensRef.current;\n if (autoIntentTokens.length > 0) {\n return autoIntentTokens;\n }\n\n return sourceSelectionTouched ? [] : fromTokens;\n }, [fromTokens, sourceSelectionTouched]);\n\n const resetSourcePickerDraft = useCallback(() => {\n sourcePickerDraftTokensRef.current = null;\n setSourcePickerDraftTokens(null);\n }, []);\n\n const beginSourcePickerEdit = useCallback(() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n resetSourcePickerDraft();\n return;\n }\n\n sourcePickerDraftDepositFilterRef.current = depositSourceFilter;\n sourcePickerDraftTouchedRef.current = sourceSelectionTouched;\n sourcePickerDraftModeRef.current = exactOutQuoteSourceModeRef.current;\n setSourcePickerDraftSelection(fromTokens);\n }, [\n activeMode,\n depositSourceFilter,\n fromTokens,\n resetSourcePickerDraft,\n setSourcePickerDraftSelection,\n sourceSelectionTouched,\n ]);\n\n const handleSourcePickerCancel = useCallback(() => {\n resetSourcePickerDraft();\n closeDrawerToIdle();\n }, [closeDrawerToIdle, resetSourcePickerDraft]);\n\n const handleSourcePickerDraftSelectionChange = useCallback(\n (tokens: SwapTokenOption[]) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n\n setSourcePickerDraftSelection(tokens);\n sourcePickerDraftTouchedRef.current = true;\n sourcePickerDraftModeRef.current = \"selected\";\n if (activeMode === \"deposit\") {\n sourcePickerDraftDepositFilterRef.current = \"custom\";\n }\n },\n [activeMode, setSourcePickerDraftSelection]\n );\n\n const handleSourcePickerFilterTabSelect = useCallback(\n (tab: Exclude) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n\n const nextFilter: DepositSourceFilter =\n tab === \"stables\" ? \"stablecoins\" : tab;\n\n if (tab === \"all\") {\n sourcePickerDraftDepositFilterRef.current = nextFilter;\n sourcePickerDraftTouchedRef.current = false;\n sourcePickerDraftModeRef.current = \"all\";\n setSourcePickerDraftSelection(getAutoExactOutSourceTokensForPicker());\n return;\n }\n\n if (activeMode !== \"deposit\") return;\n\n const selection = getResolvedDepositSourceSelection({\n filter: nextFilter,\n isManualSelection: false,\n });\n const sourcePoolTokens = getDepositSourceTokensForIds(\n selection.sourcePoolIds\n );\n\n sourcePickerDraftDepositFilterRef.current = nextFilter;\n sourcePickerDraftTouchedRef.current = false;\n sourcePickerDraftModeRef.current = \"all\";\n setSourcePickerDraftSelection(sourcePoolTokens);\n },\n [\n activeMode,\n getAutoExactOutSourceTokensForPicker,\n getDepositSourceTokensForIds,\n getResolvedDepositSourceSelection,\n setSourcePickerDraftSelection,\n ]\n );\n\n const commitSourcePickerDraft = useCallback(\n (tokens?: SwapTokenOption[]) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n resetSourcePickerDraft();\n closeDrawerToIdle();\n return;\n }\n\n const nextTokens =\n tokens ?? sourcePickerDraftTokensRef.current ?? fromTokens;\n const normalizedTokens = nextTokens.map((token) => ({\n ...token,\n userAmount: \"\",\n }));\n\n setSourceSelectionTouched(sourcePickerDraftTouchedRef.current);\n setExactOutQuoteSourceModeValue(sourcePickerDraftModeRef.current);\n if (activeMode === \"deposit\") {\n setDepositSourceFilter(sourcePickerDraftDepositFilterRef.current);\n }\n invalidateExactOutQuoteForRefresh({\n sourceTokens: normalizedTokens,\n });\n setSourceSelectionRevision((current) => current + 1);\n setFromTokens(normalizedTokens);\n resetSourcePickerDraft();\n closeDrawerToIdle();\n },\n [\n activeMode,\n closeDrawerToIdle,\n fromTokens,\n invalidateExactOutQuoteForRefresh,\n resetSourcePickerDraft,\n setExactOutQuoteSourceModeValue,\n ]\n );\n\n const sourcePickerSelectedTokens =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n swapStep === \"choose-swap-asset\"\n ? (sourcePickerDraftTokens ?? fromTokens)\n : fromTokens;\n\n useEffect(() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n if (lockedDestinationSourceTokens.length === 0) return;\n if (activeMode === \"deposit\" && !sourceSelectionTouched) return;\n\n setFromTokens((current) => {\n const missing = lockedDestinationSourceTokens.filter(\n (locked) =>\n !current.some(\n (token) =>\n getTokenSelectionKey(token) === getTokenSelectionKey(locked)\n )\n );\n if (missing.length === 0) return current;\n return [\n ...current,\n ...missing.map((token) => ({ ...token, userAmount: \"\" })),\n ];\n });\n }, [activeMode, lockedDestinationSourceTokens, sourceSelectionTouched]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (sourceSelectionTouched) return;\n if (\n !toToken ||\n !depositTokenAmountForQuote ||\n depositTokenAmountForQuote.lte(0)\n ) {\n return;\n }\n if (resolvedDepositSourceTokens.length === 0) {\n return;\n }\n\n setFromTokens((current) => {\n const canInitialize = current.length === 0;\n if (!canInitialize) return current;\n\n const next: SwapTokenOption[] = [];\n const seen = new Set();\n for (const token of resolvedDepositSourceTokens) {\n const key = getTokenSelectionKey(token);\n if (!key || seen.has(key)) continue;\n seen.add(key);\n next.push({ ...token, userAmount: \"\" });\n }\n\n const currentKeys = current.map(getTokenSelectionKey).sort().join(\"|\");\n const nextKeys = next.map(getTokenSelectionKey).sort().join(\"|\");\n if (currentKeys === nextKeys) return current;\n return next;\n });\n }, [\n activeMode,\n depositQuoteAmountKey,\n resolvedDepositSourceTokens,\n sourceSelectionTouched,\n toTokenQuoteKey,\n ]);\n\n // ---------------------------------------------------------------------------\n // Handlers\n // ---------------------------------------------------------------------------\n\n const applyConfiguredPrefillsAfterReset = () => {\n const prefillAmount = config.prefill?.amount ?? \"\";\n const prefillRecipient = config.prefill?.recipient ?? \"\";\n\n appliedTokenPrefillRef.current = null;\n syncingIntentSourcesRef.current = false;\n maxPercentRunRef.current += 1;\n setAmount(prefillAmount);\n setRecipientAddress(prefillRecipient);\n setTxError(null);\n setSwapQuoteIssue(null);\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n setPredictiveQuote(null);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setMaxCalculationPercent(null);\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setExactOutQuoteSourceModeValue(\"all\");\n setSourceSelectionRevision((current) => current + 1);\n setDepositAmountMode(\"token\");\n\n if (activeMode === \"swap\") {\n const sourcePrefill = config.prefill?.source;\n const destinationPrefill = config.prefill?.destination;\n const sourceToken = resolvePrefillToken(sourcePrefill);\n const destinationToken = resolvePrefillToken(destinationPrefill);\n\n setFromTokens(\n sourceToken\n ? [\n {\n ...sourceToken,\n userAmount: prefillAmount,\n },\n ]\n : []\n );\n setSourceSelectionTouched(Boolean(sourceToken));\n setToToken(destinationToken);\n setSelectedOpportunity(undefined);\n setSwapType(\"exactIn\");\n\n if (\n (!sourcePrefill || sourceToken) &&\n (!destinationPrefill || destinationToken) &&\n (sourcePrefill || destinationPrefill)\n ) {\n appliedTokenPrefillRef.current = [\n sourcePrefill\n ? `source:${sourcePrefill.chain}:${sourcePrefill.token.toLowerCase()}`\n : \"\",\n destinationPrefill\n ? `destination:${destinationPrefill.chain}:${destinationPrefill.token.toLowerCase()}`\n : \"\",\n config.prefill?.amount ? `amount:${config.prefill.amount}` : \"\",\n ].join(\"|\");\n }\n return;\n }\n\n if (activeMode === \"send\") {\n const sendPrefill =\n config.prefill?.token && config.prefill?.chain\n ? {\n token: config.prefill.token,\n chain: config.prefill.chain,\n }\n : config.prefill?.destination;\n const token = resolvePrefillToken(sendPrefill);\n\n setFromTokens([]);\n setSelectedOpportunity(undefined);\n setToToken(token);\n setSwapType(\"exactOut\");\n\n if (sendPrefill && token) {\n appliedTokenPrefillRef.current = `send:${sendPrefill.chain}:${sendPrefill.token.toLowerCase()}`;\n }\n return;\n }\n\n const destinationPrefill = config.prefill?.destination;\n const configuredPrefillDeposit = destinationPrefill\n ? configuredDepositOptions.find(\n (deposit) =>\n deposit.chainId === destinationPrefill.chain &&\n deposit.tokenAddress.toLowerCase() ===\n destinationPrefill.token.toLowerCase()\n )\n : undefined;\n const nextDeposit = configuredPrefillDeposit ?? configuredDeposit;\n\n setFromTokens([]);\n setSelectedOpportunity(nextDeposit);\n setToToken(nextDeposit ? toTokenFromOpportunity(nextDeposit) : undefined);\n setSwapType(\"exactOut\");\n };\n\n const handleReset = () => {\n clearPendingSwapIntent();\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n amountEnteredLastValueRef.current = config.prefill?.amount ?? \"\";\n applyConfiguredPrefillsAfterReset();\n rotateAttempt();\n };\n\n const resetSourceSelectionAfterFailure = () => {\n lastAutoIntentSourceTokensRef.current = [];\n lastIntentSourceTokensRef.current = [];\n resetSourcePickerDraft();\n resetExactOutSourcesToAuto();\n };\n\n const handleFailureBack = () => {\n clearPendingSwapIntent();\n resetSourceSelectionAfterFailure();\n setTxError(null);\n void refreshSelectedSourceBalances();\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n rotateAttempt();\n };\n\n const clearTerminalInputState = () => {\n setAmount(\"\");\n setRecipientAddress(\"\");\n setTxError(null);\n setSwapQuoteIssue(null);\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n setFromTokens((current) => (current.length === 0 ? current : []));\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setDepositAmountMode(\"token\");\n };\n\n const resetInputsAfterSuccessfulExecution = () => {\n clearTerminalInputState();\n if (activeMode === \"deposit\") {\n setSelectedOpportunity(configuredDeposit);\n setToToken(\n configuredDeposit\n ? toTokenFromOpportunity(configuredDeposit)\n : undefined\n );\n } else {\n setToToken(undefined);\n }\n };\n\n const handleTerminalReceiptDone = () => {\n clearPendingSwapIntent();\n clearTerminalInputState();\n void refreshSelectedSourceBalances();\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n amountEnteredLastValueRef.current = \"\";\n rotateAttempt();\n };\n\n const handleModalOpenChange = useCallback(\n (open: boolean) => {\n if (!open && swapStepRef.current === \"progress\") return;\n if (!isControlledOpen) {\n setInternalOpen(open);\n }\n onOpenChange?.(open);\n if (!open) {\n clearPendingSwapIntent();\n onClose?.();\n }\n },\n [clearPendingSwapIntent, isControlledOpen, onClose, onOpenChange]\n );\n\n const handleClose = () => {\n if (!embed) {\n handleModalOpenChange(false);\n return;\n }\n clearPendingSwapIntent();\n onClose?.();\n };\n\n const handleConnectWallet = async () => {\n if (walletActionPending || nexusLoading) return;\n\n const clickHandler = onConnectClick || onConnectWallet;\n if (clickHandler) {\n setWalletActionPending(true);\n setTxError(null);\n try {\n await clickHandler();\n } catch (error: any) {\n setTxError(error?.message || \"Unable to connect wallet.\");\n } finally {\n setWalletActionPending(false);\n }\n return;\n }\n\n if (isWalletConnectPending) return;\n\n setWalletActionPending(true);\n setTxError(null);\n try {\n let activeConnector = connector;\n\n if (walletStatus !== \"connected\") {\n const nextConnector = connectors[0];\n if (!nextConnector) {\n throw new Error(\"No wallet connector available.\");\n }\n await connectAsync({ connector: nextConnector });\n return;\n }\n\n const effectiveProvider = await getEffectiveWalletProvider(\n activeConnector\n );\n\n if (\n !effectiveProvider ||\n typeof effectiveProvider.request !== \"function\"\n ) {\n throw new Error(\"Wallet provider is not ready yet.\");\n }\n\n await handleInit(effectiveProvider as EthereumProvider, ownerAddress);\n } catch (error: any) {\n setTxError(error?.message || \"Unable to connect wallet.\");\n } finally {\n setWalletActionPending(false);\n }\n };\n\n const handleOpenRecipientEditor = () => {\n if (activeMode === \"swap\" && !recipientAddress && defaultRecipientAddress) {\n setRecipientAddress(defaultRecipientAddress);\n }\n setTxError(null);\n openDrawerStep(\"enter-recipient\");\n };\n\n const handleResetRecipientToDefault = () => {\n setRecipientAddress(defaultRecipientAddress);\n setTxError(null);\n };\n\n const handleSaveRecipient = () => {\n const next = recipientAddress.trim();\n if (!next) {\n setTxError(\"Recipient address is required\");\n return;\n }\n if (!next.endsWith(\".eth\") && !isAddress(next)) {\n setTxError(\"Incorrect address\");\n return;\n }\n if (\n activeMode === \"send\" &&\n ownerAddress &&\n isAddress(next) &&\n next.toLowerCase() === ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\n setRecipientAddress(next);\n setTxError(null);\n closeDrawerToIdle();\n };\n\n /** Start swap flow — v2 SDK per-operation onIntent hooks populate preview. */\n const handleEnterPreview = async (options: { background?: boolean } = {}) => {\n const { background = false } = options;\n const isExactOutFlow = activeMode === \"deposit\" || activeMode === \"send\";\n const quoteInputKey = activeQuoteInputKeyRef.current;\n const isCurrentQuoteInput = () =>\n activeQuoteInputKeyRef.current === quoteInputKey;\n\n if (!toToken) {\n return;\n }\n\n if (isExactOutFlow) {\n if (!hasPositiveDecimalInput(amount)) {\n return;\n }\n } else if (!hasReadyExactInSwapInput(fromTokens, toToken)) {\n if (!background) {\n setTxError(null);\n setSwapQuoteIssue(null);\n }\n return;\n }\n\n const receiveIssue = buildReceiveAmountIssue();\n if (receiveIssue) {\n applyReceiveAmountIssue(receiveIssue);\n if (!background && swapStepRef.current !== \"idle\") {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n }\n return;\n }\n\n const configuredIssue = buildConfiguredAmountIssue();\n if (configuredIssue) {\n clearPreviewForBlockingAmountIssue();\n if (!background && swapStepRef.current !== \"idle\") {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n }\n return;\n }\n\n const sourceBalanceIssue = isExactOutFlow\n ? buildExactOutSourceBalanceIssue()\n : null;\n if (sourceBalanceIssue) {\n clearPreviewForBlockingAmountIssue();\n setSwapQuoteIssue(sourceBalanceIssue);\n if (!background && swapStepRef.current !== \"idle\") {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n }\n return;\n }\n\n if (!background && activeMode === \"deposit\") {\n trackDeposit(\"deposit_confirm_clicked\", {\n amountToken: depositTokenDisplay,\n amountUsd: Number(depositUsdDisplay) || 0,\n selectionMode: sourceSelectionTouched ? \"manual\" : \"auto\",\n sourceCount: (intentData?.sources ?? []).length,\n });\n }\n\n setTxError(null);\n setSwapQuoteIssue(null);\n\n if (\n !background &&\n swapIntentRef.current?.runId === swapRunIdRef.current &&\n swapIntentRef.current?.quoteInputKey === quoteInputKey &&\n intentData &&\n (activeMode !== \"send\" || Boolean(recipientAddress)) &&\n ((activeMode !== \"deposit\" && activeMode !== \"send\") ||\n (intentData.sources ?? []).length > 0 ||\n Boolean(intentData.destination))\n ) {\n swapStepRef.current = \"preview-intent\";\n setSwapStep(\"preview-intent\");\n return;\n }\n\n let resolvedRecipientAddress =\n activeMode === \"swap\" ? effectiveRecipientAddress : recipientAddress;\n\n if (!background && activeMode === \"send\" && !resolvedRecipientAddress) {\n setTxError(\"Recipient address is required\");\n return;\n }\n\n if ((!background && activeMode === \"send\") || hasCustomSwapRecipient) {\n if (!resolvedRecipientAddress) {\n setTxError(\"Recipient address is required\");\n return;\n }\n\n if (\n activeMode === \"send\" &&\n ownerAddress &&\n isAddress(resolvedRecipientAddress) &&\n resolvedRecipientAddress.toLowerCase() === ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\n\n if (resolvedRecipientAddress.endsWith(\".eth\")) {\n try {\n const mainnetClient =\n publicClient?.chain?.id === 1\n ? publicClient\n : createPublicClient({\n chain: mainnet,\n transport: http(),\n });\n const ensAddr = await mainnetClient.getEnsAddress({\n name: normalize(resolvedRecipientAddress),\n });\n if (!ensAddr) {\n setTxError(\"Could not resolve ENS name to an address.\");\n return;\n }\n resolvedRecipientAddress = ensAddr;\n } catch (e: any) {\n setTxError(e.message || \"Failed to resolve ENS name.\");\n return;\n }\n } else {\n if (!isAddress(resolvedRecipientAddress)) {\n setTxError(\"Invalid recipient address.\");\n return;\n }\n }\n\n if (\n activeMode === \"send\" &&\n ownerAddress &&\n isAddress(resolvedRecipientAddress) &&\n resolvedRecipientAddress.toLowerCase() === ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\n }\n\n if (!isCurrentQuoteInput()) {\n return;\n }\n\n if (!background) {\n swapStepRef.current = \"preview-intent\";\n setSwapStep(\"preview-intent\");\n }\n setIntentLoading(true);\n setQuoteRefreshing(background);\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n swapIntentRef.current?.deny();\n swapIntentRef.current = null;\n if (!background) {\n resetProgressEvents();\n swapStepsListRef.current = [];\n resetSteps();\n }\n\n if (!nexusSDK) {\n setTxError(\"SDK not initialized\");\n if (!background) {\n setSwapStep(\"idle\");\n }\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n swapRunIdRef.current += 1;\n const runId = swapRunIdRef.current;\n\n const isActionPlanStep = (step: SwapStepType | BridgeStepType) => {\n const type = getProgressStepType(step);\n return (\n type === \"APPROVAL\" ||\n type === \"TRANSACTION_SENT\" ||\n type === \"TRANSACTION_CONFIRMED\"\n );\n };\n\n const hasSwapPlanSteps = (stepList: Array) =>\n stepList.some((step) => !isActionPlanStep(step));\n\n const handleProgressStepSideEffects = (\n event: any,\n step: SwapStepType | BridgeStepType,\n completed: boolean\n ) => {\n const type = getProgressStepType(step);\n const rawStepType = String(\n event?.stepType ?? (step as any)?.type ?? (step as any)?.typeID ?? \"\"\n ).toLowerCase();\n const rawState = String(event?.state ?? \"\").toLowerCase();\n const explorerUrl = getPlanStepExplorerUrl(event, step);\n const sourceMovementStep = isSourceMovementProgressStep(type, rawStepType);\n const hasSourceMovementTransaction = Boolean(\n explorerUrl || getPlanStepTransactionHash(event, step)\n );\n const intentExplorerUrl = getEventIntentExplorerUrl(\n appConfig.nexusNetwork,\n event,\n step\n );\n\n patchCurrentIntentExplorerUrl(intentExplorerUrl);\n\n if (\n type === \"TRANSACTION_SENT\" ||\n type === \"TRANSACTION_CONFIRMED\" ||\n type === \"SOURCE_SWAP\" ||\n type === \"BRIDGE_DEPOSIT\" ||\n type === \"BRIDGE_INTENT_SUBMISSION\" ||\n type === \"BRIDGE_FILL\" ||\n type === \"DESTINATION_SWAP\" ||\n type === \"SWAP_COMPLETE\" ||\n type === \"SWAP_SKIPPED\"\n ) {\n markSwapExecutionStarted();\n }\n\n if (\n sourceMovementStep &&\n hasSourceMovementTransaction &&\n PLAN_STEP_FUNDS_MAY_HAVE_MOVED_STATES.has(rawState)\n ) {\n fundsMovedRef.current = true;\n }\n\n if (explorerUrl) {\n if (\n rawStepType === \"destination_swap\" ||\n rawStepType === \"execute_transaction\" ||\n type.includes(\"DESTINATION_SWAP\") ||\n type === \"TRANSACTION_SENT\" ||\n type === \"TRANSACTION_CONFIRMED\"\n ) {\n mergeExplorerUrls({ destinationExplorerUrl: explorerUrl });\n } else if (\n sourceMovementStep\n ) {\n mergeExplorerUrls({ sourceExplorerUrl: explorerUrl });\n }\n\n if (\n !intentUrlRef.current &&\n (rawStepType === \"bridge_intent_submission\" ||\n rawStepType === \"request_submission\" ||\n type === \"BRIDGE_INTENT_SUBMISSION\")\n ) {\n patchCurrentIntentExplorerUrl(explorerUrl);\n }\n }\n\n if (completed) {\n onStepComplete(step as SwapStepType);\n }\n };\n\n const handlePlanEvent = (event: any) => {\n if (event.type === \"plan_preview\" || event.type === \"plan_confirmed\") {\n const stepList = Array.isArray(event.plan?.steps)\n ? event.plan.steps.map((step: any) =>\n normalizePlanStep(step, step?.type, undefined, false)\n )\n : [];\n logSwapPlanSteps(event.type, stepList, event.plan?.steps);\n if (stepList.length === 0) return;\n\n if (hasSwapPlanSteps(stepList)) {\n swapStepsListRef.current = stepList as SwapStepType[];\n appendProgressListEvent(\n PROGRESS_EVENT_NAMES.SWAP_PLAN_LIST,\n stepList\n );\n } else {\n appendProgressListEvent(\n PROGRESS_EVENT_NAMES.BRIDGE_PLAN_LIST,\n stepList\n );\n }\n onStepsList(stepList as SwapStepType[]);\n return;\n }\n\n if (event.type !== \"plan_progress\") {\n logSdkSwapEvent(\"unhandled typed event\", event);\n return;\n }\n\n const state = String(event.state ?? \"\").toLowerCase();\n const completed = PLAN_FINAL_STATES.has(state);\n const step = normalizePlanStep(\n event.step,\n event.stepType,\n event.state,\n completed\n );\n const eventName = isActionPlanStep(step)\n ? PROGRESS_EVENT_NAMES.BRIDGE_PLAN_PROGRESS\n : PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS;\n\n logSwapPlanProgress(event, step, eventName, completed);\n appendProgressEvent(eventName, step, completed, event);\n handleProgressStepSideEffects(event, step, completed);\n };\n\n const appendSkippedSwapProgress = () => {\n const step = {\n completed: true,\n type: \"SWAP_SKIPPED\",\n typeID: \"SWAP_SKIPPED\",\n } as SwapStepType;\n enterSkippedSwapProgress();\n appendProgressEvent(PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS, step, true);\n onStepComplete(step);\n };\n\n const handleSwapEvent = (event: any) => {\n if (!event || typeof event !== \"object\") return;\n if (typeof event.type === \"string\") {\n handlePlanEvent(event);\n return;\n }\n logSdkSwapEvent(\"ignored event without string type\", event);\n };\n\n const onEvent = (event: any) => {\n const isCurrentRun = swapRunIdRef.current === runId;\n const isCurrentQuote = isCurrentQuoteInput();\n logSdkSwapEvent(\"onEvent\", event, {\n currentRunId: swapRunIdRef.current,\n isCurrentQuote,\n isCurrentRun,\n quoteInputKey,\n runId,\n });\n if (!isCurrentRun || !isCurrentQuote) {\n logSdkSwapEvent(\"ignored stale onEvent\", event, {\n currentRunId: swapRunIdRef.current,\n isCurrentQuote,\n quoteInputKey,\n runId,\n });\n return;\n }\n patchCurrentIntentExplorerUrl(\n getEventIntentExplorerUrl(appConfig.nexusNetwork, event)\n );\n handleSwapEvent(event);\n };\n\n const buildRecipientTransferExecuteConfig = (transferAmount: bigint) => {\n if (!resolvedRecipientAddress) {\n throw new Error(\"Recipient address is required\");\n }\n\n const isNative =\n !toToken.contractAddress ||\n toToken.contractAddress.toLowerCase() ===\n \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\" ||\n toToken.contractAddress ===\n \"0x0000000000000000000000000000000000000000\";\n\n if (isNative) {\n return {\n to: resolvedRecipientAddress as `0x${string}`,\n value: transferAmount,\n gas: BigInt(100000),\n };\n }\n\n return {\n to: toToken.contractAddress as `0x${string}`,\n data: encodeFunctionData({\n abi: erc20Abi,\n functionName: \"transfer\",\n args: [resolvedRecipientAddress as `0x${string}`, transferAmount],\n }),\n gas: BigInt(100000),\n };\n };\n\n const executeRecipientTransfer = async (transferAmount: bigint) => {\n const result = await nexusSDK.execute(\n {\n toChainId: toToken.chainId!,\n ...buildRecipientTransferExecuteConfig(transferAmount),\n },\n { onEvent }\n );\n const finalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result\n );\n if (finalExplorerUrl) {\n setTransferExplorerUrl(finalExplorerUrl);\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n return finalExplorerUrl;\n };\n\n try {\n if (!isExactOutFlow) {\n const fromPayload: {\n chainId: number;\n tokenAddress: `0x${string}`;\n amountRaw: bigint;\n }[] = [];\n\n const exactInSourceTokens = getReadyExactInSourceTokens(fromTokens);\n\n for (const token of exactInSourceTokens) {\n // Determine the amount to use for this specific token\n let rawAmountStr = token.userAmount;\n if (!rawAmountStr && exactInSourceTokens.length === 1) {\n rawAmountStr = amount; // fallback for single-token case\n }\n\n let cleanAmount = parseFiatNumber(rawAmountStr) ?? new Decimal(0);\n if (cleanAmount.lte(0)) continue;\n\n if (token.userAmountMode === \"usd\") {\n const tokenBalance =\n parseFiatNumber(token.balance) ?? new Decimal(0);\n const fiatBalance =\n parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n const price = tokenBalance.gt(0)\n ? fiatBalance.div(tokenBalance)\n : new Decimal(0);\n if (price.gt(0)) {\n cleanAmount = cleanAmount.div(price);\n } else {\n cleanAmount = new Decimal(0);\n }\n }\n\n if (cleanAmount.lte(0)) continue;\n\n const safeTokenAmountStr = toViemDecimalString(\n cleanAmount,\n token.decimals || 18\n );\n\n fromPayload.push({\n chainId: token.chainId!,\n tokenAddress: token.contractAddress as `0x${string}`,\n amountRaw: parseUnits(safeTokenAmountStr, token.decimals || 18),\n });\n }\n\n if (fromPayload.length === 0) {\n throw new Error(\"No source amount available for swap.\");\n }\n\n resetExplorerUrls();\n const exactInSwapPayload = {\n sources: fromPayload,\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n };\n let intentExplorerUrl: string | null = null;\n let intentId = currentSwapEntry?.intentId;\n let finalExplorerUrl: string | null =\n explorerUrlsRef.current.destinationExplorerUrl ||\n explorerUrlsRef.current.sourceExplorerUrl;\n\n if (hasCustomSwapRecipient && resolvedRecipientAddress) {\n const sdkWithOptionalTransfer = nexusSDK as any;\n\n if (typeof sdkWithOptionalTransfer.swapAndTransfer === \"function\") {\n const swapAndTransferExactInInput = {\n mode: \"exactIn\",\n recipient: resolvedRecipientAddress as `0x${string}`,\n ...exactInSwapPayload,\n };\n logSdkIntentInput(\"swapAndTransfer exactIn\", swapAndTransferExactInInput, {\n activeMode,\n quoteInputKey,\n runId,\n });\n const result = await sdkWithOptionalTransfer.swapAndTransfer(\n swapAndTransferExactInInput,\n { onEvent }\n );\n if (result?.success === false) {\n throw new Error(result?.error || \"Swap and transfer failed\");\n }\n\n const swapResult = getSdkSwapResult(result);\n intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result,\n swapResult\n );\n intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const resultFinalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result,\n swapResult\n );\n finalExplorerUrl = resultFinalExplorerUrl || finalExplorerUrl;\n if (resultFinalExplorerUrl) {\n setTransferExplorerUrl(resultFinalExplorerUrl);\n mergeExplorerUrls({\n destinationExplorerUrl: resultFinalExplorerUrl,\n });\n }\n } else {\n logSdkIntentInput(\"swapWithExactIn\", exactInSwapPayload, {\n activeMode,\n quoteInputKey,\n runId,\n });\n startIntentFetchTiming({\n background,\n operation: \"swapWithExactIn\",\n quoteInputKey,\n runId,\n });\n const result = await nexusSDK.swapWithExactIn(exactInSwapPayload, {\n hooks: {\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n },\n onEvent,\n });\n\n intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result\n );\n intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n\n const latestSwapIntent = (\n swapIntentRef.current as unknown as {\n intent?: SwapIntentData;\n } | null\n )?.intent;\n const transferAmount = latestSwapIntent?.destination?.amount;\n if (!transferAmount) {\n throw new Error(\n \"Unable to determine received amount to transfer.\"\n );\n }\n\n const transferAmountBigInt = parseUnits(\n toViemDecimalString(transferAmount, toToken.decimals || 18),\n toToken.decimals || 18\n );\n finalExplorerUrl =\n (await executeRecipientTransfer(transferAmountBigInt)) ||\n finalExplorerUrl;\n }\n } else {\n // Start exact-in swap — the intent hook will fire and populate preview\n logSdkIntentInput(\"swapWithExactIn\", exactInSwapPayload, {\n activeMode,\n quoteInputKey,\n runId,\n });\n startIntentFetchTiming({\n background,\n operation: \"swapWithExactIn\",\n quoteInputKey,\n runId,\n });\n const result = await nexusSDK.swapWithExactIn(exactInSwapPayload, {\n hooks: {\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n },\n onEvent,\n });\n intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result\n );\n intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const swapResult = getSdkSwapResult(result);\n const resultFinalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result,\n swapResult\n );\n finalExplorerUrl = resultFinalExplorerUrl || finalExplorerUrl;\n if (resultFinalExplorerUrl) {\n setTransferExplorerUrl(resultFinalExplorerUrl);\n mergeExplorerUrls({\n destinationExplorerUrl: resultFinalExplorerUrl,\n });\n }\n }\n\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n const resolvedFinalExplorerUrl =\n finalExplorerUrl ||\n explorerUrlsRef.current.destinationExplorerUrl ||\n explorerUrlsRef.current.sourceExplorerUrl;\n const resolvedIntentExplorerUrl =\n intentExplorerUrl || intentUrlRef.current;\n finishCurrentSwapHistoryEntry(\"fulfilled\", {\n finalExplorerUrl: resolvedFinalExplorerUrl,\n ...(resolvedIntentExplorerUrl\n ? { intentExplorerUrl: resolvedIntentExplorerUrl }\n : {}),\n ...(intentId ? { intentId } : {}),\n });\n resetInputsAfterSuccessfulExecution();\n onComplete?.();\n setSwapStep(\"success\");\n }\n } else {\n const exactOutAmountString =\n activeMode === \"deposit\"\n ? depositTokenAmountForQuote\n ?.toDecimalPlaces(toToken.decimals || 18, Decimal.ROUND_DOWN)\n .toFixed()\n : amount;\n if (!exactOutAmountString || new Decimal(exactOutAmountString).lte(0)) {\n setTxError(\n depositAmountMode === \"usd\"\n ? \"Unable to convert USD amount into the destination token amount.\"\n : \"Enter a valid amount.\"\n );\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n const amountBigInt = parseUnits(\n toViemDecimalString(exactOutAmountString, toToken.decimals || 18),\n toToken.decimals || 18\n );\n\n resetExplorerUrls();\n\n const fromSourcesPayload = buildExactOutSourcesPayload(\n getExactOutSourceTokens()\n );\n\n let executeConfig: any;\n if (activeMode === \"deposit\" && !selectedOpportunity?.executeDeposit) {\n throw new Error(\"Deposit config is missing executeDeposit.\");\n }\n\n if (activeMode === \"deposit\" && selectedOpportunity) {\n const user = (ownerAddress ?? connectedAddress) as `0x${string}`;\n\n const executeParams = selectedOpportunity.executeDeposit(\n selectedOpportunity.tokenSymbol,\n selectedOpportunity.tokenAddress,\n amountBigInt,\n selectedOpportunity.chainId,\n user\n );\n if (!isPositiveGasLimit(executeParams.gas)) {\n throw new Error(\n \"Deposit config executeDeposit must return a positive gas limit.\"\n );\n }\n executeConfig = {\n to: executeParams.to,\n value: executeParams.value,\n data: executeParams.data,\n tokenApproval: executeParams.tokenApproval,\n gas: executeParams.gas,\n };\n } else if (\n (activeMode === \"send\" || hasCustomSwapRecipient) &&\n resolvedRecipientAddress\n ) {\n executeConfig = buildRecipientTransferExecuteConfig(amountBigInt);\n }\n\n if (executeConfig?.tokenApproval) {\n executeConfig = {\n ...executeConfig,\n tokenApproval: {\n toTokenAddress:\n executeConfig.tokenApproval.toTokenAddress ||\n toToken.contractAddress,\n amount: executeConfig.tokenApproval.amount,\n spender: executeConfig.tokenApproval.spender,\n },\n };\n }\n\n if (executeConfig) {\n const sdkWithOptionalTransfer = nexusSDK as any;\n const isTransferExactOut =\n (activeMode === \"send\" || hasCustomSwapRecipient) &&\n typeof sdkWithOptionalTransfer.swapAndTransfer === \"function\";\n const exactOutOperationInput = isTransferExactOut\n ? {\n mode: \"exactOut\",\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmountRaw: amountBigInt,\n recipient: resolvedRecipientAddress as `0x${string}`,\n ...fromSourcesPayload,\n }\n : {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmountRaw: amountBigInt,\n execute: executeConfig,\n ...fromSourcesPayload,\n };\n logSdkIntentInput(\n isTransferExactOut\n ? \"swapAndTransfer exactOut\"\n : \"swapAndExecute exactOut\",\n exactOutOperationInput,\n {\n activeMode,\n quoteInputKey,\n runId,\n }\n );\n startIntentFetchTiming({\n background,\n operation: isTransferExactOut\n ? \"swapAndTransfer exactOut\"\n : \"swapAndExecute exactOut\",\n quoteInputKey,\n runId,\n });\n const result =\n isTransferExactOut\n ? await sdkWithOptionalTransfer.swapAndTransfer(\n exactOutOperationInput,\n {\n onEvent,\n onIntent: (data: any) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n }\n )\n : await nexusSDK.swapAndExecute(\n exactOutOperationInput as any,\n {\n onEvent,\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n }\n );\n\n const swapResult = result?.swapResult ?? result?.result ?? null;\n const swapSkipped = Boolean((result as any)?.swapSkipped);\n if (swapSkipped) {\n appendSkippedSwapProgress();\n }\n if (\n !swapResult &&\n !swapSkipped &&\n activeMode !== \"send\" &&\n !hasCustomSwapRecipient\n ) {\n throw new Error(\"Swap failed\");\n }\n const executeTxHash = getSdkTransactionHash(result);\n const intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result,\n swapResult\n );\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const finalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n executeTxHash,\n result,\n swapResult\n );\n if (finalExplorerUrl) {\n if (activeMode === \"send\" || hasCustomSwapRecipient) {\n setTransferExplorerUrl(finalExplorerUrl);\n }\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n patchCurrentSwapHistoryEntry({\n ...(finalExplorerUrl ? { finalExplorerUrl } : {}),\n ...(intentExplorerUrl ? { intentExplorerUrl } : {}),\n ...(intentId ? { intentId } : {}),\n });\n } else {\n const exactOutSwapInput = {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmountRaw: amountBigInt,\n ...fromSourcesPayload,\n };\n logSdkIntentInput(\"swapWithExactOut\", exactOutSwapInput, {\n activeMode,\n quoteInputKey,\n runId,\n });\n startIntentFetchTiming({\n background,\n operation: \"swapWithExactOut\",\n quoteInputKey,\n runId,\n });\n const result = await nexusSDK.swapWithExactOut(\n exactOutSwapInput,\n {\n hooks: {\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n },\n onEvent,\n }\n );\n const intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result\n );\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const swapResult = getSdkSwapResult(result);\n const finalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result,\n swapResult\n );\n if (finalExplorerUrl) {\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n patchCurrentSwapHistoryEntry({\n ...(finalExplorerUrl ? { finalExplorerUrl } : {}),\n ...(intentExplorerUrl ? { intentExplorerUrl } : {}),\n ...(intentId ? { intentId } : {}),\n });\n }\n\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n finishCurrentSwapHistoryEntry(\"fulfilled\");\n resetInputsAfterSuccessfulExecution();\n onComplete?.();\n if (activeMode === \"deposit\") {\n reachedTerminalRef.current = true;\n const now = Date.now();\n trackDeposit(\"deposit_completed\", {\n postConfirmDurationMs: previewConfirmedTsRef.current\n ? now - previewConfirmedTsRef.current\n : 0,\n totalDurationMs: now - widgetOpenedTsRef.current,\n attemptCount: attemptCountRef.current,\n amountToken: depositTokenDisplay,\n amountUsd: Number(depositUsdDisplay) || 0,\n });\n }\n setSwapStep(\"success\");\n }\n }\n } catch (err: any) {\n const caughtTimeout = isTimeoutLikeError(err);\n if (caughtTimeout) {\n console.warn(\"Timeout in handleEnterPreview:\", err);\n } else {\n console.error(\"Error in handleEnterPreview:\", err);\n }\n if (swapRunIdRef.current !== runId || !isCurrentQuoteInput()) {\n return;\n }\n finishIntentFetchTiming(runId, \"failed\");\n if (activeMode === \"deposit\" && err?.code !== \"USER_DENIED_INTENT\") {\n const hasActiveExecution =\n swapStepRef.current === \"progress\" &&\n Boolean(currentSwapIdRef.current);\n const isInsufficient = isInsufficientSourcesError(err);\n const errMessage =\n (typeof err?.message === \"string\" ? err.message : \"\") ||\n (typeof err === \"string\" ? err : \"\");\n const errName = typeof err?.name === \"string\" ? err.name : \"\";\n const isTimeout = isTimeoutLikeError(err);\n const isUserRejected =\n err?.code === 4001 ||\n err?.code === \"ACTION_REJECTED\" ||\n errName === \"UserRejectedRequestError\" ||\n /user rejected|user denied/i.test(errMessage);\n const failedAtStep:\n | \"simulation\"\n | \"nexus_operation\"\n | \"execute_leg\"\n | \"unknown\" = !hasActiveExecution ? \"simulation\" : \"nexus_operation\";\n const errorCategory: string = isUserRejected\n ? \"user_rejected\"\n : isTimeout\n ? \"timeout\"\n : isInsufficient\n ? \"no_eligible_sources\"\n : !hasActiveExecution\n ? \"quote_failed\"\n : fundsMovedRef.current\n ? \"pending_after_funds_moved\"\n : \"execution_failed\";\n reachedTerminalRef.current = true;\n if (fundsMovedRef.current) {\n trackDeposit(\"deposit_pending_after_funds_moved\", {\n errorCode: err?.code ?? \"UNKNOWN\",\n errorMessage: errMessage || \"Transaction pending.\",\n intentUrl: intentUrlRef.current,\n failedAtStep,\n });\n } else {\n trackDeposit(\"deposit_failed\", {\n errorCode: err?.code ?? \"UNKNOWN\",\n errorCategory,\n errorMessage: errMessage || \"Transaction failed.\",\n failedAtStep,\n });\n }\n }\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n const hasActiveExecution =\n swapStepRef.current === \"progress\" && Boolean(currentSwapIdRef.current);\n const isTimeout = caughtTimeout;\n const showFailedProgressThenReceipt = (\n error: string,\n patch: Partial = {}\n ) => {\n const failedProgressEvent = progressEventsRef.current.at(-1);\n const isTransferExecution =\n activeMode === \"send\" || hasCustomSwapRecipient;\n const fallbackFailedStep =\n activeMode === \"deposit\" || isTransferExecution\n ? ({ type: \"APPROVAL\", typeID: \"AP\" } as BridgeStepType)\n : ({\n type: \"DETERMINING_SWAP\",\n typeID: \"DETERMINING_SWAP\",\n } as unknown as SwapStepType);\n const failedStep = failedProgressEvent?.step ?? fallbackFailedStep;\n const autoRefundAvailable =\n isAutoRefundAvailableProgressEvent(failedProgressEvent);\n setFailedProgressStep(failedStep);\n finishCurrentSwapHistoryEntry(\"failed\", {\n error,\n autoRefundAvailable,\n failureDescription: getFailureDescriptionForProgressStep(\n failedStep,\n autoRefundAvailable\n ),\n failureMessage: getFailureMessageForProgressStep(\n failedStep,\n hasCustomSwapRecipient ? \"send\" : activeMode,\n autoRefundAvailable\n ),\n failedStepType: getProgressStepType(failedStep),\n ...patch,\n });\n window.setTimeout(() => {\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n setSwapStep(\"failed\");\n }\n }, 700);\n };\n const showTimeoutReceipt = (\n message = \"Transaction timed out\",\n patch: Partial = {}\n ) => {\n finishCurrentSwapHistoryEntry(\"timeout\", {\n error: message,\n failureDescription:\n \"This transaction is still pending. Check the intent explorer for the latest status.\",\n failureMessage: TIMEOUT_LABEL,\n ...patch,\n });\n window.setTimeout(() => {\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n setSwapStep(\"failed\");\n }\n }, 700);\n };\n if (err?.code === \"USER_DENIED_INTENT\") {\n if (hasActiveExecution) {\n showFailedProgressThenReceipt(\"Transaction cancelled by user\");\n } else if (!background && swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n return;\n }\n if (isInsufficientSourcesError(err) && !hasActiveExecution) {\n const issue = buildInsufficientSourcesIssue(err);\n if (!background || swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n setTxError(null);\n setSwapQuoteIssue(issue);\n onError?.(issue.message);\n return;\n }\n if (isExactOutFlow && isViemInvalidDecimalError(err) && !hasActiveExecution) {\n const issue = buildExactOutSourceBalanceIssue();\n if (issue) {\n if (!background || swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n setTxError(null);\n setSwapQuoteIssue(issue);\n onError?.(issue.message);\n return;\n }\n }\n const errorMessage =\n err?.message ||\n (typeof err === \"string\"\n ? err\n : \"Transaction failed. Please try again or check console.\");\n if (isTimeout && hasActiveExecution) {\n showTimeoutReceipt(errorMessage);\n setTxError(null);\n return;\n }\n if (hasActiveExecution && fundsMovedRef.current) {\n showTimeoutReceipt(errorMessage, {\n failureDescription:\n \"Funds have already moved and the transaction is still pending. Check the intent explorer for the latest status.\",\n });\n setTxError(null);\n return;\n }\n if (hasActiveExecution) {\n showFailedProgressThenReceipt(errorMessage);\n } else if (!background || swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n setTxError(errorMessage);\n onError?.(errorMessage);\n }\n };\n\n const hasInsufficientSourcesQuoteIssue =\n swapQuoteIssue?.type === \"insufficientSources\";\n const hasReceiveAmountQuoteIssue = Boolean(receiveAmountIssue);\n\n useEffect(() => {\n if (activeMode !== \"swap\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = false;\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n }\n\n if (hasReceiveAmountQuoteIssue) {\n clearPendingSwapIntent(true);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n if (hasInsufficientSourcesQuoteIssue) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n const hasEnoughForQuote = hasReadyExactInSwapInput(fromTokens, toToken);\n\n if (!hasEnoughForQuote) {\n clearPendingSwapIntent();\n setSwapQuoteIssue(null);\n setTxError(null);\n return;\n }\n\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n return;\n }\n\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n let quoteStarted = false;\n const quoteDelay = getQuoteRequestDelay();\n const timer = window.setTimeout(() => {\n quoteStarted = true;\n void handleEnterPreview({ background: true });\n }, quoteDelay);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (!quoteStarted && swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n activeQuoteInputKey,\n amount,\n defaultRecipientAddress,\n fromTokensQuoteKey,\n hasCurrentQuoteIntent,\n hasInsufficientSourcesQuoteIssue,\n hasReceiveAmountQuoteIssue,\n getQuoteRequestDelay,\n nexusSDK,\n recipientAddress,\n swapStep,\n toTokenQuoteKey,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = false;\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n }\n\n if (hasReceiveAmountQuoteIssue) {\n clearPendingSwapIntent(true);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n if (hasInsufficientSourcesQuoteIssue) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n const parsedAmount = parseFiatNumber(amount);\n const hasEnoughForQuote = Boolean(\n parsedAmount?.gt(0) &&\n toToken &&\n selectedOpportunity &&\n depositTokenAmountForQuote\n );\n\n if (!hasEnoughForQuote) {\n clearPendingSwapIntent();\n clearSelectedSources();\n return;\n }\n\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n return;\n }\n\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n let quoteStarted = false;\n const quoteDelay = getQuoteRequestDelay();\n const timer = window.setTimeout(() => {\n quoteStarted = true;\n void handleEnterPreview({ background: true });\n }, quoteDelay);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (!quoteStarted && swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n amount,\n activeQuoteInputKey,\n depositAmountMode,\n depositQuoteAmountKey,\n hasCurrentQuoteIntent,\n hasInsufficientSourcesQuoteIssue,\n hasReceiveAmountQuoteIssue,\n getQuoteRequestDelay,\n nexusSDK,\n sourceSelectionRevision,\n selectedOpportunityIdentity,\n swapStep,\n toTokenQuoteKey,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"send\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = false;\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n }\n\n if (hasReceiveAmountQuoteIssue) {\n clearPendingSwapIntent(true);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n if (hasInsufficientSourcesQuoteIssue) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n const parsedAmount = parseFiatNumber(amount);\n const hasEnoughForQuote = Boolean(parsedAmount?.gt(0) && toToken);\n\n if (!hasEnoughForQuote) {\n clearPendingSwapIntent();\n clearSelectedSources();\n return;\n }\n\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n return;\n }\n\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n let quoteStarted = false;\n const quoteDelay = getQuoteRequestDelay();\n const timer = window.setTimeout(() => {\n quoteStarted = true;\n void handleEnterPreview({ background: true });\n }, quoteDelay);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (!quoteStarted && swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n amount,\n activeQuoteInputKey,\n hasCurrentQuoteIntent,\n hasInsufficientSourcesQuoteIssue,\n hasReceiveAmountQuoteIssue,\n getQuoteRequestDelay,\n nexusSDK,\n sourceSelectionRevision,\n swapStep,\n toTokenQuoteKey,\n ]);\n\n const refreshActiveSwapIntent = useCallback(async () => {\n if (receiveAmountIssueRef.current) return;\n\n const activeIntent = swapIntentRef.current;\n if (\n !activeIntent ||\n intentLoading ||\n quoteRefreshing ||\n receiveMaxCalculating ||\n previewQuoteRefreshing\n ) {\n return;\n }\n\n const runId = activeIntent.runId;\n const quoteInputKey = activeIntent.quoteInputKey;\n if (!quoteInputKey || activeQuoteInputKeyRef.current !== quoteInputKey) {\n return;\n }\n const isPreviewRefresh = swapStepRef.current === \"preview-intent\";\n if (isPreviewRefresh) {\n setPreviewQuoteRefreshing(true);\n } else {\n setQuoteRefreshing(true);\n }\n try {\n const updatedRaw = await activeIntent.refresh();\n const updatedBridgeProvider = normalizeBridgeProvider(\n (updatedRaw as any)?.bridgeProvider ??\n (updatedRaw as any)?.normalizedIntent?.bridgeProvider ??\n (updatedRaw as any)?.swap?.bridgeProvider ??\n activeIntent.intent?.bridgeProvider\n );\n const updated = normalizeRenderableSwapIntentData(\n updatedRaw,\n updatedBridgeProvider\n );\n if (\n !updated ||\n swapRunIdRef.current !== runId ||\n activeQuoteInputKeyRef.current !== quoteInputKey\n ) {\n return;\n }\n\n if (swapIntentRef.current) {\n swapIntentRef.current.intent = updated;\n }\n applySwapIntent(updated);\n } catch (err) {\n console.error(\"Unable to refresh swap intent\", err);\n } finally {\n if (\n swapRunIdRef.current === runId &&\n activeQuoteInputKeyRef.current === quoteInputKey\n ) {\n if (isPreviewRefresh) {\n setPreviewQuoteRefreshing(false);\n } else {\n setQuoteRefreshing(false);\n }\n }\n }\n }, [\n applySwapIntent,\n intentLoading,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n ]);\n\n useEffect(() => {\n const hasRefreshableIntent =\n (activeMode === \"swap\" ||\n activeMode === \"deposit\" ||\n activeMode === \"send\") &&\n Boolean(\n intentData &&\n swapIntentRef.current &&\n swapIntentRef.current.quoteInputKey === activeQuoteInputKey\n ) &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n\n if (!hasRefreshableIntent || receiveAmountIssue) {\n setQuoteRefreshProgress(0);\n setQuoteRefreshSecondsRemaining(0);\n return;\n }\n\n let cancelled = false;\n let timeout: number | undefined;\n\n const scheduleRefresh = () => {\n const quoteAge = Date.now() - lastSwapIntentRefreshAtRef.current;\n const delay = Math.max(0, QUOTE_REFRESH_INTERVAL_MS - quoteAge);\n timeout = window.setTimeout(() => {\n if (receiveAmountIssueRef.current) {\n clearPendingSwapIntent(true);\n setQuoteRefreshProgress(0);\n setQuoteRefreshSecondsRemaining(0);\n return;\n }\n\n if (\n intentLoading ||\n quoteRefreshing ||\n receiveMaxCalculating ||\n previewQuoteRefreshing\n ) {\n if (!cancelled) {\n timeout = window.setTimeout(scheduleRefresh, 1000);\n }\n return;\n }\n\n void refreshActiveSwapIntent().finally(() => {\n if (!cancelled) {\n scheduleRefresh();\n }\n });\n }, delay);\n };\n\n scheduleRefresh();\n\n return () => {\n cancelled = true;\n if (timeout !== undefined) {\n window.clearTimeout(timeout);\n }\n };\n }, [\n activeMode,\n activeQuoteInputKey,\n intentData,\n intentLoading,\n receiveAmountIssue,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n refreshActiveSwapIntent,\n swapStep,\n ]);\n\n useEffect(() => {\n const hasRefreshableIntent =\n (activeMode === \"swap\" ||\n activeMode === \"deposit\" ||\n activeMode === \"send\") &&\n Boolean(\n intentData &&\n swapIntentRef.current &&\n swapIntentRef.current.quoteInputKey === activeQuoteInputKey\n ) &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n\n if (!hasRefreshableIntent || receiveAmountIssue) {\n setQuoteRefreshProgress(0);\n setQuoteRefreshSecondsRemaining(0);\n return;\n }\n\n const updateProgress = () => {\n const quoteAge = Date.now() - lastSwapIntentRefreshAtRef.current;\n const remaining = Math.max(0, QUOTE_REFRESH_INTERVAL_MS - quoteAge);\n setQuoteRefreshProgress(remaining / QUOTE_REFRESH_INTERVAL_MS);\n setQuoteRefreshSecondsRemaining(Math.ceil(remaining / 1000));\n };\n\n updateProgress();\n const interval = window.setInterval(updateProgress, 250);\n\n return () => window.clearInterval(interval);\n }, [\n activeMode,\n activeQuoteInputKey,\n intentData,\n receiveAmountIssue,\n swapStep,\n ]);\n\n /** User accepted swap from the preview — call allow() from the intent hook */\n const handleSwapAccept = () => {\n const activeIntent = swapIntentRef.current;\n if (activeIntent) {\n if (\n activeIntent.quoteInputKey &&\n activeQuoteInputKeyRef.current !== activeIntent.quoteInputKey\n ) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n setSwapStep(\"idle\");\n return;\n }\n if (activeMode === \"deposit\") {\n previewConfirmedTsRef.current = Date.now();\n attemptCountRef.current += 1;\n const timeInPreviewMs = previewViewedTsRef.current\n ? previewConfirmedTsRef.current - previewViewedTsRef.current\n : 0;\n trackDeposit(\"deposit_preview_confirmed\", {\n timeInPreviewMs,\n totalFeeUsd: Number(intentFeeUsd) || 0,\n sourceCount: (intentData?.sources ?? []).length,\n });\n }\n onStart?.();\n startSwapHistoryEntry();\n setSwapStep(\"progress\");\n setQuoteRefreshing(false);\n resetProgressEvents();\n if (swapStepsListRef.current.length > 0) {\n seed(swapStepsListRef.current);\n } else {\n resetSteps();\n }\n activeIntent.allow();\n }\n };\n\n // ---------------------------------------------------------------------------\n // Header title\n // ---------------------------------------------------------------------------\n const isDepositMethodScreen =\n activeMode === \"deposit\" &&\n swapStep === \"idle\" &&\n depositFundingStep === \"method\";\n const isDepositWalletScreen =\n activeMode === \"deposit\" &&\n swapStep === \"idle\" &&\n depositFundingStep === \"wallet\";\n const isDepositOnrampScreen =\n activeMode === \"deposit\" &&\n swapStep === \"idle\" &&\n depositFundingStep === \"onramp\";\n const normalizedDepositOnrampSessionState =\n depositOnrampSessionState?.trim().toUpperCase() ?? \"\";\n const hasDepositOnrampSession =\n isDepositOnrampScreen && Boolean(depositOnrampSessionState);\n const getDepositOnrampSessionTitle = () => {\n if (\n normalizedDepositOnrampSessionState === \"SETTLED\" ||\n [\n \"COMPLETED\",\n \"DEPOSIT_COMPLETE\",\n \"DEPOSIT_SUCCESS\",\n \"DEPOSITED\",\n ].includes(normalizedDepositOnrampSessionState)\n ) {\n return \"Success\";\n }\n if (normalizedDepositOnrampSessionState === \"FAILED\") {\n return \"Payment failed\";\n }\n if (\n [\n \"DEPOSIT_ATTENTION\",\n \"DEPOSIT_FAILED\",\n \"DEPOSIT_REQUIRES_ATTENTION\",\n ].includes(normalizedDepositOnrampSessionState)\n ) {\n return \"Deposit needs your attention\";\n }\n if (normalizedDepositOnrampSessionState === \"CANCELLED\") {\n return \"Payment cancelled\";\n }\n if (normalizedDepositOnrampSessionState === \"REFUNDED\") {\n return \"Payment refunded\";\n }\n if (normalizedDepositOnrampSessionState === \"EXPIRED\") {\n return \"Payment expired\";\n }\n if (\n [\n \"COMPLETING_DEPOSIT\",\n \"DEPOSIT_PROCESSING\",\n \"DEPOSITING\",\n ].includes(normalizedDepositOnrampSessionState)\n ) {\n return \"Completing your deposit\";\n }\n if (\n normalizedDepositOnrampSessionState === \"PROCESSING\" ||\n normalizedDepositOnrampSessionState === \"SETTLING\"\n ) {\n return \"Processing your payment\";\n }\n return \"Continue on Other Window\";\n };\n\n const getTitle = () => {\n const configuredWidgetHeading = normalizeConfiguredString(\n appearanceConfig?.widgetHeading\n );\n if (swapStep === \"history\") return \"Transaction History\";\n // Drawer panels overlay the main page,\n // so the header should still show the main page title.\n\n if (swapStep === \"preview-intent\") {\n return activeMode === \"deposit\"\n ? \"Confirm Deposit\"\n : activeMode === \"send\"\n ? \"Confirm Send\"\n : \"Confirm Swap\";\n }\n\n if (activeMode === \"swap\") {\n if (swapStep === \"progress\") return \"Swapping…\";\n if (swapStep === \"success\") return \"Swap Complete\";\n if (swapStep === \"failed\" && currentSwapEntry?.status === \"timeout\") {\n return TIMEOUT_LABEL;\n }\n if (swapStep === \"failed\") return \"Swap Failed\";\n return configuredWidgetHeading ?? \"Swap and Bridge\";\n }\n if (activeMode === \"deposit\") {\n if (isDepositOnrampScreen) {\n return hasDepositOnrampSession\n ? getDepositOnrampSessionTitle()\n : \"Payment Amount\";\n }\n if (swapStep === \"progress\") return \"Depositing…\";\n if (swapStep === \"success\") return \"Deposit Complete\";\n if (swapStep === \"failed\" && currentSwapEntry?.status === \"timeout\") {\n return TIMEOUT_LABEL;\n }\n if (swapStep === \"failed\") return \"Deposit Failed\";\n return configuredWidgetHeading ?? \"Deposit\";\n }\n if (activeMode === \"send\") {\n if (swapStep === \"progress\") return \"Sending…\";\n if (swapStep === \"success\") return \"Send Complete\";\n if (swapStep === \"failed\" && currentSwapEntry?.status === \"timeout\") {\n return TIMEOUT_LABEL;\n }\n if (swapStep === \"failed\") return \"Send Failed\";\n return configuredWidgetHeading ?? \"Send\";\n }\n return configuredWidgetHeading ?? \"Nexus Widget\";\n };\n\n // Titles that should be center-aligned (main screens / confirm screens)\n // Left-aligned: choose-swap-asset, choose-receive-asset (sub-screens with subtitles)\n const isTitleCentered = () => {\n if (swapStep === \"history\") return false;\n return true; // idle, drawer panels, preview-intent, progress, etc.\n };\n\n const canGoBack =\n swapStep === \"preview-intent\" ||\n swapStep === \"history\" ||\n isDepositWalletScreen ||\n (isDepositOnrampScreen && !hasDepositOnrampSession);\n const showHistoryButton =\n !(isDepositMethodScreen || isDepositOnrampScreen);\n const handleBack = () => {\n if (\n activeMode === \"deposit\" &&\n swapStep === \"idle\" &&\n depositFundingStep !== \"method\"\n ) {\n setDepositFundingStep(\"method\");\n return;\n }\n if (swapStep === \"history\") {\n setSwapStep(\"idle\");\n return;\n }\n if (swapStep === \"choose-swap-asset\") {\n handleSourcePickerCancel();\n return;\n }\n if (swapStep === \"choose-receive-asset\") {\n closeDrawerToIdle();\n return;\n }\n if (swapStep === \"enter-recipient\") {\n closeDrawerToIdle();\n return;\n }\n if (swapStep === \"preview-intent\") {\n const canRequoteAfterPreviewBack =\n activeMode === \"swap\"\n ? hasReadyExactInSwapInput(fromTokens, toToken)\n : canRefreshExactOutQuote();\n\n if (\n canRequoteAfterPreviewBack &&\n (activeMode === \"deposit\" || activeMode === \"send\")\n ) {\n setExactOutQuoteSourceModeValue(\"all\");\n }\n if (activeMode === \"deposit\" || activeMode === \"send\") {\n invalidateExactOutQuoteForRefresh();\n } else {\n clearPendingSwapIntent(true, {\n keepQuoteRefreshing: canRequoteAfterPreviewBack,\n });\n }\n if (canRequoteAfterPreviewBack && activeMode === \"swap\") {\n setQuoteRefreshing(true);\n setTxError(null);\n setSwapQuoteIssue(null);\n }\n setSwapStep(\"idle\");\n return;\n }\n if (swapStep === \"progress\") {\n return;\n } // can't go back during tx\n setSwapStep(\"idle\");\n };\n\n const handleSwapAmountChange = (val: string, panel: \"send\" | \"receive\") => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n setSwapQuoteIssue(null);\n setTxError(null);\n const nextAmount = parseFiatNumber(val);\n const receiveIssue = buildReceiveAmountIssue({ inputAmount: val });\n applyReceiveAmountIssue(receiveIssue);\n const hasSelectedSourceToken = fromTokens.some(\n (token) => token.chainId && token.contractAddress\n );\n const shouldLoadQuote = Boolean(\n !receiveIssue &&\n nexusSDK &&\n nextAmount?.gt(0) &&\n toToken &&\n hasSelectedSourceToken\n );\n if (!receiveIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n }\n setAmount(val);\n if (panel === \"receive\") {\n setFromTokens((prev) =>\n prev.map((token) => ({ ...token, userAmount: \"\" }))\n );\n }\n // Nexus Widget swaps are exact-in only. Exact-out is reserved for Deposit and Send.\n if (swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n };\n\n const handleSwapTokensUpdate = (tokens: SwapTokenOption[]) => {\n setSwapQuoteIssue(null);\n setTxError(null);\n applyReceiveAmountIssue(buildReceiveAmountIssue({ sourceTokens: tokens }));\n setFromTokens(tokens);\n };\n\n const handleDepositAmountChange = (val: string) => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n resetExactOutSourcesToAuto();\n maxPercentRunRef.current += 1;\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setSwapQuoteIssue(null);\n const nextAmount = parseFiatNumber(val);\n const receiveIssue = buildReceiveAmountIssue({ inputAmount: val });\n applyReceiveAmountIssue(receiveIssue);\n const shouldLoadQuote = Boolean(\n !receiveIssue &&\n nexusSDK &&\n nextAmount?.gt(0) &&\n toToken &&\n selectedOpportunity\n );\n if (!receiveIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n } else {\n clearSelectedSources();\n }\n setAmount(val);\n };\n\n const handleSendAmountChange = (val: string) => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n resetExactOutSourcesToAuto();\n maxPercentRunRef.current += 1;\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setSwapQuoteIssue(null);\n setSwapType(\"exactOut\");\n const nextAmount = parseFiatNumber(val);\n const receiveIssue = buildReceiveAmountIssue({\n inputAmount: val,\n type: \"exactOut\",\n });\n applyReceiveAmountIssue(receiveIssue);\n const shouldLoadQuote = Boolean(\n !receiveIssue && nexusSDK && nextAmount?.gt(0) && toToken\n );\n if (!receiveIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n } else {\n clearSelectedSources();\n }\n setAmount(val);\n };\n\n const handleDepositAmountModeToggle = () => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n resetExactOutSourcesToAuto();\n const rate = getDepositTokenUsdRate();\n const parsedAmount = parseFiatNumber(amount) ?? new Decimal(0);\n if (parsedAmount.gt(0) && rate.gt(0)) {\n const converted =\n depositAmountMode === \"token\"\n ? parsedAmount.mul(rate).toDecimalPlaces(2)\n : parsedAmount\n .div(rate)\n .toDecimalPlaces(\n getCappedTokenDisplayDecimals(toToken?.decimals),\n Decimal.ROUND_DOWN\n );\n setAmount(converted.toFixed());\n }\n clearPendingSwapIntent();\n setDepositAmountMode((current) => (current === \"token\" ? \"usd\" : \"token\"));\n };\n\n const handleDepositPercentSelect = async (pct: number) => {\n if (isConfiguredAmountFixed) return;\n if (!toToken) return;\n\n syncingIntentSourcesRef.current = false;\n setTxError(null);\n setSwapQuoteIssue(null);\n const runId = ++maxPercentRunRef.current;\n lastInputMethodRef.current =\n pct === 20\n ? \"percent_20\"\n : pct === 25\n ? \"percent_25\"\n : pct === 50\n ? \"percent_50\"\n : pct === 75\n ? \"percent_75\"\n : \"percent_max\";\n\n if (pct !== 100) {\n const nextAmount = getExactOutPercentAmountFromBalance(\n toToken,\n pct,\n depositAmountMode === \"usd\"\n );\n\n if (nextAmount) {\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setDepositAmountMode(nextAmount.mode);\n handleDepositAmountChange(nextAmount.amount);\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setTxError(\"Unable to calculate this percentage for the deposit asset.\");\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n const maxAmount = getExactOutPercentAmountFromBalance(\n toToken,\n 100,\n depositAmountMode === \"usd\"\n );\n if (runId !== maxPercentRunRef.current) return;\n if (!maxAmount) {\n setTxError(\"No depositable amount is available for this deposit.\");\n return;\n }\n\n setDepositAmountMode(maxAmount.mode);\n handleDepositAmountChange(maxAmount.amount);\n };\n\n const handleSendPercentSelect = async (pct: number) => {\n if (isConfiguredAmountFixed) return;\n if (!toToken) return;\n\n syncingIntentSourcesRef.current = false;\n setTxError(null);\n setSwapQuoteIssue(null);\n const runId = ++maxPercentRunRef.current;\n lastInputMethodRef.current =\n pct === 20\n ? \"percent_20\"\n : pct === 25\n ? \"percent_25\"\n : pct === 50\n ? \"percent_50\"\n : pct === 75\n ? \"percent_75\"\n : \"percent_max\";\n\n if (pct !== 100) {\n const nextAmount = getExactOutPercentAmountFromBalance(\n toToken,\n pct,\n false\n );\n\n if (nextAmount) {\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleSendAmountChange(nextAmount.amount);\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setTxError(\"Unable to calculate this percentage for the send asset.\");\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n const maxAmount = getExactOutPercentAmountFromBalance(toToken, 100, false);\n if (runId !== maxPercentRunRef.current) return;\n if (!maxAmount) {\n setTxError(\"No transferable amount is available for this asset.\");\n return;\n }\n\n handleSendAmountChange(maxAmount.amount);\n };\n\n // ---------------------------------------------------------------------------\n // Render\n // ---------------------------------------------------------------------------\n const insufficientSourceIssue =\n swapQuoteIssue?.type === \"insufficientSources\" ? swapQuoteIssue : null;\n const configuredAmountIssue = buildConfiguredAmountIssue();\n const blockingQuoteIssue =\n insufficientSourceIssue ?? receiveAmountIssue ?? configuredAmountIssue;\n const isExactOutPaymentFlow =\n activeMode === \"deposit\" || activeMode === \"send\";\n const hasCurrentRunnableIntent = hasCurrentQuoteIntent;\n const hasIntentSources = Boolean((intentData?.sources ?? []).length > 0);\n const hasCurrentExactOutPaymentIntent =\n hasCurrentRunnableIntent &&\n (hasIntentSources ||\n (isExactOutPaymentFlow && Boolean(intentData?.destination)));\n const isExactOutRouteLoading =\n isExactOutPaymentFlow &&\n swapStep === \"idle\" &&\n swapType === \"exactOut\" &&\n Boolean(\n toToken && (receiveMaxCalculating || (amount && Number(amount) > 0))\n ) &&\n !blockingQuoteIssue &&\n !hasCurrentExactOutPaymentIntent &&\n (quoteRefreshing || intentLoading || receiveMaxCalculating);\n const isQuoteUnavailableForAutoSourceFlow =\n isExactOutPaymentFlow &&\n Boolean(hasPositiveDecimalInput(amount) && toToken) &&\n !quoteRefreshing &&\n !receiveMaxCalculating &&\n !intentLoading &&\n !blockingQuoteIssue &&\n !hasCurrentExactOutPaymentIntent;\n const hasPositiveRootAmount = hasPositiveDecimalInput(amount);\n const hasReadySwapQuoteInput = hasReadyExactInSwapInput(fromTokens, toToken);\n const needsWalletConnection = !ownerAddress || !nexusSDK;\n const isExactOutPaymentQuotePending =\n isExactOutPaymentFlow && (quoteRefreshing || intentLoading);\n const walletConnectBusy =\n walletActionPending ||\n nexusLoading ||\n isWalletConnectPending ||\n walletStatus === \"connecting\";\n const hasConnectWalletHandler = Boolean(\n onConnectClick || onConnectWallet || connectors.length > 0\n );\n const walletCtaLabel = hasConnectWalletHandler\n ? walletConnectBusy\n ? \"Connecting...\"\n : \"Connect Wallet\"\n : \"Connect your wallet to proceed\";\n const isSwapCtaDisabled = needsWalletConnection\n ? !hasConnectWalletHandler || walletConnectBusy\n : !hasReadySwapQuoteInput ||\n receiveMaxCalculating ||\n quoteRefreshing ||\n Boolean(blockingQuoteIssue);\n const isDepositCtaDisabled = needsWalletConnection\n ? !hasConnectWalletHandler || walletConnectBusy\n : !hasPositiveRootAmount ||\n !toToken ||\n receiveMaxCalculating ||\n isExactOutPaymentQuotePending ||\n (!hasCurrentExactOutPaymentIntent &&\n isQuoteUnavailableForAutoSourceFlow) ||\n Boolean(blockingQuoteIssue);\n const sendNeedsRecipient = activeMode === \"send\" && !recipientAddress;\n const isSendCtaDisabled = needsWalletConnection\n ? !hasConnectWalletHandler || walletConnectBusy\n : !hasPositiveRootAmount ||\n !toToken ||\n hasSameOwnerSendRecipient ||\n receiveMaxCalculating ||\n (!sendNeedsRecipient &&\n (isExactOutPaymentQuotePending ||\n (!hasCurrentExactOutPaymentIntent &&\n isQuoteUnavailableForAutoSourceFlow))) ||\n Boolean(blockingQuoteIssue);\n const quoteCtaLabel = (fallback: string) => {\n if (needsWalletConnection) return walletCtaLabel;\n if (insufficientSourceIssue) return \"Insufficient balance\";\n if (receiveAmountIssue) return receiveAmountIssue.ctaLabel;\n if (configuredAmountIssue) return configuredAmountIssue.ctaLabel;\n if (receiveMaxCalculating) return \"Calculating...\";\n if (\n isExactOutPaymentQuotePending ||\n (!hasCurrentExactOutPaymentIntent && (quoteRefreshing || intentLoading))\n ) {\n return \"Fetching quotes...\";\n }\n if (isQuoteUnavailableForAutoSourceFlow) return \"Quote unavailable\";\n if (!hasPositiveRootAmount) return \"Enter amount\";\n return fallback;\n };\n const sendCtaLabel = (() => {\n if (needsWalletConnection) return walletCtaLabel;\n if (insufficientSourceIssue) return \"Insufficient balance\";\n if (receiveAmountIssue) return receiveAmountIssue.ctaLabel;\n if (configuredAmountIssue) return configuredAmountIssue.ctaLabel;\n if (!hasPositiveRootAmount) return \"Enter amount\";\n if (!toToken) return \"Select token\";\n if (hasSameOwnerSendRecipient) return \"Change recipient\";\n if (sendNeedsRecipient) return \"Add recipient\";\n return quoteCtaLabel(\"Review send\");\n })();\n const previewIntentSourceUsdNumber = (intentData?.sources ?? []).reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0)\n );\n const previewSourceUsdNumber = previewIntentSourceUsdNumber.gt(0)\n ? previewIntentSourceUsdNumber\n : fromTokens.length > 0\n ? fromTokens.reduce(\n (sum, token) =>\n sum.plus(\n getTokenUsdValue(\n token,\n swapType === \"exactIn\" && fromTokens.length === 1\n ? amount\n : undefined\n )\n ),\n new Decimal(0)\n )\n : undefined;\n const previewExactOutDestinationAmount =\n activeMode === \"deposit\"\n ? depositTokenAmountForQuote\n : activeMode === \"send\"\n ? parseFiatNumber(amount)\n : undefined;\n const previewExactOutDestinationUsdNumber =\n activeMode === \"deposit\"\n ? depositUsdDecimal\n : activeMode === \"send\" && amount && toToken\n ? getTokenUsdValue(\n {\n ...toToken,\n userAmount: amount,\n userAmountMode: \"token\",\n },\n amount\n )\n : undefined;\n const previewDestinationUsdNumber =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n previewExactOutDestinationUsdNumber?.gt(0)\n ? previewExactOutDestinationUsdNumber\n : parseFiatNumber((intentData?.destination as any)?.value);\n const previewDestinationAmount =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n previewExactOutDestinationAmount?.gt(0)\n ? previewExactOutDestinationAmount\n .toDecimalPlaces(toToken?.decimals ?? 18, Decimal.ROUND_DOWN)\n .toFixed()\n : intentToAmount;\n const previewFromAmountUsd =\n previewSourceUsdNumber && previewSourceUsdNumber.gt(0)\n ? previewSourceUsdNumber.toDecimalPlaces(6).toFixed()\n : undefined;\n const previewToAmountUsd =\n previewDestinationUsdNumber && previewDestinationUsdNumber.gt(0)\n ? previewDestinationUsdNumber.toDecimalPlaces(6).toFixed()\n : undefined;\n const previewDestinationGasFeeUsd = (() => {\n const value = getIntentDestinationGasUsdValue(intentData);\n return value && value.gt(0)\n ? value.toDecimalPlaces(6, Decimal.ROUND_DOWN).toFixed()\n : undefined;\n })();\n const predictiveExactInQuote =\n predictiveQuote?.mode === \"exactIn\" &&\n predictiveQuote.key === getPredictiveQuoteCacheKey(\"swap\", \"exactIn\")\n ? predictiveQuote\n : null;\n const predictiveExactOutQuote =\n predictiveQuote?.mode === \"exactOut\" &&\n predictiveQuote.key === getPredictiveQuoteCacheKey(activeMode, \"exactOut\")\n ? predictiveQuote\n : null;\n const shouldUseCurrentExactOutIntentSources =\n isExactOutPaymentFlow && hasCurrentQuoteIntent && hasIntentSources;\n const currentExactOutIntentSourceTokens =\n shouldUseCurrentExactOutIntentSources\n ? sortSwapTokensByUsdDesc(\n (intentData?.sources ?? [])\n .map(buildIntentSourceToken)\n .filter(hasPositiveSourceSpend)\n )\n : [];\n const resolvedToToken =\n toToken ??\n (activeMode === \"deposit\" && selectedOpportunity\n ? toTokenFromOpportunity(selectedOpportunity)\n : undefined);\n const enrichedResolvedToToken = enrichTokenOptionLogo(resolvedToToken);\n const toTokenWithFetchedBalance =\n enrichedResolvedToToken && destinationBalance\n ? { ...enrichedResolvedToToken, balance: destinationBalance }\n : enrichedResolvedToToken;\n const idleReceiveQuoteAmount =\n activeMode === \"swap\" && swapType === \"exactIn\"\n ? (intentToAmount ?? predictiveExactInQuote?.toAmount)\n : undefined;\n const idleReceiveQuoteUsd =\n activeMode === \"swap\" && swapType === \"exactIn\"\n ? (previewToAmountUsd ?? predictiveExactInQuote?.toUsd)\n : previewToAmountUsd;\n const exactOutDestinationCoverage = getExactOutDestinationBalanceCoverage({\n requestedAmount: previewExactOutDestinationAmount,\n requestedUsd: previewExactOutDestinationUsdNumber,\n producedAmount: hasIntentSources\n ? parseFiatNumber(intentData?.destination?.amount)\n : undefined,\n producedUsd: hasIntentSources\n ? parseFiatNumber(intentData?.destination?.value)\n : undefined,\n token: toTokenWithFetchedBalance,\n });\n const destinationBalanceDisplayToken = buildDestinationBalanceDisplayToken(\n exactOutDestinationCoverage,\n toTokenWithFetchedBalance\n );\n const shouldShowPredictiveExactOutDisplay =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (quoteRefreshing || intentLoading) &&\n !hasIntentSources &&\n Boolean(\n predictiveExactOutQuote &&\n ((predictiveExactOutQuote.sources?.length ?? 0) > 0 ||\n destinationBalanceDisplayToken)\n );\n const baseDisplayFromTokens = shouldUseCurrentExactOutIntentSources\n ? currentExactOutIntentSourceTokens\n : shouldShowPredictiveExactOutDisplay\n ? (predictiveExactOutQuote?.sources ?? fromTokens)\n : fromTokens;\n const displayFromTokens = (() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n return baseDisplayFromTokens;\n }\n if (!destinationBalanceDisplayToken) {\n return mergeDisplaySourceTokens(\n sortDisplaySourcesByBalanceUsdDesc(baseDisplayFromTokens)\n );\n }\n\n const destinationKey = getTokenSelectionKey(destinationBalanceDisplayToken);\n let replacedEmptyDestinationToken = false;\n const tokens = baseDisplayFromTokens.map((token) => {\n const isDestinationToken = getTokenSelectionKey(token) === destinationKey;\n if (\n isDestinationToken &&\n !hasPositiveDecimalInput(token.userAmount) &&\n !hasPositiveDecimalInput(token.userAmountUsd)\n ) {\n replacedEmptyDestinationToken = true;\n return destinationBalanceDisplayToken;\n }\n return token;\n });\n\n const displayTokens = replacedEmptyDestinationToken\n ? tokens\n : [...tokens, destinationBalanceDisplayToken];\n return mergeDisplaySourceTokens(\n sortDisplaySourcesByBalanceUsdDesc(displayTokens)\n );\n })();\n const displayExactOutRouteLoading =\n isExactOutRouteLoading && !shouldShowPredictiveExactOutDisplay;\n const isSourcePickerRefreshDisabled =\n quoteRefreshing || intentLoading || previewQuoteRefreshing;\n const isIntentSourceUiActionable =\n activeMode === \"deposit\"\n ? !isDepositCtaDisabled\n : activeMode === \"send\"\n ? !isSendCtaDisabled\n : !isSwapCtaDisabled;\n\n useEffect(() => {\n if (!intentFetchTiming?.endTimeMs) {\n return;\n }\n if (latestIntentFetchRunIdRef.current !== intentFetchTiming.runId) {\n return;\n }\n if (intentUiRenderLoggedRunIdRef.current === intentFetchTiming.runId) {\n return;\n }\n const hasRenderedSources =\n displayFromTokens.length > 0 ||\n hasIntentSources ||\n Boolean(intentData?.destination);\n if (!hasRenderedSources) return;\n if (\n isSwapBalancePending ||\n isSourcePickerRefreshDisabled ||\n quoteRefreshing ||\n intentLoading ||\n previewQuoteRefreshing ||\n receiveMaxCalculating ||\n displayExactOutRouteLoading ||\n !isIntentSourceUiActionable\n ) {\n return;\n }\n\n const renderTimeMs = Date.now();\n const renderDurationMs = renderTimeMs - intentFetchTiming.startTimeMs;\n const fetchDurationMs =\n intentFetchTiming.fetchDurationMs ??\n intentFetchTiming.endTimeMs - intentFetchTiming.startTimeMs;\n const differenceMs = renderDurationMs - fetchDurationMs;\n\n console.log(\"[NexusWidget] intent sources UI render complete\", {\n runId: intentFetchTiming.runId,\n walletAddress: intentFetchTiming.walletAddress,\n operation: intentFetchTiming.operation,\n quoteInputKey: intentFetchTiming.quoteInputKey,\n sourceCount: displayFromTokens.length,\n dateNow: renderTimeMs,\n date: new Date(renderTimeMs),\n totalRenderTimeSeconds: renderDurationMs / 1000,\n elapsedMs: renderDurationMs,\n });\n console.log(\"[NexusWidget] intent render/fetch difference\", {\n runId: intentFetchTiming.runId,\n walletAddress: intentFetchTiming.walletAddress,\n operation: intentFetchTiming.operation,\n quoteInputKey: intentFetchTiming.quoteInputKey,\n differenceMs,\n dateNow: renderTimeMs,\n date: new Date(renderTimeMs),\n });\n intentUiRenderLoggedRunIdRef.current = intentFetchTiming.runId;\n }, [\n displayExactOutRouteLoading,\n displayFromTokens.length,\n hasIntentSources,\n intentLoading,\n intentData?.destination,\n intentFetchTiming,\n isDepositCtaDisabled,\n isIntentSourceUiActionable,\n isSendCtaDisabled,\n isSourcePickerRefreshDisabled,\n isSwapCtaDisabled,\n isSwapBalancePending,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n ]);\n\n const totalSwapBalanceUsd = getActiveTotalBalanceUsd()\n .toDecimalPlaces(2)\n .toFixed();\n const requestedOutputUsd =\n amount && toToken\n ? getTokenUsdValue(\n {\n ...toToken,\n userAmount: amount,\n userAmountMode: \"token\",\n },\n amount\n )\n : new Decimal(0);\n const sendAmountUsd = requestedOutputUsd.toNumber();\n const exactOutRequiredUsdAmount = (() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return undefined;\n\n const issueMissingUsd = parseFiatNumber(insufficientSourceIssue?.missingUsd);\n if (issueMissingUsd && issueMissingUsd.gt(0)) {\n return getExactOutAvailableSourceUsd().plus(issueMissingUsd);\n }\n\n const requiredFundingUsd = getExactOutRequiredFundingUsd();\n if (requiredFundingUsd?.gt(0)) return requiredFundingUsd;\n\n if (requestedOutputUsd.gt(0)) return requestedOutputUsd;\n\n return undefined;\n })();\n const exactOutRequiredUsdDisplay = exactOutRequiredUsdAmount\n ?.toDecimalPlaces(2)\n .toFixed();\n const assetSelectorRequiredUsdDisplay =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (swapStep === \"choose-swap-asset\" ||\n closingDrawerStep === \"choose-swap-asset\")\n ? (sourceSelectionRequiredUsdDisplay ?? exactOutRequiredUsdDisplay)\n : exactOutRequiredUsdDisplay;\n const captureSourceSelectionRequiredUsd = () => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n setSourceSelectionRequiredUsdDisplay(exactOutRequiredUsdDisplay);\n };\n const isIdleSwapQuoteLoading =\n activeMode === \"swap\" &&\n swapStep === \"idle\" &&\n (quoteRefreshing || intentLoading);\n const isReceiveAmountLoading =\n receiveMaxCalculating ||\n (isIdleSwapQuoteLoading &&\n swapType === \"exactIn\" &&\n !idleReceiveQuoteAmount);\n const isReceiveUsdLoading =\n receiveMaxCalculating ||\n (isIdleSwapQuoteLoading && swapType === \"exactIn\" && !idleReceiveQuoteUsd);\n const hasQuoteRefreshCountdown =\n (activeMode === \"swap\" ||\n activeMode === \"deposit\" ||\n activeMode === \"send\") &&\n hasCurrentQuoteIntent &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n const isRecipientDrawerClosing = closingDrawerStep === \"enter-recipient\";\n const isSwapAssetDrawerClosing = closingDrawerStep === \"choose-swap-asset\";\n const isReceiveAssetDrawerClosing =\n closingDrawerStep === \"choose-receive-asset\";\n const isReceiveAssetPickerLocked =\n activeMode === \"deposit\"\n ? configuredDepositOptions.length <= 1\n : (config.allowedDestinationPairs?.length ?? 0) === 1;\n const hideDestinationTokenDropdownIcon =\n activeMode === \"deposit\"\n ? configuredDepositOptions.length === 1\n : (config.allowedDestinationPairs?.length ?? 0) === 1;\n const isTokenAssetDrawerActive =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n closingDrawerStep === \"choose-swap-asset\" ||\n closingDrawerStep === \"choose-receive-asset\";\n const configuredReceiveTokenCount =\n activeMode === \"deposit\" || activeMode === \"send\"\n ? Math.max(\n configuredDestinationTokenOptions.length,\n config.allowedDestinationPairs?.length ?? 0\n )\n : 0;\n const isConfiguredReceiveAssetDrawerActive =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (swapStep === \"choose-receive-asset\" ||\n closingDrawerStep === \"choose-receive-asset\") &&\n Boolean(config.allowedDestinationPairs?.length);\n const tokenAssetDrawerMinRootContentHeight =\n isConfiguredReceiveAssetDrawerActive\n ? getConfiguredReceiveSelectorRootHeight(configuredReceiveTokenCount)\n : isTokenAssetDrawerActive\n ? TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT\n : 0;\n const isDrawerOverlayActive =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n swapStep === \"enter-recipient\" ||\n closingDrawerStep !== null;\n const isPreviewIntentScreen = swapStep === \"preview-intent\";\n const displayedRootContentHeight =\n hasMeasuredRootContent && rootContentHeight\n ? Math.max(\n rootContentHeight,\n tokenAssetDrawerMinRootContentHeight\n )\n : null;\n\n const widgetContent = (\n {\n const transitions = [];\n if (hasMeasuredRootContent && shouldAnimateRootHeight) {\n transitions.push(`height ${ROOT_HEIGHT_TRANSITION_MS}ms ease-out`);\n }\n return transitions.join(\", \");\n })(),\n willChange: \"height\",\n width: \"450px\",\n maxWidth: \"100%\",\n minWidth: \"320px\",\n WebkitFontSmoothing: \"antialiased\",\n MozOsxFontSmoothing: \"grayscale\",\n }}\n >\n \n \n \n {canGoBack && (\n \n \n \n )}\n \n {getTitle()}\n \n\n {/* Sub-screen asset counts */}\n {!isTitleCentered() &&\n activeMode === \"swap\" &&\n swapStep === \"choose-swap-asset\" &&\n swapType === \"exactIn\" && (\n \n {fromTokens.length} asset(s) selected\n \n )}\n \n\n {/* Right side icons */}\n \n {hasQuoteRefreshCountdown && (\n \n )}\n {showHistoryButton && (\n setSwapStep(\"history\")}\n style={{\n alignItems: \"center\",\n backgroundColor: theme.primitives.iconButton.backgroundColor,\n borderColor: theme.primitives.iconButton.borderColor,\n borderRadius: theme.radius.iconButton,\n borderStyle: \"solid\",\n borderWidth: \"1px\",\n boxShadow: theme.primitives.iconButton.boxShadow,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n height: \"28px\",\n justifyContent: \"center\",\n width: \"28px\",\n cursor: \"pointer\",\n padding: 0,\n }}\n >\n \n \n \n \n \n \n )}\n {showCloseButton && (\n \n \n \n \n \n )}\n \n \n\n {/* ------------------------------------------------------------------ */}\n {/* Main content area */}\n {/* ------------------------------------------------------------------ */}\n \n {/* =============================================================== */}\n {/* SHARED SUB-SCREENS (non-drawer panels) */}\n {/* =============================================================== */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep !== \"idle\" &&\n swapStep !== \"choose-swap-asset\" &&\n swapStep !== \"choose-receive-asset\" &&\n swapStep !== \"enter-recipient\" && (\n <>\n {/* Panel: preview. */}\n {swapStep === \"preview-intent\" && (\n \n {\n clearPendingSwapIntent();\n setSwapStep(\"idle\");\n }}\n onTransitionChange={setIsPreviewTransitioning}\n opportunity={selectedOpportunity}\n recipientAddress={transferRecipientAddress}\n steps={steps}\n supportedTokenAssets={supportedChainsAndTokens}\n swapBalances={swapBalance}\n swapType={swapType}\n toAmount={previewDestinationAmount}\n toAmountTokens={\n previewDestinationAmount\n ? `${previewDestinationAmount}`\n : undefined\n }\n toAmountUsd={previewToAmountUsd}\n toToken={toTokenWithFetchedBalance}\n totalFeeUsd={intentFeeUsd}\n />\n \n )}\n\n {swapStep === \"progress\" && (\n \n )}\n\n {(swapStep === \"success\" || swapStep === \"failed\") &&\n currentSwapEntry && (\n
\n \n
\n )}\n \n )}\n\n {/* =============================================================== */}\n {/* HISTORY SCREEN */}\n {/* =============================================================== */}\n {swapStep === \"history\" && (\n \n )}\n\n {/* =============================================================== */}\n {/* SWAP IDLE SCREEN */}\n {/* =============================================================== */}\n {activeMode === \"swap\" &&\n [\n \"idle\",\n \"choose-swap-asset\",\n \"choose-receive-asset\",\n \"enter-recipient\",\n ].includes(swapStep) && (\n <>\n {\n handleSwapAmountChange(val, panel);\n }}\n onOpenDestPicker={() =>\n openDrawerStep(\"choose-receive-asset\")\n }\n onOpenRecipientPicker={handleOpenRecipientEditor}\n onOpenSourcePicker={(index) => {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n setEditingAssetIndex(index ?? null);\n beginSourcePickerEdit();\n openDrawerStep(\"choose-swap-asset\");\n }}\n onUpdateTokens={handleSwapTokensUpdate}\n receiveQuoteAmount={\n swapType === \"exactIn\" ? idleReceiveQuoteAmount : undefined\n }\n receiveQuoteUsd={idleReceiveQuoteUsd}\n recipientAddress={effectiveRecipientAddress}\n sourceRouteMessage={insufficientSourceIssue?.message}\n sourceRouteStatus={\n insufficientSourceIssue\n ? \"insufficient\"\n : isExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n swapType={swapType}\n toToken={toTokenWithFetchedBalance}\n totalBalance={totalSwapBalanceUsd}\n usdValue={amount && usdValue > 0 ? usdValue.toFixed(2) : \"\"}\n />\n\n {receiveAmountIssue && (\n \n )}\n\n {configuredAmountIssue && (\n \n )}\n\n {txError && !blockingQuoteIssue && (\n \n )}\n\n {/* CTA Button */}\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n void handleEnterPreview();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: blockingQuoteIssue\n ? \"#FCEEED\"\n : isSwapCtaDisabled\n ? theme.colors.surfaceCool\n : primaryButtonBackground,\n border: blockingQuoteIssue ? \"1px solid #F7C4C1\" : \"none\",\n borderRadius: theme.radius.primaryButton,\n boxShadow:\n blockingQuoteIssue || isSwapCtaDisabled\n ? \"none\"\n : theme.shadows.primaryButton,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"7px\",\n height: \"42px\",\n justifyContent: \"center\",\n marginTop: \"8px\",\n paddingInline: \"16px\",\n cursor: isSwapCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {blockingQuoteIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n quoteRefreshing ||\n receiveMaxCalculating ? (\n\t \n ) : null}\n \n {needsWalletConnection\n ? walletCtaLabel\n : fromTokens.length === 0\n ? \"Add Assets to Bridge\"\n : quoteCtaLabel(\"Review swap\")}\n \n \n \n \n )}\n\n {/* =============================================================== */}\n {/* DEPOSIT MODE LAYOUT */}\n {/* =============================================================== */}\n {activeMode === \"deposit\" &&\n [\n \"idle\",\n \"choose-swap-asset\",\n \"choose-receive-asset\",\n \"enter-recipient\",\n ].includes(swapStep) && (\n <>\n {swapStep === \"idle\" && depositFundingStep === \"method\" && (\n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n setDepositFundingStep(\"onramp\");\n }}\n onSelectWallet={() => setDepositFundingStep(\"wallet\")}\n primaryButtonForeground={primaryButtonForeground}\n totalBalance={totalSwapBalanceUsd}\n />\n )}\n\n {swapStep === \"idle\" && depositFundingStep === \"onramp\" && (\n {\n setTxError(message);\n onError?.(message);\n }}\n onSelectDestinationToken={(token) => {\n const tokenChanged = !isSameTokenSelection(toToken, token);\n if (tokenChanged) {\n onReceiveAssetChange?.({\n chainId: token.chainId,\n chainName: token.chainName,\n contractAddress: token.contractAddress,\n symbol: token.symbol,\n });\n }\n const nextDeposit = getDepositForTokenSelection(\n configuredDepositOptions,\n token\n );\n if (nextDeposit) {\n setSelectedOpportunity(nextDeposit);\n }\n setToToken(token);\n }}\n onSessionStateChange={setDepositOnrampSessionState}\n ownerAddress={ownerAddress}\n opportunity={selectedOpportunity}\n primaryButtonForeground={primaryButtonForeground}\n toToken={toTokenWithFetchedBalance}\n />\n )}\n\n {selectedOpportunity && depositFundingStep === \"wallet\" && (\n <>\n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n captureSourceSelectionRequiredUsd();\n beginSourcePickerEdit();\n openDrawerStep(\"choose-swap-asset\");\n }}\n onOpenTokenPicker={() =>\n openDrawerStep(\"choose-receive-asset\")\n }\n onSetPercent={handleDepositPercentSelect}\n routeMessage={insufficientSourceIssue?.message}\n routeStatus={\n insufficientSourceIssue\n ? \"insufficient\"\n : displayExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n showAutoBadge={!sourceSelectionTouched}\n isTokenPickerDisabled={isReceiveAssetPickerLocked}\n hideDestinationTokenDropdownIcon={\n hideDestinationTokenDropdownIcon\n }\n tokenValue={depositTokenDisplay}\n toToken={toTokenWithFetchedBalance}\n totalBalance={totalSwapBalanceUsd}\n usdValue={depositUsdDisplay}\n />\n\n {receiveAmountIssue && (\n \n )}\n\n {configuredAmountIssue && (\n \n )}\n\n {txError && !blockingQuoteIssue && (\n \n )}\n\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n void handleEnterPreview();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: blockingQuoteIssue\n ? \"#FCEEED\"\n : isDepositCtaDisabled\n ? theme.colors.surfaceCool\n : primaryButtonBackground,\n border: blockingQuoteIssue\n ? \"1px solid #F7C4C1\"\n : \"none\",\n borderRadius: blockingQuoteIssue\n ? \"4px\"\n : theme.radius.primaryButton,\n boxShadow:\n blockingQuoteIssue || isDepositCtaDisabled\n ? \"none\"\n : theme.shadows.primaryButton,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"7px\",\n height: \"40px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n cursor: isDepositCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {blockingQuoteIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n isExactOutPaymentQuotePending ||\n receiveMaxCalculating ? (\n\t \n ) : null}\n \n {quoteCtaLabel(\"Review deposit\")}\n \n \n \n \n )}\n \n )}\n\n {/* =============================================================== */}\n {/* SEND MODE — recipient first, then amount, then asset */}\n {/* =============================================================== */}\n {activeMode === \"send\" &&\n [\n \"idle\",\n \"choose-swap-asset\",\n \"choose-receive-asset\",\n \"enter-recipient\",\n ].includes(swapStep) && (\n <>\n \n openDrawerStep(\"choose-receive-asset\")\n }\n isRecipientLocked={isConfiguredRecipientLocked}\n onOpenRecipientPicker={\n isConfiguredRecipientLocked\n ? () => undefined\n : handleOpenRecipientEditor\n }\n onOpenSourcePicker={() => {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n setEditingAssetIndex(null);\n captureSourceSelectionRequiredUsd();\n beginSourcePickerEdit();\n openDrawerStep(\"choose-swap-asset\");\n }}\n onSetPercent={handleSendPercentSelect}\n recipientAddress={recipientAddress || \"\"}\n routeMessage={insufficientSourceIssue?.message}\n routeStatus={\n insufficientSourceIssue\n ? \"insufficient\"\n : displayExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n showAutoBadge={!sourceSelectionTouched}\n toToken={toTokenWithFetchedBalance}\n totalBalance={totalSwapBalanceUsd}\n usdValue={\n amount && sendAmountUsd > 0 ? sendAmountUsd.toFixed(2) : \"\"\n }\n />\n\n {receiveAmountIssue && (\n \n )}\n\n {configuredAmountIssue && (\n \n )}\n\n {txError && !blockingQuoteIssue && (\n \n )}\n\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n if (sendNeedsRecipient) {\n handleOpenRecipientEditor();\n return;\n }\n void handleEnterPreview();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: blockingQuoteIssue\n ? \"#FCEEED\"\n : isSendCtaDisabled\n ? theme.colors.surfaceCool\n : primaryButtonBackground,\n border: blockingQuoteIssue ? \"1px solid #F7C4C1\" : \"none\",\n borderRadius: blockingQuoteIssue\n ? \"4px\"\n : theme.radius.primaryButton,\n boxShadow:\n blockingQuoteIssue || isSendCtaDisabled\n ? \"none\"\n : theme.shadows.primaryButton,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"7px\",\n height: \"40px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n cursor: isSendCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {blockingQuoteIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n (!sendNeedsRecipient &&\n (isExactOutPaymentQuotePending ||\n receiveMaxCalculating)) ? (\n\t \n ) : null}\n \n {sendCtaLabel}\n \n \n \n \n )}\n \n \n\n {/* ================================================================== */}\n {/* DRAWER PANELS — rendered as direct children of root widget */}\n {/* so they overlay the main page as bottom drawers */}\n {/* ================================================================== */}\n\n {/* Drawer: enter-recipient */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep === \"enter-recipient\" && (\n \n {\n setTxError(null);\n closeDrawerToIdle();\n }}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n backgroundColor: \"rgba(0,0,0,0.35)\",\n pointerEvents: \"auto\",\n opacity: isRecipientDrawerClosing ? 0 : 1,\n transition: `opacity ${DRAWER_CLOSE_MS}ms ease`,\n }}\n />\n \n \n \n \n \n {\n setTxError(null);\n closeDrawerToIdle();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: theme.colors.surface,\n border: `1px solid ${theme.colors.border}`,\n borderRadius: \"8px\",\n cursor: \"pointer\",\n display: \"flex\",\n flexShrink: 0,\n height: \"32px\",\n justifyContent: \"center\",\n padding: 0,\n width: \"32px\",\n }}\n type=\"button\"\n >\n \n \n \n Recipient\n \n \n \n \n \n Wallet Address\n \n {activeMode === \"swap\" && defaultRecipientAddress && (\n \n Reset to default\n \n )}\n \n {\n setRecipientAddress(next);\n if (txError) setTxError(null);\n }}\n onClear={() => setRecipientAddress(\"\")}\n placeholder=\"Wallet address\"\n value={recipientAddress}\n />\n {txError && (\n \n {txError}\n \n )}\n {activeMode === \"send\" && (\n \n Recipient must be different from the connected wallet.\n \n )}\n \n Save\n \n \n \n )}\n\n {/* Drawer: choose-swap-asset */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep === \"choose-swap-asset\" && (\n \n \n \n {\n if (activeMode === \"swap\") {\n const next = [...fromTokens];\n const targetIndex =\n editingAssetIndex !== null &&\n editingAssetIndex < next.length\n ? editingAssetIndex\n : null;\n const existingToken =\n targetIndex !== null ? next[targetIndex] : undefined;\n const tokenChanged = !isSameTokenSelection(\n existingToken,\n token\n );\n const preservedAmount = tokenChanged\n ? \"\"\n : existingToken?.userAmount ||\n (targetIndex === 0 ? amount : \"\");\n const newToken = {\n ...token,\n userAmount: preservedAmount,\n };\n\n if (targetIndex !== null) {\n next[targetIndex] = newToken;\n } else {\n next.push(newToken);\n }\n\n if (tokenChanged) {\n clearPendingSwapIntent();\n setAmount(getSourceAmountInput(next));\n }\n if (swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n setFromTokens(next);\n closeDrawerToIdle();\n } else if (\n activeMode === \"deposit\" ||\n activeMode === \"send\"\n ) {\n handleSourcePickerDraftSelectionChange([\n { ...token, userAmount: \"\" },\n ]);\n }\n }}\n onSelectionChange={\n activeMode === \"deposit\" || activeMode === \"send\"\n ? handleSourcePickerDraftSelectionChange\n : undefined\n }\n onToggle={(token) => {\n const isDepositOrSendSourcePicker =\n activeMode === \"deposit\" || activeMode === \"send\";\n const prevTokens = isDepositOrSendSourcePicker\n ? sourcePickerSelectedTokens\n : fromTokens;\n const isSameSelection = (\n a: SwapTokenOption,\n b: SwapTokenOption\n ) => {\n if (a.isUnified || b.isUnified) {\n return Boolean(\n a.isUnified &&\n b.isUnified &&\n a.unifiedSymbol === b.unifiedSymbol\n );\n }\n return (\n a.contractAddress.toLowerCase() ===\n b.contractAddress.toLowerCase() && a.chainId === b.chainId\n );\n };\n const sourceTokens = token.sourceTokens ?? [];\n const isSameUnifiedGroup = (item: SwapTokenOption) =>\n Boolean(\n item.isUnified &&\n token.isUnified &&\n item.unifiedSymbol === token.unifiedSymbol\n );\n const withDefaultAmount = (item: SwapTokenOption) => ({\n ...item,\n userAmount:\n activeMode === \"swap\" && prevTokens.length === 0\n ? amount\n : \"\",\n });\n const getNextTokens = () => {\n if (\n isDepositOrSendSourcePicker &&\n token.isUnified &&\n sourceTokens.length > 0\n ) {\n const hasUnifiedSelection =\n prevTokens.some(isSameUnifiedGroup);\n const areAllChildrenSelected = sourceTokens.every(\n (source) =>\n prevTokens.some((item) =>\n isSameSelection(item, source)\n )\n );\n const withoutGroup = prevTokens.filter(\n (item) =>\n !isSameUnifiedGroup(item) &&\n !sourceTokens.some((source) =>\n isSameSelection(item, source)\n )\n );\n\n if (hasUnifiedSelection || areAllChildrenSelected) {\n return withoutGroup;\n }\n\n return [\n ...withoutGroup,\n ...sourceTokens.map((source) =>\n withDefaultAmount(source)\n ),\n ];\n }\n\n if (isDepositOrSendSourcePicker && !token.isUnified) {\n const unifiedSelection = prevTokens.find(\n (item) =>\n item.isUnified &&\n item.sourceTokens?.some((source) =>\n isSameSelection(source, token)\n )\n );\n\n if (unifiedSelection?.sourceTokens?.length) {\n const withoutUnified = prevTokens.filter(\n (item) => !isSameSelection(item, unifiedSelection)\n );\n return [\n ...withoutUnified,\n ...unifiedSelection.sourceTokens\n .filter((source) => !isSameSelection(source, token))\n .map((source) => withDefaultAmount(source)),\n ];\n }\n }\n\n const exists = prevTokens.find((item) =>\n isSameSelection(item, token)\n );\n if (exists) {\n return prevTokens.filter(\n (item) => !isSameSelection(item, token)\n );\n }\n const tokenSourceKeys = new Set(\n (token.sourceTokens ?? []).map(\n (source) =>\n `${source.chainId}-${source.contractAddress.toLowerCase()}`\n )\n );\n const next = prevTokens.filter((existing) => {\n if (\n token.isUnified &&\n tokenSourceKeys.has(\n `${existing.chainId}-${existing.contractAddress.toLowerCase()}`\n )\n ) {\n return false;\n }\n if (\n existing.isUnified &&\n existing.sourceTokens?.some(\n (source) =>\n source.chainId === token.chainId &&\n source.contractAddress.toLowerCase() ===\n token.contractAddress.toLowerCase()\n )\n ) {\n return false;\n }\n return true;\n });\n return [...next, withDefaultAmount(token)];\n };\n const nextTokens = getNextTokens();\n\n if (isDepositOrSendSourcePicker) {\n handleSourcePickerDraftSelectionChange(nextTokens);\n } else {\n clearPendingSwapIntent();\n setFromTokens(nextTokens);\n }\n }}\n preserveSelectedBelowMinimum={false}\n requiredUsd={assetSelectorRequiredUsdDisplay}\n selectedTokens={sourcePickerSelectedTokens}\n showBelowMinimumInline={\n activeMode === \"swap\" && swapType === \"exactIn\"\n }\n swapBalance={swapBalance}\n swapSupportedChains={swapSupportedChainsAndTokens}\n title={\n activeMode === \"deposit\" || activeMode === \"send\"\n ? \"Choose Assets to Pay with\"\n : \"Choose assets to send\"\n }\n />\n \n \n )}\n\n {/* Drawer: choose-receive-asset */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep === \"choose-receive-asset\" && (\n \n \n \n {\n const tokenChanged = !isSameTokenSelection(toToken, token);\n if (tokenChanged) {\n onReceiveAssetChange?.({\n chainId: token.chainId,\n chainName: token.chainName,\n contractAddress: token.contractAddress,\n symbol: token.symbol,\n });\n }\n if (activeMode === \"send\" || activeMode === \"deposit\") {\n setExactOutQuoteSourceModeValue(\"all\");\n if (tokenChanged) {\n clearPendingSwapIntent();\n if (!isConfiguredAmountFixed) {\n setAmount(\"\");\n }\n }\n setSwapType(\"exactOut\");\n if (activeMode === \"deposit\") {\n const nextDeposit = getDepositForTokenSelection(\n configuredDepositOptions,\n token\n );\n if (nextDeposit) {\n setSelectedOpportunity(nextDeposit);\n }\n }\n setToToken(token);\n closeDrawerToIdle();\n return;\n }\n if (tokenChanged) {\n clearPendingSwapIntent();\n }\n if (swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n setToToken(token);\n closeDrawerToIdle();\n }}\n />\n \n \n )}\n \n );\n\n if (embed) return widgetContent;\n\n return (\n \n \n \n \n \n {widgetContent}\n \n \n );\n}\n\nexport default NexusWidget;\n", "type": "registry:component", "target": "components/nexus/nexus.tsx" }, diff --git a/registry.json b/registry.json index 178b6e5b..7430cc06 100644 --- a/registry.json +++ b/registry.json @@ -883,11 +883,21 @@ "target": "components/nexus/components/amount-input-unified.tsx", "type": "registry:component" }, + { + "path": "registry/avail-widgets/nexus-widget/components/deposit-funding-method.tsx", + "target": "components/nexus/components/deposit-funding-method.tsx", + "type": "registry:component" + }, { "path": "registry/avail-widgets/nexus-widget/components/deposit-idle-form.tsx", "target": "components/nexus/components/deposit-idle-form.tsx", "type": "registry:component" }, + { + "path": "registry/avail-widgets/nexus-widget/components/deposit-onramp-flow.tsx", + "target": "components/nexus/components/deposit-onramp-flow.tsx", + "type": "registry:component" + }, { "path": "registry/avail-widgets/nexus-widget/components/nexus-widget-progress-screen.tsx", "target": "components/nexus/components/nexus-widget-progress-screen.tsx", diff --git a/registry/avail-widgets/nexus-widget/components/deposit-funding-method.tsx b/registry/avail-widgets/nexus-widget/components/deposit-funding-method.tsx new file mode 100644 index 00000000..cca249bc --- /dev/null +++ b/registry/avail-widgets/nexus-widget/components/deposit-funding-method.tsx @@ -0,0 +1,303 @@ +// biome-ignore-all lint: NexusWidget registry component from shadcn registry. + +import { CreditCard, Wallet } from "lucide-react"; +import React from "react"; +import { nexusWidgetTheme } from "../theme"; + +type FundingMethod = "wallet" | "local-currency"; + +interface DepositFundingMethodProps { + onSelectLocalCurrency: () => void; + onSelectWallet: () => void; + primaryButtonForeground: string; + totalBalance: string; +} + +const theme = nexusWidgetTheme; +const brand = "var(--foreground-brand)"; + +const optionBaseStyle: React.CSSProperties = { + alignItems: "center", + backgroundColor: theme.colors.surface, + borderRadius: "12px", + borderStyle: "solid", + borderWidth: "1px", + boxSizing: "border-box", + cursor: "pointer", + display: "flex", + gap: "10px", + minHeight: "66px", + padding: "12px", + textAlign: "left", + width: "100%", +}; + +function MethodIcon({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ {children} +
+ ); +} + +function RadioMark({ selected }: { selected: boolean }) { + return ( + \n );\n}\n\nfunction AmountSkeleton({\n height = \"16px\",\n width = \"64px\",\n}: {\n height?: string;\n width?: string;\n}) {\n return (\n \n );\n}\n\nfunction FundingOption({\n active,\n amount,\n amountLoading = false,\n description,\n icon,\n label,\n onClick,\n recommended,\n}: {\n active: boolean;\n amount?: string;\n amountLoading?: boolean;\n description: string;\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n recommended?: boolean;\n}) {\n return (\n \n {icon}\n \n \n \n {label}\n \n {recommended && (\n \n Recommended\n \n )}\n \n \n {description}\n \n \n {(amount || amountLoading) && (\n \n {amountLoading ? : `$${amount}`}\n \n )}\n \n \n );\n}\n\nexport function DepositFundingMethod({\n isBalanceLoading = false,\n onSelectLocalCurrency,\n onSelectWallet,\n primaryButtonForeground,\n totalBalance,\n}: DepositFundingMethodProps) {\n const [selectedMethod, setSelectedMethod] =\n React.useState(null);\n\n const handleContinue = () => {\n if (!selectedMethod) return;\n if (selectedMethod === \"wallet\") {\n onSelectWallet();\n return;\n }\n onSelectLocalCurrency();\n };\n\n return (\n \n \n Select a funding method\n \n \n \n }\n label=\"Pay with Wallet\"\n onClick={() => {\n setSelectedMethod(\"wallet\");\n }}\n />\n \n }\n label=\"Pay with Local Currency\"\n onClick={() => {\n setSelectedMethod(\"local-currency\");\n }}\n recommended\n />\n \n \n Continue\n \n \n );\n}\n", "type": "registry:component", "target": "components/nexus/components/deposit-funding-method.tsx" }, @@ -44,7 +44,7 @@ }, { "path": "registry/avail-widgets/nexus-widget/components/deposit-onramp-flow.tsx", - "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\nimport Decimal from \"decimal.js\";\nimport {\n Check,\n ChevronDown,\n CreditCard,\n ExternalLink,\n Info,\n Landmark,\n Loader2,\n Search,\n Smartphone,\n X,\n} from \"lucide-react\";\nimport React from \"react\";\nimport {\n NEXUS_WIDGET_FAST_SPINNER_STYLE,\n nexusWidgetTheme,\n} from \"../theme\";\nimport type { NexusWidgetDepositOpportunityConfig } from \"../types\";\nimport type { SwapTokenOption } from \"./swap-asset-selector\";\n\ntype OnrampCryptoCurrency = {\n chainId: number;\n contract?: string;\n currencyCode: string;\n decimals?: number;\n name?: string;\n};\n\ntype OnrampOptionsResponse = {\n countries?: {\n countryCode: string;\n flagUrl?: string;\n name: string;\n }[];\n selection?: {\n countryCode: string;\n cryptoCurrencies?: OnrampCryptoCurrency[];\n defaultFiat?: string;\n defaultPaymentMethods?: string[];\n fiatCurrencies?: string[];\n } | null;\n};\n\ntype OnrampPaymentMethod = {\n limits?: {\n currencyCode?: string;\n max?: string;\n min?: string;\n };\n method: string;\n type?: string;\n};\n\ntype OnrampRoute = {\n paymentMethods?: OnrampPaymentMethod[];\n provider: string;\n};\n\ntype OnrampRoutesResponse = {\n routes?: OnrampRoute[];\n};\n\ntype OnrampQuote = {\n destinationAmount: string;\n destinationCurrencyCode: string;\n fees?: {\n network?: string;\n partner?: string;\n provider?: string;\n total?: string;\n };\n lowKyc?: boolean;\n paymentMethodType: string;\n provider: string;\n rampScore?: number;\n sourceAmount: string;\n sourceCurrencyCode: string;\n};\n\ntype OnrampQuoteResponse = {\n quotes?: OnrampQuote[];\n};\n\ntype OnrampSessionResponse = {\n createdAt?: string;\n fallbackWidgetUrl?: string;\n paymentMethodType?: string;\n provider?: string;\n rawMeldStatus?: string;\n deposit?: {\n explorerUrl?: string;\n state?: string;\n txHash?: string;\n };\n sessionId?: string;\n state?: string;\n transaction?: {\n destinationAmount?: string;\n destinationCurrencyCode?: string;\n sourceAmount?: string;\n sourceCurrencyCode?: string;\n txHash?: string;\n walletAddress?: string;\n };\n updatedAt?: string;\n widgetUrl?: string;\n};\n\ntype OnrampSheet =\n | \"currency\"\n | \"destination\"\n | \"fees\"\n | \"method\"\n | \"partner\"\n | null;\n\ninterface DepositOnrampFlowProps {\n baseUrl?: string;\n destinationTokens?: SwapTokenOption[];\n onConnectWallet: () => void | Promise;\n onError?: (message: string) => void;\n onSelectDestinationToken?: (token: SwapTokenOption) => void;\n onSessionStateChange?: (state: string | null) => void;\n ownerAddress?: string;\n opportunity?: NexusWidgetDepositOpportunityConfig;\n primaryButtonForeground: string;\n toToken?: SwapTokenOption;\n}\n\nconst ONRAMP_CLIENT_HEADER = \"nexus-widget\";\nconst ONRAMP_DEFAULT_COUNTRY = \"IN\";\nconst ONRAMP_DEFAULT_BASE_URL = \"http://localhost:8000\";\nconst QUOTE_REFRESH_SECONDS = 15;\nconst ONRAMP_SESSION_POLL_MS = 3000;\nconst TEST_FIAT_CURRENCIES = [\"INR\", \"USD\", \"EUR\", \"JPY\", \"GBP\"] as const;\nconst theme = nexusWidgetTheme;\nconst brand = \"var(--foreground-brand)\";\n\nconst panelStyle: React.CSSProperties = {\n backgroundColor: theme.colors.surface,\n border: `1px solid ${theme.colors.border}`,\n borderRadius: \"12px\",\n boxShadow: theme.shadows.card,\n boxSizing: \"border-box\",\n overflow: \"hidden\",\n width: \"100%\",\n};\n\nconst sectionLabelStyle: React.CSSProperties = {\n color: theme.colors.textSubtle,\n fontFamily: theme.fonts.sans,\n fontSize: \"12px\",\n fontWeight: 600,\n letterSpacing: \"0.08em\",\n lineHeight: \"15px\",\n textTransform: \"uppercase\",\n};\n\nconst compactTitleStyle: React.CSSProperties = {\n color: theme.colors.textStrong,\n fontFamily: theme.fonts.display,\n fontSize: \"18px\",\n fontWeight: 500,\n letterSpacing: \"0\",\n lineHeight: \"23px\",\n};\n\nconst compactBodyStyle: React.CSSProperties = {\n color: theme.colors.textSubtle,\n fontFamily: theme.fonts.sans,\n fontSize: \"13px\",\n lineHeight: \"18px\",\n};\n\nconst parseDecimal = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = String(value).replace(/[^0-9.-]/g, \"\");\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst normalizeAmountInput = (raw: string) => {\n let next = raw.replaceAll(/[^0-9.]/g, \"\");\n const parts = next.split(\".\");\n if (parts.length > 2) next = parts[0] + \".\" + parts.slice(1).join(\"\");\n const [integerPart, decimalPart] = next.split(\".\");\n if (decimalPart !== undefined) {\n next = `${integerPart}.${decimalPart.slice(0, 2)}`;\n }\n if (next === \".\") next = \"0.\";\n return next;\n};\n\nconst formatNumberDisplay = (value: unknown, maxDecimals = 2) => {\n const parsed = parseDecimal(value) ?? new Decimal(0);\n return parsed\n .toDecimalPlaces(maxDecimals, Decimal.ROUND_DOWN)\n .toFixed()\n .replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n};\n\nconst formatCurrencyAmount = (value: unknown, currencyCode?: string) => {\n const suffix = currencyCode ? ` ${currencyCode}` : \"\";\n return `${formatNumberDisplay(value)}${suffix}`;\n};\n\nconst formatUsdDisplay = (value: unknown) => {\n const parsed = parseDecimal(value) ?? new Decimal(0);\n if (parsed.gt(0) && parsed.lt(0.01)) return \"<$0.01\";\n return `$${formatNumberDisplay(parsed, 2)}`;\n};\n\nconst getFiatCurrencyName = (currencyCode?: string) => {\n switch ((currencyCode ?? \"\").toUpperCase()) {\n case \"EUR\":\n return \"Euro\";\n case \"GBP\":\n return \"British Pound\";\n case \"INR\":\n return \"Indian Rupee\";\n case \"JPY\":\n return \"Japanese Yen\";\n case \"USD\":\n return \"US Dollar\";\n default:\n return currencyCode ?? \"\";\n }\n};\n\nconst getMethodLabel = (method?: string) => {\n switch ((method ?? \"\").toUpperCase()) {\n case \"APPLE_PAY\":\n return \"Apple Pay\";\n case \"GOOGLE_PAY\":\n return \"Google Pay\";\n case \"UPI\":\n return \"UPI\";\n case \"BANK_TRANSFER\":\n case \"IMPS\":\n case \"NEFT\":\n case \"RTGS\":\n return \"Bank Transfer\";\n case \"CREDIT_DEBIT_CARD\":\n case \"CARD\":\n return \"Credit / Debit Cards\";\n default:\n return method\n ? method\n .split(\"_\")\n .filter(Boolean)\n .map(\n (part) =>\n part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()\n )\n .join(\" \")\n : \"Payment method\";\n }\n};\n\nconst getMethodSubtitle = (method?: string) => {\n switch ((method ?? \"\").toUpperCase()) {\n case \"UPI\":\n return \"~1 min\";\n case \"APPLE_PAY\":\n case \"GOOGLE_PAY\":\n return \"~2 min\";\n case \"BANK_TRANSFER\":\n case \"IMPS\":\n case \"NEFT\":\n case \"RTGS\":\n return \"~3 min\";\n case \"CREDIT_DEBIT_CARD\":\n case \"CARD\":\n return \"~3 min\";\n default:\n return \"Available route\";\n }\n};\n\nconst getProviderLabel = (provider?: string) =>\n provider\n ? provider\n .split(/[_\\s-]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())\n .join(\" \")\n : \"Payment partner\";\n\nconst getProviderInitials = (provider?: string) => {\n const label = getProviderLabel(provider);\n const parts = label.split(/\\s+/).filter(Boolean);\n if (parts.length <= 1) return label.slice(0, 2).toUpperCase();\n return `${parts[0]?.[0] ?? \"\"}${parts[1]?.[0] ?? \"\"}`.toUpperCase();\n};\n\nconst getErrorMessage = (error: unknown) => {\n if (error instanceof Error) return error.message;\n if (typeof error === \"string\") return error;\n return \"Unable to continue with local currency.\";\n};\n\nconst ONRAMP_TERMINAL_STATES = new Set([\n \"CANCELLED\",\n \"EXPIRED\",\n \"FAILED\",\n \"REFUNDED\",\n \"SETTLED\",\n]);\n\nconst getNormalizedOnrampState = (state?: string | null) =>\n (state ?? \"\").trim().toUpperCase();\n\nconst isOnrampTerminalState = (state?: string | null) =>\n ONRAMP_TERMINAL_STATES.has(getNormalizedOnrampState(state)) ||\n isOnrampDepositSuccessState(state) ||\n isOnrampDepositFailedState(state);\n\nconst isOnrampProcessingState = (state?: string | null) => {\n const normalized = getNormalizedOnrampState(state);\n return normalized === \"PROCESSING\" || normalized === \"SETTLING\";\n};\n\nconst ONRAMP_DEPOSIT_PROCESSING_STATES = new Set([\n \"COMPLETING_DEPOSIT\",\n \"DEPOSIT_PROCESSING\",\n \"DEPOSITING\",\n]);\n\nconst ONRAMP_DEPOSIT_SUCCESS_STATES = new Set([\n \"COMPLETED\",\n \"DEPOSIT_COMPLETE\",\n \"DEPOSIT_SUCCESS\",\n \"DEPOSITED\",\n]);\n\nconst ONRAMP_DEPOSIT_FAILED_STATES = new Set([\n \"DEPOSIT_ATTENTION\",\n \"DEPOSIT_FAILED\",\n \"DEPOSIT_REQUIRES_ATTENTION\",\n]);\n\nconst isOnrampDepositProcessingState = (state?: string | null) =>\n ONRAMP_DEPOSIT_PROCESSING_STATES.has(getNormalizedOnrampState(state));\n\nconst isOnrampDepositSuccessState = (state?: string | null) =>\n ONRAMP_DEPOSIT_SUCCESS_STATES.has(getNormalizedOnrampState(state));\n\nconst isOnrampDepositFailedState = (state?: string | null) =>\n ONRAMP_DEPOSIT_FAILED_STATES.has(getNormalizedOnrampState(state));\n\nconst isNativeAddress = (address?: string) => {\n const lower = address?.toLowerCase();\n return (\n lower === \"0x0000000000000000000000000000000000000000\" ||\n lower === \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\"\n );\n};\n\nconst getChainCurrencySuffix = (chainId?: number, chainName?: string) => {\n switch (chainId) {\n case 1:\n return \"ETHEREUM\";\n case 10:\n return \"OPTIMISM\";\n case 56:\n return \"BNB\";\n case 137:\n return \"POLYGON\";\n case 8453:\n return \"BASE\";\n case 42161:\n return \"ARBITRUM\";\n case 43114:\n return \"AVALANCHE\";\n default:\n return (chainName ?? \"CHAIN\").replaceAll(/[^a-zA-Z0-9]+/g, \"_\").toUpperCase();\n }\n};\n\nconst getFallbackDestinationCurrencyCode = (token?: SwapTokenOption) => {\n if (!token?.symbol) return \"\";\n return `${token.symbol.toUpperCase()}_${getChainCurrencySuffix(\n token.chainId,\n token.chainName\n )}`;\n};\n\nconst getDestinationCurrencyCode = (\n options: OnrampOptionsResponse | null,\n token?: SwapTokenOption\n) => {\n if (!token?.chainId || !token.contractAddress) {\n return getFallbackDestinationCurrencyCode(token);\n }\n\n const targetAddress = token.contractAddress.toLowerCase();\n const currencies = options?.selection?.cryptoCurrencies ?? [];\n const matched = currencies.find((currency) => {\n if (currency.chainId !== token.chainId) return false;\n const contract = currency.contract?.toLowerCase();\n if (!contract) return false;\n return (\n contract === targetAddress ||\n (isNativeAddress(contract) && isNativeAddress(targetAddress))\n );\n });\n\n return matched?.currencyCode ?? getFallbackDestinationCurrencyCode(token);\n};\n\nconst getOnrampTokenKey = (token?: SwapTokenOption) => {\n if (!token?.chainId || !token.contractAddress) return \"\";\n return `${token.chainId}:${token.contractAddress.toLowerCase()}`;\n};\n\nconst isSameOnrampToken = (\n left?: SwapTokenOption,\n right?: SwapTokenOption\n) => Boolean(left && right && getOnrampTokenKey(left) === getOnrampTokenKey(right));\n\nconst sortQuotes = (quotes: OnrampQuote[]) =>\n [...quotes].sort((a, b) => {\n const scoreDelta = (b.rampScore ?? 0) - (a.rampScore ?? 0);\n if (scoreDelta !== 0) return scoreDelta;\n const destinationDelta =\n parseDecimal(b.destinationAmount)?.cmp(\n parseDecimal(a.destinationAmount) ?? new Decimal(0)\n ) ?? 0;\n return destinationDelta;\n });\n\nconst matchesSearch = (query: string, values: Array) => {\n const normalizedQuery = query.trim().toLowerCase();\n if (!normalizedQuery) return true;\n return values.some((value) =>\n (value ?? \"\").toLowerCase().includes(normalizedQuery)\n );\n};\n\nconst createIdempotencyKey = () => {\n if (typeof crypto !== \"undefined\" && \"randomUUID\" in crypto) {\n return crypto.randomUUID();\n }\n return `${Date.now()}-${Math.random().toString(16).slice(2)}`;\n};\n\nconst fetchOnrampJson = async (\n baseUrl: string,\n path: string,\n init?: RequestInit\n) => {\n const response = await fetch(`${baseUrl}${path}`, {\n ...init,\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-nexus-client\": ONRAMP_CLIENT_HEADER,\n ...(init?.headers ?? {}),\n },\n });\n const text = await response.text();\n const data = text ? JSON.parse(text) : {};\n\n if (!response.ok) {\n const message =\n typeof data?.message === \"string\"\n ? data.message\n : `Onramp request failed (${response.status})`;\n throw new Error(message);\n }\n\n return data as T;\n};\n\nfunction TokenLogo({\n label,\n size = 30,\n src,\n}: {\n label?: string;\n size?: number;\n src?: string;\n}) {\n const [failed, setFailed] = React.useState(!src);\n\n React.useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n backgroundColor: theme.colors.surface,\n borderRadius: \"999px\",\n height: size,\n objectFit: \"cover\",\n width: size,\n }}\n />\n );\n }\n\n return (\n \n {(label || \"?\").slice(0, 2).toUpperCase()}\n \n );\n}\n\nfunction TokenLogoPair({ token }: { token?: SwapTokenOption }) {\n return (\n \n \n {token?.chainLogo && (\n \n \n \n )}\n \n );\n}\n\nfunction CurrencyMark({ code }: { code?: string }) {\n return (\n \n {(code ?? \"?\").slice(0, 3).toUpperCase()}\n \n );\n}\n\nfunction ProviderMark({ provider }: { provider?: string }) {\n return (\n \n {getProviderInitials(provider)}\n \n );\n}\n\nfunction MethodMark({ method }: { method?: string }) {\n const normalized = (method ?? \"\").toUpperCase();\n const icon =\n normalized === \"UPI\" || normalized === \"APPLE_PAY\" || normalized === \"GOOGLE_PAY\" ? (\n \n ) : normalized.includes(\"BANK\") ||\n normalized === \"IMPS\" ||\n normalized === \"NEFT\" ||\n normalized === \"RTGS\" ? (\n \n ) : (\n \n );\n\n return (\n \n {icon}\n \n );\n}\n\nfunction SelectPill({\n children,\n disabled,\n onClick,\n}: {\n children: React.ReactNode;\n disabled?: boolean;\n onClick?: () => void;\n}) {\n return (\n \n {children}\n {!disabled && (\n \n )}\n \n );\n}\n\nfunction EditButton({\n disabled,\n onClick,\n}: {\n disabled?: boolean;\n onClick: () => void;\n}) {\n return (\n \n Edit\n \n );\n}\n\nfunction DetailRow({\n action,\n children,\n divider,\n label,\n}: {\n action?: React.ReactNode;\n children: React.ReactNode;\n divider?: boolean;\n label: string;\n}) {\n return (\n \n \n
{label}
\n {action}\n \n {children}\n \n );\n}\n\nfunction Sheet({\n children,\n onClose,\n title,\n}: {\n children: React.ReactNode;\n onClose: () => void;\n title: string;\n}) {\n return (\n \n \n \n \n \n {title}\n \n \n \n \n \n \n {children}\n \n \n \n );\n}\n\nfunction SheetSearchInput({\n onChange,\n placeholder,\n value,\n}: {\n onChange: (value: string) => void;\n placeholder: string;\n value: string;\n}) {\n const [focused, setFocused] = React.useState(false);\n return (\n \n \n setFocused(false)}\n onChange={(event) => onChange(event.target.value)}\n onFocus={() => setFocused(true)}\n placeholder={placeholder}\n style={{\n backgroundColor: \"transparent\",\n border: \"none\",\n color: theme.colors.textStrong,\n flex: \"1 1 0%\",\n fontFamily: theme.fonts.sans,\n fontSize: \"14px\",\n lineHeight: \"18px\",\n minWidth: 0,\n outline: \"none\",\n padding: 0,\n }}\n value={value}\n />\n {value && (\n onChange(\"\")}\n style={{\n alignItems: \"center\",\n backgroundColor: \"transparent\",\n border: \"none\",\n cursor: \"pointer\",\n display: \"flex\",\n padding: \"2px\",\n }}\n type=\"button\"\n >\n \n \n )}\n \n );\n}\n\nfunction SelectRow({\n icon,\n onClick,\n primary,\n selected,\n subtitle,\n title,\n value,\n}: {\n icon: React.ReactNode;\n onClick: () => void;\n primary?: boolean;\n selected: boolean;\n subtitle?: string;\n title: string;\n value?: string;\n}) {\n return (\n \n {icon}\n \n \n \n {title}\n \n {primary && (\n \n Best rate\n \n )}\n \n {subtitle && (\n \n {subtitle}\n \n )}\n \n {value && (\n \n {value}\n \n )}\n \n {selected && (\n \n )}\n \n \n );\n}\n\nfunction EmptySheetMessage({ children }: { children: React.ReactNode }) {\n return (\n \n {children}\n \n );\n}\n\nfunction SkeletonBlock({\n borderRadius = \"8px\",\n height,\n width,\n}: {\n borderRadius?: string;\n height: string;\n width: string;\n}) {\n return (\n \n );\n}\n\nfunction QuoteDetailsSkeleton() {\n return (\n
\n }\n label=\"Payment Method\"\n >\n
\n \n \n
\n \n\n }\n divider\n label=\"Payment Partner\"\n >\n
\n \n
\n \n \n
\n
\n \n\n \n
\n \n \n
\n
\n \n \n
\n
\n \n );\n}\n\nconst formatTokenAmountDisplay = (amount: unknown, symbol?: string) => {\n const suffix = symbol ? ` ${symbol}` : \"\";\n if (amount === null || amount === undefined || amount === \"\") {\n return suffix ? `--${suffix}` : \"--\";\n }\n return `${formatNumberDisplay(amount, 6)}${suffix}`;\n};\n\nconst getOnrampSessionSubtitle = (\n state: string | null | undefined,\n opportunity?: NexusWidgetDepositOpportunityConfig\n) => {\n const normalized = getNormalizedOnrampState(state);\n if (normalized === \"SETTLED\" || isOnrampDepositSuccessState(normalized)) {\n return `The amount was deposited on ${getDepositTargetLabel(opportunity)}`;\n }\n if (isOnrampDepositFailedState(normalized)) {\n return \"The funds are in your wallet, but the deposit transaction could not be performed automatically.\";\n }\n if (isOnrampDepositProcessingState(normalized)) {\n return \"Usually takes 20 seconds\";\n }\n if (normalized === \"FAILED\") return \"The payment could not be completed\";\n if (normalized === \"CANCELLED\") return \"No payment was completed\";\n if (normalized === \"REFUNDED\") return \"The payment was refunded\";\n if (normalized === \"EXPIRED\") return \"Start a new payment to continue\";\n if (isOnrampProcessingState(normalized)) return \"Usually takes 2 - 5 min\";\n return \"The payment processor requires some information\";\n};\n\nconst getOnrampSummaryLabel = (\n state: string | null | undefined,\n provider?: string\n) => {\n const partner = getProviderLabel(provider);\n const normalized = getNormalizedOnrampState(state);\n if (normalized === \"SETTLED\") return `Payment received from ${partner}`;\n if (normalized === \"FAILED\") return `Payment failed with ${partner}`;\n if (normalized === \"CANCELLED\") return `Payment cancelled with ${partner}`;\n if (normalized === \"REFUNDED\") return `Payment refunded by ${partner}`;\n if (normalized === \"EXPIRED\") return `Payment expired with ${partner}`;\n if (isOnrampProcessingState(normalized)) {\n return `Payment to be received by ${partner}`;\n }\n return `Payment to receive from ${partner}`;\n};\n\nconst getDepositTargetLabel = (\n opportunity?: NexusWidgetDepositOpportunityConfig\n) =>\n opportunity?.title ||\n opportunity?.label ||\n opportunity?.protocol ||\n \"the selected market\";\n\nconst getDepositChainName = (\n opportunity?: NexusWidgetDepositOpportunityConfig,\n token?: SwapTokenOption\n) => token?.chainName ?? opportunity?.subtitle?.replace(/^on\\s+/i, \"\") ?? \"chain\";\n\nconst getExplorerBaseUrl = (chainId?: number) => {\n switch (chainId) {\n case 1:\n return \"https://etherscan.io/tx/\";\n case 10:\n return \"https://optimistic.etherscan.io/tx/\";\n case 56:\n return \"https://bscscan.com/tx/\";\n case 137:\n return \"https://polygonscan.com/tx/\";\n case 8453:\n return \"https://basescan.org/tx/\";\n case 42161:\n return \"https://arbiscan.io/tx/\";\n case 43114:\n return \"https://snowtrace.io/tx/\";\n default:\n return undefined;\n }\n};\n\nconst getOnrampExplorerUrl = (\n session: OnrampSessionResponse,\n opportunity?: NexusWidgetDepositOpportunityConfig,\n token?: SwapTokenOption\n) => {\n if (session.deposit?.explorerUrl) return session.deposit.explorerUrl;\n if (opportunity?.explorerUrl) return opportunity.explorerUrl;\n const txHash = session.deposit?.txHash ?? session.transaction?.txHash;\n const baseUrl = getExplorerBaseUrl(token?.chainId ?? opportunity?.chainId);\n return txHash && baseUrl ? `${baseUrl}${txHash}` : undefined;\n};\n\nfunction OnrampStatusArtwork({ tone = \"neutral\" }: { tone?: \"green\" | \"neutral\" }) {\n const pulseColor =\n tone === \"green\"\n ? \"rgba(67, 190, 162, 0.72)\"\n : \"rgba(115, 129, 148, 0.58)\";\n return (\n \n \n {Array.from({ length: 18 }).map((_, index) => (\n \n ))}\n \n \n );\n}\n\nfunction SafeCloseNotice() {\n return (\n \n \n Safe to close - we'll notify you when complete\n \n );\n}\n\nfunction OnrampStatusButton({\n children,\n onClick,\n primaryButtonForeground,\n variant = \"primary\",\n}: {\n children: React.ReactNode;\n onClick: () => void;\n primaryButtonForeground: string;\n variant?: \"primary\" | \"secondary\";\n}) {\n const isPrimary = variant === \"primary\";\n return (\n \n {children}\n \n );\n}\n\nfunction WarningStatusIcon({ tone = \"red\" }: { tone?: \"orange\" | \"red\" }) {\n const color = tone === \"orange\" ? \"#F59E0B\" : \"#E8453C\";\n const backgroundColor = tone === \"orange\" ? \"#FFF7ED\" : \"#FEF2F2\";\n return (\n \n \n \n \n \n \n );\n}\n\nfunction OnrampActionStatusPanel({\n description,\n onPrimary,\n onSecondary,\n primaryButtonForeground,\n primaryLabel,\n secondaryLabel,\n title,\n}: {\n description: string;\n onPrimary: () => void;\n onSecondary: () => void;\n primaryButtonForeground: string;\n primaryLabel: string;\n secondaryLabel: string;\n title: string;\n}) {\n return (\n \n \n
\n
\n {title}\n
\n
\n {description}\n
\n
\n \n \n {primaryLabel}\n \n \n {secondaryLabel}\n \n \n \n );\n}\n\nfunction TimelineMarker({\n isComplete,\n isLast,\n isPending,\n}: {\n isComplete?: boolean;\n isLast?: boolean;\n isPending?: boolean;\n}) {\n return (\n \n \n {isComplete ? (\n \n ) : (\n \n )}\n \n {!isLast && (\n \n )}\n \n );\n}\n\nconst primaryButtonContrastColor = \"#FFFFFE\";\n\nfunction TimelineStep({\n isComplete,\n isLast,\n isPending,\n subtitle,\n title,\n}: {\n isComplete?: boolean;\n isLast?: boolean;\n isPending?: boolean;\n subtitle: string;\n title: string;\n}) {\n return (\n \n \n \n \n {title}\n \n \n {subtitle}\n \n \n \n );\n}\n\nfunction OnrampProcessingTimelinePanel({\n destinationAmount,\n destinationChainName,\n destinationSymbol,\n isSettling,\n provider,\n sourceAmount,\n sourceCurrencyCode,\n targetLabel,\n}: {\n destinationAmount: string;\n destinationChainName: string;\n destinationSymbol?: string;\n isSettling: boolean;\n provider?: string;\n sourceAmount: string;\n sourceCurrencyCode: string;\n targetLabel: string;\n}) {\n const destinationDisplay = formatTokenAmountDisplay(\n destinationAmount,\n destinationSymbol\n );\n const sourceDisplay = formatCurrencyAmount(sourceAmount, sourceCurrencyCode);\n return (\n <>\n \n Usually takes 2 - 5 min\n \n \n \n \n \n \n {isSettling\n ? \"Sending amount to your wallet\"\n : \"Processing your payment\"}\n \n \n {isSettling ? destinationDisplay : sourceDisplay}\n \n \n \n \n \n \n \n \n \n \n \n \n );\n}\n\nfunction OnrampCompletingDepositPanel({\n destinationAmount,\n destinationSymbol,\n targetLabel,\n}: {\n destinationAmount: string;\n destinationSymbol?: string;\n targetLabel: string;\n}) {\n return (\n <>\n \n Usually takes 20 seconds\n \n \n \n \n \n Depositing to {targetLabel}\n \n \n {formatTokenAmountDisplay(destinationAmount, destinationSymbol)}\n \n \n \n \n \n \n );\n}\n\nfunction OnrampSuccessPanel({\n destinationAmount,\n destinationSymbol,\n explorerUrl,\n onDone,\n paymentMethod,\n primaryButtonForeground,\n provider,\n sourceAmount,\n sourceCurrencyCode,\n subtitle,\n}: {\n destinationAmount: string;\n destinationSymbol?: string;\n explorerUrl?: string;\n onDone: () => void;\n paymentMethod?: string;\n primaryButtonForeground: string;\n provider?: string;\n sourceAmount: string;\n sourceCurrencyCode: string;\n subtitle: string;\n}) {\n return (\n <>\n \n {subtitle}\n \n \n \n
Transaction Summary
\n \n \n \n \n \n \n \n {explorerUrl && (\n \n \n View Explorer\n \n \n )}\n \n Done\n \n \n );\n}\n\nfunction SummaryRow({ label, value }: { label: string; value: string }) {\n return (\n \n \n {label}\n \n \n {value}\n \n \n );\n}\n\nfunction OnrampHandoffPanel({\n provider,\n summaryAmount,\n}: {\n provider?: string;\n summaryAmount: string;\n}) {\n return (\n <>\n \n The payment processor requires some information\n \n \n \n \n \n {getOnrampSummaryLabel(\"AWAITING_USER\", provider)}\n \n \n {summaryAmount}\n \n \n \n \n \n );\n}\n\nfunction OnrampSessionStatusPanel({\n onCancel,\n onDone,\n onRetryPayment,\n opportunity,\n primaryButtonForeground,\n quote,\n session,\n sourceAmount,\n sourceCurrencyCode,\n toToken,\n}: {\n onCancel: () => void;\n onDone: () => void;\n onRetryPayment: () => void;\n opportunity?: NexusWidgetDepositOpportunityConfig;\n primaryButtonForeground: string;\n quote?: OnrampQuote;\n session: OnrampSessionResponse;\n sourceAmount: string;\n sourceCurrencyCode: string;\n toToken?: SwapTokenOption;\n}) {\n const normalizedState =\n getNormalizedOnrampState(session.state) || \"AWAITING_USER\";\n const provider = session.provider ?? quote?.provider;\n const transaction = session.transaction;\n const destinationSymbol = toToken?.symbol ?? quote?.destinationCurrencyCode;\n const destinationAmount =\n transaction?.destinationAmount ?? quote?.destinationAmount ?? \"\";\n const sourceDisplayAmount =\n transaction?.sourceAmount ?? quote?.sourceAmount ?? sourceAmount;\n const sourceDisplayCurrency =\n transaction?.sourceCurrencyCode ??\n quote?.sourceCurrencyCode ??\n sourceCurrencyCode;\n const destinationChainName = getDepositChainName(opportunity, toToken);\n const targetLabel = getDepositTargetLabel(opportunity);\n const explorerUrl = getOnrampExplorerUrl(session, opportunity, toToken);\n const containerStyle: React.CSSProperties = {\n boxSizing: \"border-box\",\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"12px\",\n width: \"100%\",\n };\n\n if (normalizedState === \"FAILED\") {\n return (\n
\n \n
\n );\n }\n\n if ([\"CANCELLED\", \"EXPIRED\", \"REFUNDED\"].includes(normalizedState)) {\n const title =\n normalizedState === \"CANCELLED\"\n ? \"Payment cancelled\"\n : normalizedState === \"REFUNDED\"\n ? \"Payment refunded\"\n : \"Payment expired\";\n const description =\n normalizedState === \"CANCELLED\"\n ? \"No payment was completed.\"\n : normalizedState === \"REFUNDED\"\n ? `${getProviderLabel(provider)} refunded the payment.`\n : \"Start a new local currency payment to continue.\";\n return (\n
\n \n
\n );\n }\n\n if (isOnrampDepositFailedState(normalizedState)) {\n return (\n
\n \n
\n );\n }\n\n if (normalizedState === \"SETTLED\" || isOnrampDepositSuccessState(normalizedState)) {\n return (\n
\n \n
\n );\n }\n\n if (isOnrampDepositProcessingState(normalizedState)) {\n return (\n
\n \n
\n );\n }\n\n if (isOnrampProcessingState(normalizedState)) {\n return (\n
\n \n
\n );\n }\n\n return (\n
\n \n
\n );\n}\n\nexport function DepositOnrampFlow({\n baseUrl = ONRAMP_DEFAULT_BASE_URL,\n destinationTokens,\n onConnectWallet,\n onError,\n onSelectDestinationToken,\n onSessionStateChange,\n ownerAddress,\n opportunity,\n primaryButtonForeground,\n toToken,\n}: DepositOnrampFlowProps) {\n const [countryCode] = React.useState(ONRAMP_DEFAULT_COUNTRY);\n const [sourceCurrencyCode, setSourceCurrencyCode] = React.useState(\"INR\");\n const [sourceAmount, setSourceAmount] = React.useState(\"\");\n const [options, setOptions] = React.useState(\n null\n );\n const [routes, setRoutes] = React.useState([]);\n const [selectedPaymentMethod, setSelectedPaymentMethod] =\n React.useState(\"\");\n const [quotes, setQuotes] = React.useState([]);\n const [selectedProvider, setSelectedProvider] = React.useState(\"\");\n const [activeSheet, setActiveSheet] = React.useState(null);\n const [currencySearch, setCurrencySearch] = React.useState(\"\");\n const [destinationSearch, setDestinationSearch] = React.useState(\"\");\n const [methodSearch, setMethodSearch] = React.useState(\"\");\n const [partnerSearch, setPartnerSearch] = React.useState(\"\");\n const [optionsLoading, setOptionsLoading] = React.useState(false);\n const [routesLoading, setRoutesLoading] = React.useState(false);\n const [quotesLoading, setQuotesLoading] = React.useState(false);\n const [sessionLoading, setSessionLoading] = React.useState(false);\n const [error, setError] = React.useState(null);\n const [session, setSession] = React.useState(\n null\n );\n const [quoteRefreshSeconds, setQuoteRefreshSeconds] = React.useState(\n QUOTE_REFRESH_SECONDS\n );\n const quoteRunIdRef = React.useRef(0);\n const normalizedSessionState = getNormalizedOnrampState(session?.state);\n\n const fiatCurrencies = TEST_FIAT_CURRENCIES;\n const availableDestinationTokens = React.useMemo(() => {\n const tokens = destinationTokens?.length ? destinationTokens : toToken ? [toToken] : [];\n const byKey = new Map();\n for (const token of tokens) {\n const key = getOnrampTokenKey(token);\n if (!key) continue;\n byKey.set(key, token);\n }\n return Array.from(byKey.values());\n }, [destinationTokens, toToken]);\n const destinationCurrencyCode = React.useMemo(\n () => getDestinationCurrencyCode(options, toToken),\n [options, toToken]\n );\n const availablePaymentMethods = React.useMemo(() => {\n const map = new Map();\n for (const route of routes) {\n for (const method of route.paymentMethods ?? []) {\n if (!method.method || map.has(method.method)) continue;\n map.set(method.method, method);\n }\n }\n return Array.from(map.values());\n }, [routes]);\n const selectedPaymentMethodDetails = availablePaymentMethods.find(\n (method) => method.method === selectedPaymentMethod\n );\n const selectedQuote =\n quotes.find((quote) => quote.provider === selectedProvider) ?? quotes[0];\n const filteredFiatCurrencies = React.useMemo(\n () =>\n fiatCurrencies.filter((currencyCode) =>\n matchesSearch(currencySearch, [\n currencyCode,\n getFiatCurrencyName(currencyCode),\n ])\n ),\n [currencySearch, fiatCurrencies]\n );\n const filteredDestinationTokens = React.useMemo(\n () =>\n availableDestinationTokens.filter((token) =>\n matchesSearch(destinationSearch, [\n token.symbol,\n token.name,\n token.chainName,\n token.contractAddress,\n ])\n ),\n [availableDestinationTokens, destinationSearch]\n );\n const filteredPaymentMethods = React.useMemo(\n () =>\n availablePaymentMethods.filter((method) =>\n matchesSearch(methodSearch, [\n method.method,\n getMethodLabel(method.method),\n getMethodSubtitle(method.method),\n ])\n ),\n [availablePaymentMethods, methodSearch]\n );\n const filteredQuotes = React.useMemo(\n () =>\n quotes.filter((quote) =>\n matchesSearch(partnerSearch, [\n quote.provider,\n getProviderLabel(quote.provider),\n quote.paymentMethodType,\n getMethodLabel(quote.paymentMethodType),\n ])\n ),\n [partnerSearch, quotes]\n );\n const hasMultipleCurrencies = fiatCurrencies.length > 1;\n const hasMultipleDestinationTokens = availableDestinationTokens.length > 1;\n const hasMultipleMethods = availablePaymentMethods.length > 1;\n const hasMultipleProviders = quotes.length > 1;\n const isLoading = optionsLoading || routesLoading || quotesLoading;\n const parsedSourceAmount = React.useMemo(\n () => parseDecimal(sourceAmount),\n [sourceAmount]\n );\n const hasPositiveSourceAmount = Boolean(parsedSourceAmount?.gt(0));\n const amountLimitMessage = (() => {\n const limits = selectedPaymentMethodDetails?.limits;\n if (!limits || !parsedSourceAmount?.gt(0)) return null;\n const min = parseDecimal(limits.min);\n const max = parseDecimal(limits.max);\n if (min && parsedSourceAmount.lt(min)) {\n return `Minimum ${formatCurrencyAmount(min, sourceCurrencyCode)}`;\n }\n if (max && parsedSourceAmount.gt(max)) {\n return `Maximum ${formatCurrencyAmount(max, sourceCurrencyCode)}`;\n }\n return null;\n })();\n\n const loadOptions = React.useCallback(async () => {\n setOptionsLoading(true);\n setError(null);\n try {\n const data = await fetchOnrampJson(\n baseUrl,\n `/api/v1/onramp/options?countryCode=${encodeURIComponent(countryCode)}`,\n { method: \"GET\" }\n );\n setOptions(data);\n setSourceCurrencyCode((current) => current || \"INR\");\n } catch (requestError) {\n const message = getErrorMessage(requestError);\n setError(message);\n onError?.(message);\n } finally {\n setOptionsLoading(false);\n }\n }, [baseUrl, countryCode, onError]);\n\n React.useEffect(() => {\n void loadOptions();\n }, [loadOptions]);\n\n React.useEffect(() => {\n if (activeSheet !== \"currency\") setCurrencySearch(\"\");\n if (activeSheet !== \"destination\") setDestinationSearch(\"\");\n if (activeSheet !== \"method\") setMethodSearch(\"\");\n if (activeSheet !== \"partner\") setPartnerSearch(\"\");\n }, [activeSheet]);\n\n React.useEffect(() => {\n if (!sourceCurrencyCode || !destinationCurrencyCode) return;\n let cancelled = false;\n\n const loadRoutes = async () => {\n setRoutesLoading(true);\n setError(null);\n try {\n const params = new URLSearchParams({\n countryCode,\n destinationCurrencyCode,\n sourceCurrencyCode,\n });\n const data = await fetchOnrampJson(\n baseUrl,\n `/api/v1/onramp/routes?${params.toString()}`,\n { method: \"GET\" }\n );\n if (cancelled) return;\n const nextRoutes = data.routes ?? [];\n setRoutes(nextRoutes);\n const methods = nextRoutes.flatMap((route) => route.paymentMethods ?? []);\n const preferred =\n options?.selection?.defaultPaymentMethods?.find((method) =>\n methods.some((candidate) => candidate.method === method)\n ) ?? methods[0]?.method;\n setSelectedPaymentMethod((current) =>\n current && methods.some((method) => method.method === current)\n ? current\n : preferred ?? \"\"\n );\n } catch (requestError) {\n if (cancelled) return;\n const message = getErrorMessage(requestError);\n setError(message);\n onError?.(message);\n } finally {\n if (!cancelled) setRoutesLoading(false);\n }\n };\n\n void loadRoutes();\n return () => {\n cancelled = true;\n };\n }, [\n baseUrl,\n countryCode,\n destinationCurrencyCode,\n onError,\n options?.selection?.defaultPaymentMethods,\n sourceCurrencyCode,\n ]);\n\n const fetchQuotes = React.useCallback(async () => {\n if (\n !ownerAddress ||\n !sourceCurrencyCode ||\n !destinationCurrencyCode ||\n !selectedPaymentMethod ||\n !parsedSourceAmount?.gt(0) ||\n amountLimitMessage\n ) {\n return;\n }\n\n const runId = quoteRunIdRef.current + 1;\n quoteRunIdRef.current = runId;\n setQuotesLoading(true);\n setError(null);\n try {\n const data = await fetchOnrampJson(\n baseUrl,\n \"/api/v1/onramp/quote\",\n {\n body: JSON.stringify({\n countryCode,\n destinationCurrencyCode,\n paymentMethodType: selectedPaymentMethod,\n sourceAmount,\n sourceCurrencyCode,\n walletAddress: ownerAddress,\n }),\n method: \"POST\",\n }\n );\n if (quoteRunIdRef.current !== runId) return;\n const nextQuotes = sortQuotes(data.quotes ?? []);\n setQuotes(nextQuotes);\n setSelectedProvider((current) =>\n current && nextQuotes.some((quote) => quote.provider === current)\n ? current\n : nextQuotes[0]?.provider ?? \"\"\n );\n if (nextQuotes.length === 0) {\n setError(\"No onramp quotes are available for this amount.\");\n }\n } catch (requestError) {\n if (quoteRunIdRef.current !== runId) return;\n const message = getErrorMessage(requestError);\n setQuotes([]);\n setError(message);\n onError?.(message);\n } finally {\n if (quoteRunIdRef.current === runId) {\n setQuotesLoading(false);\n setQuoteRefreshSeconds(QUOTE_REFRESH_SECONDS);\n }\n }\n }, [\n amountLimitMessage,\n baseUrl,\n countryCode,\n destinationCurrencyCode,\n onError,\n ownerAddress,\n parsedSourceAmount,\n selectedPaymentMethod,\n sourceAmount,\n sourceCurrencyCode,\n ]);\n\n React.useEffect(() => {\n setRoutes([]);\n setSelectedPaymentMethod(\"\");\n setQuotes([]);\n setSelectedProvider(\"\");\n setSession(null);\n }, [destinationCurrencyCode]);\n\n React.useEffect(() => {\n if (session?.sessionId) return;\n setQuotes([]);\n setSelectedProvider(\"\");\n if (\n !ownerAddress ||\n !sourceCurrencyCode ||\n !destinationCurrencyCode ||\n !selectedPaymentMethod ||\n !parsedSourceAmount?.gt(0) ||\n amountLimitMessage\n ) {\n return;\n }\n const timer = window.setTimeout(() => {\n void fetchQuotes();\n }, 350);\n return () => {\n window.clearTimeout(timer);\n };\n }, [\n amountLimitMessage,\n destinationCurrencyCode,\n fetchQuotes,\n ownerAddress,\n parsedSourceAmount,\n selectedPaymentMethod,\n session?.sessionId,\n sourceCurrencyCode,\n ]);\n\n React.useEffect(() => {\n if (session?.sessionId || !selectedQuote || quotesLoading || sessionLoading) {\n return;\n }\n setQuoteRefreshSeconds(QUOTE_REFRESH_SECONDS);\n const interval = window.setInterval(() => {\n setQuoteRefreshSeconds((current) => {\n if (current <= 1) {\n void fetchQuotes();\n return QUOTE_REFRESH_SECONDS;\n }\n return current - 1;\n });\n }, 1000);\n return () => {\n window.clearInterval(interval);\n };\n }, [\n fetchQuotes,\n quotesLoading,\n selectedQuote,\n session?.sessionId,\n sessionLoading,\n ]);\n\n React.useEffect(() => {\n onSessionStateChange?.(\n session?.sessionId\n ? normalizedSessionState || \"AWAITING_USER\"\n : null\n );\n }, [\n normalizedSessionState,\n onSessionStateChange,\n session?.sessionId,\n ]);\n\n React.useEffect(\n () => () => {\n onSessionStateChange?.(null);\n },\n [onSessionStateChange]\n );\n\n React.useEffect(() => {\n const sessionId = session?.sessionId;\n if (!sessionId || isOnrampTerminalState(normalizedSessionState)) return;\n\n let cancelled = false;\n let inFlight = false;\n\n const pollSession = async () => {\n if (inFlight) return;\n inFlight = true;\n try {\n const data = await fetchOnrampJson(\n baseUrl,\n `/api/v1/onramp/sessions/${encodeURIComponent(sessionId)}`,\n { method: \"GET\" }\n );\n if (cancelled) return;\n setSession((current) =>\n current?.sessionId === sessionId ? { ...current, ...data } : current\n );\n } catch (requestError) {\n if (cancelled) return;\n const message = getErrorMessage(requestError);\n setError(message);\n onError?.(message);\n } finally {\n inFlight = false;\n }\n };\n\n void pollSession();\n const interval = window.setInterval(pollSession, ONRAMP_SESSION_POLL_MS);\n return () => {\n cancelled = true;\n window.clearInterval(interval);\n };\n }, [\n baseUrl,\n normalizedSessionState,\n onError,\n session?.sessionId,\n ]);\n\n const createSession = async () => {\n if (!selectedQuote || !ownerAddress || !selectedPaymentMethod) return;\n setSessionLoading(true);\n setError(null);\n try {\n const returnUrl =\n typeof window !== \"undefined\"\n ? window.location.href\n : \"http://localhost:8000/onramp/complete\";\n const data = await fetchOnrampJson(\n baseUrl,\n \"/api/v1/onramp/sessions\",\n {\n body: JSON.stringify({\n countryCode,\n destinationCurrencyCode: selectedQuote.destinationCurrencyCode,\n paymentMethodType: selectedPaymentMethod,\n provider: selectedQuote.provider,\n returnUrl,\n sourceAmount: selectedQuote.sourceAmount,\n sourceCurrencyCode: selectedQuote.sourceCurrencyCode,\n walletAddress: ownerAddress,\n }),\n headers: {\n \"Idempotency-Key\": createIdempotencyKey(),\n },\n method: \"POST\",\n }\n );\n setSession(data);\n const widgetUrl = data.widgetUrl ?? data.fallbackWidgetUrl;\n if (widgetUrl && typeof window !== \"undefined\") {\n const opened = window.open(widgetUrl, \"_blank\", \"noopener,noreferrer\");\n if (!opened) {\n window.location.assign(widgetUrl);\n }\n }\n } catch (requestError) {\n const message = getErrorMessage(requestError);\n setError(message);\n onError?.(message);\n } finally {\n setSessionLoading(false);\n }\n };\n\n const resetSession = () => {\n setError(null);\n setSession(null);\n };\n\n const completeSession = () => {\n setError(null);\n setQuotes([]);\n setSelectedProvider(\"\");\n setSession(null);\n setSourceAmount(\"\");\n };\n\n const handleCurrencySelect = (currencyCode: string) => {\n setSourceCurrencyCode(currencyCode);\n setSelectedPaymentMethod(\"\");\n setSelectedProvider(\"\");\n setQuotes([]);\n setRoutes([]);\n setActiveSheet(null);\n };\n\n const handleDestinationTokenSelect = (token: SwapTokenOption) => {\n if (!isSameOnrampToken(token, toToken)) {\n onSelectDestinationToken?.(token);\n setSelectedPaymentMethod(\"\");\n setSelectedProvider(\"\");\n setQuotes([]);\n setRoutes([]);\n setSession(null);\n }\n setActiveSheet(null);\n };\n\n const handleMethodSelect = (method: string) => {\n setSelectedPaymentMethod(method);\n setSelectedProvider(\"\");\n setQuotes([]);\n setActiveSheet(null);\n };\n\n const receiveAmount = selectedQuote?.destinationAmount;\n const receiveUsd = selectedQuote?.destinationAmount;\n const feeTotal = selectedQuote?.fees?.total;\n const fetchingBestRates =\n hasPositiveSourceAmount &&\n !selectedQuote &&\n !amountLimitMessage &&\n !error &&\n Boolean(destinationCurrencyCode) &&\n (isLoading || Boolean(selectedPaymentMethod));\n const ctaRateLoading = fetchingBestRates || (hasPositiveSourceAmount && isLoading);\n const quoteDetailsLoading = fetchingBestRates;\n const shouldShowQuoteDetails =\n hasPositiveSourceAmount && (quoteDetailsLoading || Boolean(selectedQuote));\n const shouldShowQuoteTimer = Boolean(selectedQuote);\n const ctaDisabled =\n !ownerAddress ||\n !hasPositiveSourceAmount ||\n !selectedQuote ||\n Boolean(amountLimitMessage) ||\n ctaRateLoading ||\n sessionLoading;\n\n if (!ownerAddress) {\n return (\n
\n \n \n Connect wallet to continue\n
\n \n Your wallet address is required for the hosted onramp session.\n \n void onConnectWallet()}\n style={{\n backgroundColor: brand,\n border: \"none\",\n borderRadius: theme.radius.primaryButton,\n color: primaryButtonForeground,\n cursor: \"pointer\",\n fontFamily: theme.fonts.sans,\n fontSize: \"14px\",\n fontWeight: 500,\n height: \"40px\",\n paddingInline: \"16px\",\n }}\n type=\"button\"\n >\n Connect Wallet\n \n \n \n );\n }\n\n if (session?.sessionId) {\n return (\n void createSession()}\n opportunity={opportunity}\n primaryButtonForeground={primaryButtonForeground}\n quote={selectedQuote}\n session={session}\n sourceAmount={sourceAmount}\n sourceCurrencyCode={sourceCurrencyCode}\n toToken={toToken}\n />\n );\n }\n\n return (\n \n {shouldShowQuoteTimer && (\n \n \n {quotesLoading || routesLoading ? \"...\" : `${quoteRefreshSeconds}s`}\n \n )}\n\n
\n \n \n \n
You Pay
\n \n setSourceAmount(normalizeAmountInput(event.target.value))\n }\n placeholder=\"0\"\n style={{\n background: \"transparent\",\n border: \"none\",\n color: theme.colors.textStrong,\n fontFamily: theme.fonts.display,\n fontSize: \"31px\",\n fontWeight: 500,\n letterSpacing: \"0\",\n lineHeight: \"36px\",\n outline: \"none\",\n padding: 0,\n width: \"100%\",\n }}\n value={sourceAmount}\n />\n \n {selectedQuote\n ? `${formatUsdDisplay(sourceAmount)} USD`\n : hasPositiveSourceAmount\n ? `${formatUsdDisplay(sourceAmount)} USD`\n : \"Select currency\"}\n
\n \n setActiveSheet(\"currency\")}\n >\n \n \n {sourceCurrencyCode || \"---\"}\n \n \n \n\n {selectedPaymentMethodDetails?.limits && (\n \n \n \n Limits\n {selectedPaymentMethodDetails.limits.min\n ? ` · Min ${formatCurrencyAmount(\n selectedPaymentMethodDetails.limits.min,\n sourceCurrencyCode\n )}`\n : \"\"}\n {selectedPaymentMethodDetails.limits.max\n ? ` · Max ${formatCurrencyAmount(\n selectedPaymentMethodDetails.limits.max,\n sourceCurrencyCode\n )}`\n : \"\"}\n \n \n )}\n\n \n\n \n \n
Receive
\n \n {receiveAmount ? formatNumberDisplay(receiveAmount, 6) : \"0\"}\n \n \n {receiveAmount ? formatUsdDisplay(receiveUsd) : \"$0.00 USD\"}\n \n \n setActiveSheet(\"destination\")}\n >\n \n \n {toToken?.symbol ?? \"Token\"}\n \n \n \n \n \n\n {shouldShowQuoteDetails &&\n (quoteDetailsLoading ? (\n \n ) : selectedQuote ? (\n
\n setActiveSheet(\"method\")}\n />\n }\n label=\"Payment Method\"\n >\n
\n \n \n {getMethodLabel(selectedPaymentMethod)}\n
\n
\n \n\n setActiveSheet(\"partner\")}\n />\n }\n divider\n label=\"Payment Partner\"\n >\n
\n \n \n \n {getProviderLabel(selectedQuote.provider)}\n
\n \n Best available quote\n \n \n \n \n\n setActiveSheet(\"fees\")}\n style={{\n alignItems: \"center\",\n backgroundColor: \"transparent\",\n border: \"none\",\n borderTop: `1px solid ${theme.colors.divider}`,\n boxSizing: \"border-box\",\n cursor: \"pointer\",\n display: \"flex\",\n justifyContent: \"space-between\",\n padding: \"14px\",\n textAlign: \"left\",\n width: \"100%\",\n }}\n type=\"button\"\n >\n
\n \n Total Fees\n
\n \n Inclusive of fees\n \n \n \n \n {feeTotal\n ? formatCurrencyAmount(feeTotal, sourceCurrencyCode)\n : \"--\"}\n \n \n View breakdown\n \n \n \n \n ) : null)}\n\n {(error || amountLimitMessage || session?.state) && (\n \n {error ??\n amountLimitMessage ??\n `Onramp session ${session?.state?.toLowerCase() ?? \"created\"}.`}\n \n )}\n\n void createSession()}\n style={{\n alignItems: \"center\",\n backgroundColor: ctaDisabled ? theme.colors.surfaceCool : brand,\n border: \"none\",\n borderRadius: theme.radius.primaryButton,\n boxShadow: ctaDisabled ? \"none\" : theme.shadows.primaryButton,\n color: ctaDisabled ? theme.colors.muted : primaryButtonForeground,\n cursor: ctaDisabled ? \"default\" : \"pointer\",\n display: \"flex\",\n fontFamily: theme.fonts.sans,\n fontSize: \"14px\",\n fontWeight: 500,\n gap: \"8px\",\n height: \"44px\",\n justifyContent: \"center\",\n lineHeight: \"18px\",\n width: \"100%\",\n }}\n type=\"button\"\n >\n {sessionLoading || ctaRateLoading ? (\n \n ) : selectedQuote ? (\n \n ) : null}\n {sessionLoading\n ? \"Opening provider...\"\n : ctaRateLoading\n ? \"fetching best rates...\"\n : selectedQuote\n ? `Pay ${formatCurrencyAmount(sourceAmount, sourceCurrencyCode)}`\n : \"Enter amount\"}\n \n\n {activeSheet === \"fees\" && (\n setActiveSheet(null)} title={`Buying ${formatNumberDisplay(receiveAmount, 6)} ${toToken?.symbol ?? \"\"}`}>\n \n 1 {toToken?.symbol ?? \"token\"} ≈{\" \"}\n {selectedQuote && parseDecimal(selectedQuote.destinationAmount)?.gt(0)\n ? formatCurrencyAmount(\n parseDecimal(selectedQuote.sourceAmount)?.div(\n parseDecimal(selectedQuote.destinationAmount) ?? new Decimal(1)\n ),\n sourceCurrencyCode\n )\n : \"--\"}\n \n
\n {[\n [\"Provider fee\", selectedQuote?.fees?.provider],\n [\"Network fee\", selectedQuote?.fees?.network],\n [\"Partner fee\", selectedQuote?.fees?.partner],\n ].map(([label, value], index) => (\n 0 ? `1px solid ${theme.colors.divider}` : undefined,\n display: \"flex\",\n justifyContent: \"space-between\",\n padding: \"12px 14px\",\n }}\n >\n \n {label}\n \n \n {value ? formatCurrencyAmount(value, sourceCurrencyCode) : \"--\"}\n \n
\n ))}\n \n \n Total you pay\n \n \n {formatCurrencyAmount(sourceAmount, sourceCurrencyCode)}\n \n \n \n
\n )}\n\n {activeSheet === \"partner\" && (\n setActiveSheet(null)} title=\"Payment partner\">\n \n {filteredQuotes.length > 0 ? (\n
\n {filteredQuotes.map((quote) => {\n const quoteIndex = quotes.findIndex(\n (candidate) =>\n candidate.provider === quote.provider &&\n candidate.paymentMethodType === quote.paymentMethodType\n );\n return (\n }\n key={`${quote.provider}-${quote.paymentMethodType}`}\n onClick={() => {\n setSelectedProvider(quote.provider);\n setActiveSheet(null);\n }}\n primary={quoteIndex === 0}\n selected={quote.provider === selectedQuote?.provider}\n subtitle={`${getMethodLabel(quote.paymentMethodType)} · ${getMethodSubtitle(quote.paymentMethodType)}`}\n title={getProviderLabel(quote.provider)}\n value={`${formatNumberDisplay(quote.destinationAmount, 6)} ${toToken?.symbol ?? \"\"}`}\n />\n );\n })}\n
\n ) : (\n No payment partners found.\n )}\n
\n )}\n\n {activeSheet === \"currency\" && (\n setActiveSheet(null)} title=\"Select currency\">\n \n
Fiat currencies
\n {filteredFiatCurrencies.length > 0 ? (\n
\n {filteredFiatCurrencies.map((currencyCode) => (\n }\n key={currencyCode}\n onClick={() => handleCurrencySelect(currencyCode)}\n selected={currencyCode === sourceCurrencyCode}\n subtitle={getFiatCurrencyName(currencyCode)}\n title={currencyCode}\n />\n ))}\n
\n ) : (\n No currencies found.\n )}\n
\n )}\n\n {activeSheet === \"method\" && (\n setActiveSheet(null)} title=\"Payment method\">\n \n {filteredPaymentMethods.length > 0 ? (\n
\n {filteredPaymentMethods.map((method) => (\n }\n key={method.method}\n onClick={() => handleMethodSelect(method.method)}\n selected={method.method === selectedPaymentMethod}\n subtitle={getMethodSubtitle(method.method)}\n title={getMethodLabel(method.method)}\n />\n ))}\n
\n ) : (\n No payment methods found.\n )}\n
\n )}\n\n {activeSheet === \"destination\" && (\n setActiveSheet(null)} title=\"Select token\">\n \n {filteredDestinationTokens.length > 0 ? (\n
\n {filteredDestinationTokens.map((token) => (\n }\n key={getOnrampTokenKey(token)}\n onClick={() => handleDestinationTokenSelect(token)}\n selected={isSameOnrampToken(token, toToken)}\n subtitle={token.chainName}\n title={token.symbol}\n value={token.balanceInFiat}\n />\n ))}\n
\n ) : (\n No tokens found.\n )}\n
\n )}\n \n );\n}\n", + "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\nimport Decimal from \"decimal.js\";\nimport {\n Check,\n ChevronDown,\n CreditCard,\n ExternalLink,\n Info,\n Landmark,\n Loader2,\n Search,\n Smartphone,\n X,\n} from \"lucide-react\";\nimport React from \"react\";\nimport {\n erc20Abi,\n formatUnits,\n parseUnits,\n zeroAddress,\n type Address,\n type Hex,\n type PublicClient,\n} from \"viem\";\nimport { NEXUS_WIDGET_FAST_SPINNER_STYLE, nexusWidgetTheme } from \"../theme\";\nimport type { NexusWidgetDepositOpportunityConfig } from \"../types\";\nimport type { SwapTokenOption } from \"./swap-asset-selector\";\n\ntype OnrampCryptoCurrency = {\n chainCode?: string;\n chainId: number | string;\n contract?: string;\n currencyCode: string;\n decimals?: number;\n name?: string;\n symbolUrl?: string;\n token?: string;\n};\n\ntype OnrampCountry = {\n countryCode: string;\n flagUrl?: string;\n name: string;\n subdivisions?: unknown[];\n};\n\ntype OnrampFiatCurrency =\n | string\n | {\n code?: string;\n currencyCode?: string;\n flagUrl?: string;\n name?: string;\n symbol?: string;\n symbolUrl?: string;\n };\n\ntype OnrampFiatCurrencyOption = {\n currencyCode: string;\n flagUrl?: string;\n name?: string;\n symbol?: string;\n symbolUrl?: string;\n};\n\ntype OnrampOptionsResponse = {\n countries?: OnrampCountry[];\n selection?: {\n countryCode: string;\n cryptoCurrencies?: OnrampCryptoCurrency[];\n defaultFiat?: string;\n defaultPaymentMethods?: string[];\n fiatCurrencies?: OnrampFiatCurrency[];\n } | null;\n};\n\ntype IpCountryResponse = {\n country?: string;\n ip?: string;\n};\n\ntype OnrampErrorResponse = {\n code?: string;\n errorId?: string;\n message?: string;\n subcode?: string;\n};\n\ntype OnrampPaymentMethod = {\n description?: string;\n duration?: string;\n estimatedDuration?: string;\n estimatedTime?: string;\n limits?: {\n currencyCode?: string;\n max?: string;\n min?: string;\n };\n method: string;\n subtitle?: string;\n type?: string;\n};\n\ntype OnrampRoute = {\n paymentMethods?: OnrampPaymentMethod[];\n provider: string;\n};\n\ntype OnrampRoutesResponse = {\n routes?: OnrampRoute[];\n};\n\ntype OnrampProviderOption = {\n destinationAmount?: string;\n paymentMethod?: OnrampPaymentMethod;\n paymentMethodType?: string;\n provider: string;\n quote?: OnrampQuote;\n route?: OnrampRoute;\n};\n\ntype OnrampQuote = {\n destinationAmount: string;\n destinationCurrencyCode: string;\n fees?: {\n network?: string;\n partner?: string;\n provider?: string;\n total?: string;\n };\n lowKyc?: boolean;\n paymentMethodType: string;\n provider: string;\n rampScore?: number;\n sourceAmount: string;\n sourceCurrencyCode: string;\n};\n\ntype OnrampQuoteResponse = {\n quotes?: OnrampQuote[];\n};\n\ntype OnrampSessionResponse = {\n createdAt?: string;\n fallbackWidgetUrl?: string;\n paymentMethodType?: string;\n provider?: string;\n rawMeldStatus?: string;\n deposit?: {\n explorerUrl?: string;\n state?: string;\n txHash?: string;\n };\n sessionId?: string;\n state?: string;\n transaction?: {\n destinationAmount?: string;\n destinationCurrencyCode?: string;\n sourceAmount?: string;\n sourceCurrencyCode?: string;\n txHash?: string;\n walletAddress?: string;\n };\n updatedAt?: string;\n widgetUrl?: string;\n};\n\ntype OnrampCacheRecord = {\n expiresAt: number;\n value: T;\n};\n\ntype OnrampBlockedRequest = {\n key: string;\n message: string;\n};\n\ntype OnrampDepositExecutionState = {\n amount?: string;\n error?: string;\n explorerUrl?: string;\n skipped?: boolean;\n status: \"idle\" | \"running\" | \"success\" | \"failed\";\n txHash?: Hex;\n};\n\ntype OnrampNexusSDK = {\n execute: (params: any, options?: any) => Promise;\n};\n\ntype OnrampSheet =\n | \"currency\"\n | \"destination\"\n | \"fees\"\n | \"method\"\n | \"partner\"\n | null;\n\ninterface DepositOnrampFlowProps {\n baseUrl?: string;\n destinationTokens?: SwapTokenOption[];\n onConnectWallet: () => void | Promise;\n onError?: (message: string) => void;\n onSelectDestinationToken?: (token: SwapTokenOption) => void;\n onSessionStateChange?: (state: string | null) => void;\n nexusSDK?: OnrampNexusSDK | null;\n ownerAddress?: string;\n opportunity?: NexusWidgetDepositOpportunityConfig;\n primaryButtonForeground: string;\n publicClient?: PublicClient | null;\n toToken?: SwapTokenOption;\n}\n\nconst ONRAMP_CLIENT_HEADER = \"nexus-widgets\";\nconst ONRAMP_DEFAULT_BASE_URL = \"https://nexus-v2.canary.avail.so/middleware\";\nconst ONRAMP_RETURN_PATH = \"/onramp/complete\";\nconst ONRAMP_IP_COUNTRY_URL = \"https://api.country.is/\";\nconst ONRAMP_OPTIONS_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;\nconst ONRAMP_COUNTRY_CACHE_TTL_MS = 24 * 60 * 60 * 1000;\nconst ONRAMP_COUNTRY_CACHE_KEY = \"nexus-widgets:onramp:country:v1\";\nconst ONRAMP_OPTIONS_CACHE_KEY_PREFIX = \"nexus-widgets:onramp:options:v1\";\nconst ONRAMP_SANDBOX_FALLBACK_COUNTRY = \"FR\";\nconst ONRAMP_PRODUCTION_FALLBACK_COUNTRY = \"US\";\nconst QUOTE_REFRESH_SECONDS = 60;\nconst ONRAMP_SESSION_POLL_MS = 3000;\nconst ONRAMP_CALLBACK_MESSAGE_TYPE = \"nexus-widgets:onramp:session\";\nconst ONRAMP_CALLBACK_SUCCESS_MESSAGE = \"nexus-onramp-success\";\nconst ONRAMP_CALLBACK_CHANNEL = \"nexus-widgets:onramp\";\nconst ONRAMP_CALLBACK_STORAGE_KEY = \"nexus-widgets:onramp:session\";\nconst ONRAMP_PROGRESS_ARTWORK_URL =\n \"https://files.availproject.org/nexus-elements/nexus-one/progress-grid.gif\";\nconst ONRAMP_SHEET_EDGE_OFFSET = \"-16px\";\nconst theme = nexusWidgetTheme;\nconst brand = \"var(--foreground-brand)\";\n\nconst panelStyle: React.CSSProperties = {\n backgroundColor: theme.colors.surface,\n border: `1px solid ${theme.colors.border}`,\n borderRadius: \"12px\",\n boxShadow: theme.shadows.card,\n boxSizing: \"border-box\",\n overflow: \"hidden\",\n width: \"100%\",\n};\n\nconst sectionLabelStyle: React.CSSProperties = {\n color: theme.colors.textSubtle,\n fontFamily: theme.fonts.sans,\n fontSize: \"12px\",\n fontWeight: 600,\n letterSpacing: \"0.08em\",\n lineHeight: \"15px\",\n textTransform: \"uppercase\",\n};\n\nconst compactTitleStyle: React.CSSProperties = {\n color: theme.colors.textStrong,\n fontFamily: theme.fonts.display,\n fontSize: \"18px\",\n fontWeight: 500,\n letterSpacing: \"0\",\n lineHeight: \"23px\",\n};\n\nconst compactBodyStyle: React.CSSProperties = {\n color: theme.colors.textSubtle,\n fontFamily: theme.fonts.sans,\n fontSize: \"13px\",\n lineHeight: \"18px\",\n};\n\nconst parseDecimal = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = String(value).replace(/[^0-9.-]/g, \"\");\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst normalizeAmountInput = (raw: string) => {\n let next = raw.replaceAll(/[^0-9.]/g, \"\");\n const parts = next.split(\".\");\n if (parts.length > 2) next = parts[0] + \".\" + parts.slice(1).join(\"\");\n const [integerPart, decimalPart] = next.split(\".\");\n if (decimalPart !== undefined) {\n next = `${integerPart}.${decimalPart.slice(0, 2)}`;\n }\n if (next === \".\") next = \"0.\";\n return next;\n};\n\nconst formatNumberDisplay = (value: unknown, maxDecimals = 2) => {\n const parsed = parseDecimal(value) ?? new Decimal(0);\n return parsed\n .toDecimalPlaces(maxDecimals, Decimal.ROUND_DOWN)\n .toFixed()\n .replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n};\n\nconst formatCurrencyAmount = (value: unknown, currencyCode?: string) => {\n const suffix = currencyCode ? ` ${currencyCode}` : \"\";\n return `${formatNumberDisplay(value)}${suffix}`;\n};\n\nconst formatUsdDisplay = (value: unknown) => {\n const parsed = parseDecimal(value) ?? new Decimal(0);\n if (parsed.gt(0) && parsed.lt(0.01)) return \"<$0.01\";\n return `$${formatNumberDisplay(parsed, 2)}`;\n};\n\nconst getIntlCurrencyName = (currencyCode?: string) => {\n if (!currencyCode) return \"\";\n try {\n return (\n new Intl.DisplayNames([\"en\"], { type: \"currency\" }).of(\n currencyCode.toUpperCase(),\n ) ?? currencyCode\n );\n } catch {\n return currencyCode;\n }\n};\n\nconst getIntlCurrencySymbol = (currencyCode?: string) => {\n if (!currencyCode) return undefined;\n try {\n return new Intl.NumberFormat(\"en\", {\n currency: currencyCode.toUpperCase(),\n currencyDisplay: \"narrowSymbol\",\n style: \"currency\",\n })\n .formatToParts(0)\n .find((part) => part.type === \"currency\")?.value;\n } catch {\n return undefined;\n }\n};\n\nconst getFiatCurrencyCode = (currency: OnrampFiatCurrency) =>\n (typeof currency === \"string\"\n ? currency\n : (currency.currencyCode ?? currency.code ?? \"\")\n ).toUpperCase();\n\nconst getFiatCurrencyName = (\n currencyCode?: string,\n currency?: OnrampFiatCurrencyOption,\n) => currency?.name ?? getIntlCurrencyName(currencyCode);\n\nconst getCountryByCode = (\n countries: OnrampCountry[] | undefined,\n countryCode?: string,\n) =>\n countries?.find(\n (country) =>\n country.countryCode.toUpperCase() === countryCode?.toUpperCase(),\n );\n\nconst isCountryInOptionsList = (\n options: OnrampOptionsResponse,\n countryCode: string,\n) => {\n if (!options.countries?.length) return true;\n return Boolean(getCountryByCode(options.countries, countryCode));\n};\n\nconst getCountryFlagUrl = (\n countries: OnrampCountry[] | undefined,\n countryCode?: string,\n) => {\n const normalizedCountryCode = countryCode?.toUpperCase();\n if (!normalizedCountryCode) return undefined;\n return (\n getCountryByCode(countries, normalizedCountryCode)?.flagUrl ??\n `https://flagsapi.com/${normalizedCountryCode}/flat/64.png`\n );\n};\n\nconst getFiatCurrencyOptions = (\n options: OnrampOptionsResponse | null,\n): OnrampFiatCurrencyOption[] => {\n const selection = options?.selection;\n const selectedCountryFlagUrl = getCountryFlagUrl(\n options?.countries,\n selection?.countryCode,\n );\n const byCode = new Map();\n\n for (const currency of selection?.fiatCurrencies ?? []) {\n const currencyCode = getFiatCurrencyCode(currency);\n if (!currencyCode || byCode.has(currencyCode)) continue;\n const isDefaultFiat =\n currencyCode === selection?.defaultFiat?.toUpperCase();\n byCode.set(currencyCode, {\n currencyCode,\n flagUrl:\n typeof currency === \"string\"\n ? isDefaultFiat\n ? selectedCountryFlagUrl\n : undefined\n : (currency.flagUrl ??\n currency.symbolUrl ??\n (isDefaultFiat ? selectedCountryFlagUrl : undefined)),\n name:\n typeof currency === \"string\"\n ? getIntlCurrencyName(currencyCode)\n : (currency.name ?? getIntlCurrencyName(currencyCode)),\n symbol:\n typeof currency === \"string\"\n ? getIntlCurrencySymbol(currencyCode)\n : (currency.symbol ?? getIntlCurrencySymbol(currencyCode)),\n symbolUrl: typeof currency === \"string\" ? undefined : currency.symbolUrl,\n });\n }\n\n const defaultFiat = selection?.defaultFiat?.toUpperCase();\n if (defaultFiat && !byCode.has(defaultFiat)) {\n byCode.set(defaultFiat, {\n currencyCode: defaultFiat,\n flagUrl: selectedCountryFlagUrl,\n name: getIntlCurrencyName(defaultFiat),\n symbol: getIntlCurrencySymbol(defaultFiat),\n });\n }\n\n return Array.from(byCode.values());\n};\n\nconst readOnrampCache = (key: string) => {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = window.localStorage.getItem(key);\n if (!raw) return null;\n const cached = JSON.parse(raw) as OnrampCacheRecord;\n if (!cached?.expiresAt || cached.expiresAt <= Date.now()) {\n window.localStorage.removeItem(key);\n return null;\n }\n return cached.value;\n } catch {\n return null;\n }\n};\n\nconst writeOnrampCache = (key: string, value: T, ttlMs: number) => {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(\n key,\n JSON.stringify({\n expiresAt: Date.now() + ttlMs,\n value,\n } satisfies OnrampCacheRecord),\n );\n } catch {\n // Cache failures should not affect the onramp flow.\n }\n};\n\nconst getOnrampOptionsCacheKey = (baseUrl: string, countryCode: string) =>\n `${ONRAMP_OPTIONS_CACHE_KEY_PREFIX}:${baseUrl}:${countryCode.toUpperCase()}`;\n\nconst readCachedOnrampOptions = (baseUrl: string, countryCode: string) =>\n readOnrampCache(\n getOnrampOptionsCacheKey(baseUrl, countryCode),\n );\n\nconst writeCachedOnrampOptions = (\n baseUrl: string,\n countryCode: string,\n options: OnrampOptionsResponse,\n) => {\n writeOnrampCache(\n getOnrampOptionsCacheKey(baseUrl, countryCode),\n options,\n ONRAMP_OPTIONS_CACHE_TTL_MS,\n );\n};\n\nconst getOnrampEnvironmentValue = (key: string) => {\n if (typeof process === \"undefined\") return \"\";\n return process.env?.[key]?.trim() ?? \"\";\n};\n\nconst normalizeOnrampBaseUrl = (baseUrl: string) => baseUrl.replace(/\\/+$/, \"\");\n\nconst getOnrampBaseUrl = () =>\n normalizeOnrampBaseUrl(\n getOnrampEnvironmentValue(\"NEXT_PUBLIC_NEXUS_ONRAMP_BASE_URL\") ||\n ONRAMP_DEFAULT_BASE_URL,\n );\n\nconst getOnrampReturnUrl = () =>\n getOnrampEnvironmentValue(\"NEXT_PUBLIC_NEXUS_ONRAMP_RETURN_URL\") ||\n (typeof window !== \"undefined\" && window.location.origin\n ? `${window.location.origin}${ONRAMP_RETURN_PATH}`\n : ONRAMP_RETURN_PATH);\n\nconst getOnrampRuntimeEnvironment = (baseUrl: string) => {\n const explicitEnvironment =\n getOnrampEnvironmentValue(\"NEXT_PUBLIC_NEXUS_ONRAMP_ENV\") ||\n getOnrampEnvironmentValue(\"NEXT_PUBLIC_ONRAMP_ENV\") ||\n getOnrampEnvironmentValue(\"NEXT_PUBLIC_NEXUS_ENV\") ||\n getOnrampEnvironmentValue(\"NEXT_PUBLIC_VERCEL_ENV\") ||\n getOnrampEnvironmentValue(\"VERCEL_ENV\");\n\n if (explicitEnvironment) return explicitEnvironment.toLowerCase();\n\n const normalizedBaseUrl = baseUrl.toLowerCase();\n if (\n normalizedBaseUrl.includes(\"canary\") ||\n normalizedBaseUrl.includes(\"localhost\") ||\n normalizedBaseUrl.includes(\"127.0.0.1\")\n ) {\n return \"sandbox\";\n }\n\n return \"production\";\n};\n\nconst getUnsupportedCountryFallbackCode = (baseUrl: string) => {\n const environment = getOnrampRuntimeEnvironment(baseUrl);\n return environment === \"production\" || environment === \"prod\"\n ? ONRAMP_PRODUCTION_FALLBACK_COUNTRY\n : ONRAMP_SANDBOX_FALLBACK_COUNTRY;\n};\n\nconst getIpCountryCode = async () => {\n try {\n const response = await fetch(ONRAMP_IP_COUNTRY_URL, {\n headers: { Accept: \"application/json\" },\n method: \"GET\",\n });\n if (!response.ok) return \"\";\n const data = (await response.json()) as IpCountryResponse;\n return data.country?.toUpperCase() ?? \"\";\n } catch {\n return \"\";\n }\n};\n\nconst getLocalCountryCode = () => {\n if (typeof navigator === \"undefined\") return \"\";\n const locales = [...(navigator.languages ?? []), navigator.language].filter(\n Boolean,\n );\n for (const locale of locales) {\n const countryCode = locale.match(/[-_]([A-Za-z]{2})$/)?.[1]?.toUpperCase();\n if (countryCode) return countryCode;\n }\n return \"\";\n};\n\nconst resolveOnrampCountryCode = async () => {\n const cached = readOnrampCache(ONRAMP_COUNTRY_CACHE_KEY);\n if (cached) return cached;\n\n const resolved = (await getIpCountryCode()) || getLocalCountryCode() || \"US\";\n writeOnrampCache(\n ONRAMP_COUNTRY_CACHE_KEY,\n resolved,\n ONRAMP_COUNTRY_CACHE_TTL_MS,\n );\n return resolved;\n};\n\nconst getDefaultFiatCurrencyCode = (options: OnrampOptionsResponse | null) => {\n const currencies = getFiatCurrencyOptions(options);\n const defaultFiat = options?.selection?.defaultFiat?.toUpperCase();\n if (\n defaultFiat &&\n currencies.some((currency) => currency.currencyCode === defaultFiat)\n ) {\n return defaultFiat;\n }\n return currencies[0]?.currencyCode ?? \"\";\n};\n\nconst getMethodLabel = (method?: string) => {\n switch ((method ?? \"\").toUpperCase()) {\n case \"APPLE_PAY\":\n return \"Apple Pay\";\n case \"GOOGLE_PAY\":\n return \"Google Pay\";\n case \"UPI\":\n return \"UPI\";\n case \"BANK_TRANSFER\":\n case \"IMPS\":\n case \"NEFT\":\n case \"RTGS\":\n return \"Bank Transfer\";\n case \"CREDIT_DEBIT_CARD\":\n case \"CARD\":\n return \"Credit / Debit Cards\";\n default:\n return method\n ? method\n .split(\"_\")\n .filter(Boolean)\n .map(\n (part) =>\n part.charAt(0).toUpperCase() + part.slice(1).toLowerCase(),\n )\n .join(\" \")\n : \"Payment method\";\n }\n};\n\nconst getMethodSubtitle = (method?: OnrampPaymentMethod) => {\n const subtitle =\n method?.subtitle ??\n method?.description ??\n method?.estimatedTime ??\n method?.estimatedDuration ??\n method?.duration;\n return typeof subtitle === \"string\" && subtitle.trim()\n ? subtitle.trim()\n : undefined;\n};\n\nconst getProviderLabel = (provider?: string) =>\n provider\n ? provider\n .split(/[_\\s-]+/)\n .filter(Boolean)\n .map(\n (part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase(),\n )\n .join(\" \")\n : \"Payment partner\";\n\nconst getProviderInitials = (provider?: string) => {\n const label = getProviderLabel(provider);\n const parts = label.split(/\\s+/).filter(Boolean);\n if (parts.length <= 1) return label.slice(0, 2).toUpperCase();\n return `${parts[0]?.[0] ?? \"\"}${parts[1]?.[0] ?? \"\"}`.toUpperCase();\n};\n\nclass OnrampRequestError extends Error {\n code?: string;\n errorId?: string;\n rawMessage?: string;\n status: number;\n subcode?: string;\n\n constructor({\n code,\n errorId,\n message,\n rawMessage,\n status,\n subcode,\n }: {\n code?: string;\n errorId?: string;\n message: string;\n rawMessage?: string;\n status: number;\n subcode?: string;\n }) {\n super(message);\n this.name = \"OnrampRequestError\";\n this.code = code;\n this.errorId = errorId;\n this.rawMessage = rawMessage;\n this.status = status;\n this.subcode = subcode;\n }\n}\n\nconst getErrorMessage = (error: unknown) => {\n if (error instanceof Error) return error.message;\n if (typeof error === \"string\") return error;\n return \"Unable to continue with local currency.\";\n};\n\nconst getOnrampErrorSignature = (error: OnrampRequestError) =>\n [error.code, error.subcode, error.rawMessage, error.message]\n .filter(Boolean)\n .join(\" \")\n .toUpperCase();\n\nconst isUnsupportedOnrampDestinationError = (error: unknown) => {\n if (!(error instanceof OnrampRequestError)) return false;\n const signature = getOnrampErrorSignature(error);\n return (\n signature.includes(\"ASSET_NOT_SUPPORTED\") ||\n signature.includes(\"CURRENCY_NOT_SUPPORTED\") ||\n signature.includes(\"TOKEN_NOT_SUPPORTED\") ||\n signature.includes(\"TOKEN UNSUPPORTED\") ||\n signature.includes(\"UNSUPPORTED_DESTINATION\") ||\n signature.includes(\"UNSUPPORTED TOKEN\") ||\n signature.includes(\"DESTINATION ASSET IS NOT SUPPORTED\") ||\n signature.includes(\"DESTINATIONCHAINID IS INVALID\") ||\n signature.includes(\"DESTINATIONTOKEN IS INVALID\")\n );\n};\n\nconst isOnrampConfigError = (error: unknown) => {\n if (!(error instanceof OnrampRequestError)) return false;\n const signature = getOnrampErrorSignature(error);\n return (\n signature.includes(\"CONFIG\") ||\n signature.includes(\"CONFIGURATION\") ||\n signature.includes(\"NOT CONFIGURED\") ||\n signature.includes(\"NO ROUTE\") ||\n (signature.includes(\"ROUTE\") && signature.includes(\"UNAVAILABLE\"))\n );\n};\n\nconst isTerminalOnrampRateError = (error: unknown) =>\n isUnsupportedOnrampDestinationError(error) || isOnrampConfigError(error);\n\nconst getOnrampRequestErrorMessage = (\n error: unknown,\n context?: {\n countryCode?: string;\n sourceCurrencyCode?: string;\n token?: SwapTokenOption;\n },\n) => {\n if (isTerminalOnrampRateError(error)) {\n const tokenLabel = context?.token?.symbol\n ? `${context.token.symbol}${\n context.token.chainName ? ` on ${context.token.chainName}` : \"\"\n }`\n : \"this token\";\n const localeLabel = [context?.countryCode, context?.sourceCurrencyCode]\n .filter(Boolean)\n .join(\" / \");\n return `Local currency deposits are not available for ${tokenLabel}${\n localeLabel ? ` with ${localeLabel}` : \"\"\n }. Choose another deposit token or pay with wallet.`;\n }\n\n return getErrorMessage(error);\n};\n\nconst getOnrampRateRequestKey = ({\n countryCode,\n destinationChainId,\n destinationCurrencyCode,\n destinationToken,\n sourceCurrencyCode,\n}: {\n countryCode?: string;\n destinationChainId?: string;\n destinationCurrencyCode?: string;\n destinationToken?: string;\n sourceCurrencyCode?: string;\n}) =>\n [\n countryCode?.toUpperCase() ?? \"\",\n sourceCurrencyCode?.toUpperCase() ?? \"\",\n destinationCurrencyCode?.toUpperCase() ?? \"\",\n destinationChainId ?? \"\",\n destinationToken?.toLowerCase() ?? \"\",\n ].join(\"|\");\n\nconst ONRAMP_TERMINAL_STATES = new Set([\n \"CANCELLED\",\n \"EXPIRED\",\n \"FAILED\",\n \"REFUNDED\",\n \"SETTLED\",\n]);\n\nconst getNormalizedOnrampState = (state?: string | null) =>\n (state ?? \"\").trim().toUpperCase();\n\nconst isOnrampTerminalState = (state?: string | null) =>\n ONRAMP_TERMINAL_STATES.has(getNormalizedOnrampState(state)) ||\n isOnrampDepositSuccessState(state) ||\n isOnrampDepositFailedState(state);\n\nconst isOnrampProcessingState = (state?: string | null) => {\n const normalized = getNormalizedOnrampState(state);\n return normalized === \"PROCESSING\" || normalized === \"SETTLING\";\n};\n\nconst ONRAMP_DEPOSIT_PROCESSING_STATES = new Set([\n \"COMPLETING_DEPOSIT\",\n \"DEPOSIT_PROCESSING\",\n \"DEPOSITING\",\n]);\n\nconst ONRAMP_DEPOSIT_SUCCESS_STATES = new Set([\n \"COMPLETED\",\n \"DEPOSIT_COMPLETE\",\n \"DEPOSIT_SUCCESS\",\n \"DEPOSITED\",\n]);\n\nconst ONRAMP_DEPOSIT_FAILED_STATES = new Set([\n \"DEPOSIT_ATTENTION\",\n \"DEPOSIT_FAILED\",\n \"DEPOSIT_REQUIRES_ATTENTION\",\n]);\n\nconst isOnrampDepositProcessingState = (state?: string | null) =>\n ONRAMP_DEPOSIT_PROCESSING_STATES.has(getNormalizedOnrampState(state));\n\nconst isOnrampDepositSuccessState = (state?: string | null) =>\n ONRAMP_DEPOSIT_SUCCESS_STATES.has(getNormalizedOnrampState(state));\n\nconst isOnrampDepositFailedState = (state?: string | null) =>\n ONRAMP_DEPOSIT_FAILED_STATES.has(getNormalizedOnrampState(state));\n\nconst isNativeAddress = (address?: string) => {\n const lower = address?.toLowerCase();\n return (\n lower === zeroAddress ||\n lower === \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\"\n );\n};\n\nconst isPositiveGasLimit = (value: unknown): value is bigint => {\n try {\n return BigInt(value as bigint) > BigInt(0);\n } catch {\n return false;\n }\n};\n\nconst getOnrampChainIdNumber = (chainId?: number | string) => {\n if (typeof chainId === \"number\") return chainId;\n const match = chainId?.match(/\\d+$/);\n return match ? Number(match[0]) : undefined;\n};\n\nconst getOnrampCurrencyAddress = (currency?: OnrampCryptoCurrency) =>\n currency?.contract ?? currency?.token;\n\nconst getOnrampCryptoCurrency = (\n options: OnrampOptionsResponse | null,\n token?: SwapTokenOption,\n) => {\n if (!token?.chainId || !token.contractAddress) return undefined;\n\n const targetAddress = token.contractAddress.toLowerCase();\n return options?.selection?.cryptoCurrencies?.find((currency) => {\n if (getOnrampChainIdNumber(currency.chainId) !== token.chainId)\n return false;\n const address = getOnrampCurrencyAddress(currency)?.toLowerCase();\n if (!address) return false;\n return (\n address === targetAddress ||\n (isNativeAddress(address) && isNativeAddress(targetAddress))\n );\n });\n};\n\nconst getDestinationRequestDetails = (\n options: OnrampOptionsResponse | null,\n token?: SwapTokenOption,\n) => {\n const matched = getOnrampCryptoCurrency(options, token);\n return {\n destinationChainId: token?.chainId ? `EVM_${token.chainId}` : undefined,\n destinationCurrencyCode:\n matched?.currencyCode ?? token?.symbol?.toUpperCase() ?? \"\",\n destinationToken: token?.contractAddress,\n };\n};\n\nconst getRawTokenAmount = (amount: unknown, decimals: number) => {\n const parsed = parseDecimal(amount);\n if (!parsed?.gt(0)) return null;\n return parseUnits(\n parsed.toDecimalPlaces(decimals, Decimal.ROUND_DOWN).toFixed(),\n decimals,\n );\n};\n\nconst getTransactionExplorerUrl = (chainId?: number, txHash?: string) => {\n const baseUrl = getExplorerBaseUrl(chainId);\n return baseUrl && txHash ? `${baseUrl}${txHash}` : undefined;\n};\n\nconst getSandboxDepositAmountRaw = (decimals: number) =>\n BigInt(new Decimal(0.1).mul(Decimal.pow(10, decimals)).toFixed());\n\nconst getSdkExecuteTransactionHash = (result: any): Hex | undefined => {\n const candidates = [\n result?.execute?.txHash,\n result?.execute?.transactionHash,\n result?.execute?.hash,\n result?.executeResponse?.txHash,\n result?.executeResponse?.transactionHash,\n result?.executeResponse?.hash,\n result?.receipt?.transactionHash,\n result?.txHash,\n result?.transactionHash,\n result?.hash,\n ];\n\n return candidates.find(\n (candidate): candidate is Hex =>\n typeof candidate === \"string\" && candidate.startsWith(\"0x\"),\n );\n};\n\nconst getSdkExecuteExplorerUrl = (result: any): string | undefined => {\n const candidates = [\n result?.execute?.explorerUrl,\n result?.execute?.explorerURL,\n result?.execute?.txExplorerUrl,\n result?.execute?.transactionExplorerUrl,\n result?.executeResponse?.explorerUrl,\n result?.executeResponse?.explorerURL,\n result?.executeResponse?.txExplorerUrl,\n result?.executeResponse?.transactionExplorerUrl,\n result?.explorerUrl,\n result?.explorerURL,\n result?.txExplorerUrl,\n result?.transactionExplorerUrl,\n ];\n\n return candidates.find(\n (candidate): candidate is string =>\n typeof candidate === \"string\" && candidate.length > 0,\n );\n};\n\nconst getOnrampTokenKey = (token?: SwapTokenOption) => {\n if (!token?.chainId || !token.contractAddress) return \"\";\n return `${token.chainId}:${token.contractAddress.toLowerCase()}`;\n};\n\nconst isSameOnrampToken = (left?: SwapTokenOption, right?: SwapTokenOption) =>\n Boolean(\n left && right && getOnrampTokenKey(left) === getOnrampTokenKey(right),\n );\n\nconst sortQuotes = (quotes: OnrampQuote[]) =>\n [...quotes].sort((a, b) => {\n const scoreDelta = (b.rampScore ?? 0) - (a.rampScore ?? 0);\n if (scoreDelta !== 0) return scoreDelta;\n const destinationDelta =\n parseDecimal(b.destinationAmount)?.cmp(\n parseDecimal(a.destinationAmount) ?? new Decimal(0),\n ) ?? 0;\n return destinationDelta;\n });\n\nconst matchesSearch = (query: string, values: Array) => {\n const normalizedQuery = query.trim().toLowerCase();\n if (!normalizedQuery) return true;\n return values.some((value) =>\n (value ?? \"\").toLowerCase().includes(normalizedQuery),\n );\n};\n\nconst createIdempotencyKey = () => {\n if (typeof crypto !== \"undefined\" && \"randomUUID\" in crypto) {\n return crypto.randomUUID();\n }\n return `${Date.now()}-${Math.random().toString(16).slice(2)}`;\n};\n\nconst openOnrampProviderWindow = () => {\n if (typeof window === \"undefined\") return null;\n const providerWindow = window.open(\"about:blank\", \"_blank\");\n if (!providerWindow) return null;\n providerWindow.document.title = \"Opening payment provider\";\n providerWindow.document.body.style.fontFamily =\n \"system-ui, -apple-system, BlinkMacSystemFont, sans-serif\";\n providerWindow.document.body.style.margin = \"0\";\n providerWindow.document.body.style.display = \"grid\";\n providerWindow.document.body.style.minHeight = \"100vh\";\n providerWindow.document.body.style.placeItems = \"center\";\n providerWindow.document.body.textContent = \"Opening payment provider...\";\n return providerWindow;\n};\n\ntype OnrampCallbackPayload = {\n session?: OnrampSessionResponse;\n sessionId: string;\n state?: string;\n timestamp?: number;\n type: string;\n};\n\ndeclare global {\n interface Window {\n setRampSessionId?: (sessionId: string) => void;\n }\n}\n\nconst isOnrampCallbackPayload = (\n payload: unknown,\n): payload is OnrampCallbackPayload => {\n if (!payload || typeof payload !== \"object\") return false;\n const candidate = payload as Partial;\n return (\n candidate.type === ONRAMP_CALLBACK_MESSAGE_TYPE &&\n typeof candidate.sessionId === \"string\" &&\n candidate.sessionId.length > 0\n );\n};\n\nconst fetchOnrampJson = async (\n baseUrl: string,\n path: string,\n init?: RequestInit,\n) => {\n const response = await fetch(`${baseUrl}${path}`, {\n ...init,\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-nexus-client\": ONRAMP_CLIENT_HEADER,\n ...(init?.headers ?? {}),\n },\n });\n const text = await response.text();\n let data: any = {};\n try {\n data = text ? JSON.parse(text) : {};\n } catch {\n data = text ? { message: text } : {};\n }\n\n if (!response.ok) {\n const errorData = data as OnrampErrorResponse;\n const baseMessage =\n typeof errorData?.message === \"string\"\n ? errorData.message\n : `Onramp request failed (${response.status})`;\n const details = [errorData?.subcode, errorData?.errorId]\n .filter(Boolean)\n .join(\" · \");\n const message = details ? `${baseMessage} (${details})` : baseMessage;\n throw new OnrampRequestError({\n code: errorData?.code,\n errorId: errorData?.errorId,\n message,\n rawMessage: baseMessage,\n status: response.status,\n subcode: errorData?.subcode,\n });\n }\n\n return data as T;\n};\n\nfunction TokenLogo({\n label,\n size = 30,\n src,\n}: {\n label?: string;\n size?: number;\n src?: string;\n}) {\n const [failed, setFailed] = React.useState(!src);\n\n React.useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n backgroundColor: theme.colors.surface,\n borderRadius: \"999px\",\n height: size,\n objectFit: \"cover\",\n width: size,\n }}\n />\n );\n }\n\n return (\n \n {(label || \"?\").slice(0, 2).toUpperCase()}\n \n );\n}\n\nfunction TokenLogoPair({ token }: { token?: SwapTokenOption }) {\n return (\n \n \n {token?.chainLogo && (\n \n \n \n )}\n \n );\n}\n\nfunction CurrencyMark({\n code,\n currency,\n}: {\n code?: string;\n currency?: OnrampFiatCurrencyOption;\n}) {\n const displayCode = currency?.currencyCode ?? code;\n const imageUrl = currency?.flagUrl ?? currency?.symbolUrl;\n\n if (imageUrl) {\n return ;\n }\n\n return (\n \n {(currency?.symbol ?? displayCode ?? \"?\").slice(0, 3).toUpperCase()}\n \n );\n}\n\nfunction ProviderMark({ provider }: { provider?: string }) {\n return (\n \n {getProviderInitials(provider)}\n \n );\n}\n\nfunction MethodMark({ method }: { method?: string }) {\n const normalized = (method ?? \"\").toUpperCase();\n const icon =\n normalized === \"UPI\" ||\n normalized === \"APPLE_PAY\" ||\n normalized === \"GOOGLE_PAY\" ? (\n \n ) : normalized.includes(\"BANK\") ||\n normalized === \"IMPS\" ||\n normalized === \"NEFT\" ||\n normalized === \"RTGS\" ? (\n \n ) : (\n \n );\n\n return (\n \n {icon}\n \n );\n}\n\nfunction SelectPill({\n children,\n disabled,\n onClick,\n}: {\n children: React.ReactNode;\n disabled?: boolean;\n onClick?: () => void;\n}) {\n return (\n \n {children}\n {!disabled && (\n \n )}\n \n );\n}\n\nfunction EditButton({\n disabled,\n onClick,\n}: {\n disabled?: boolean;\n onClick: () => void;\n}) {\n return (\n \n Edit\n \n );\n}\n\nfunction DetailRow({\n action,\n children,\n divider,\n label,\n}: {\n action?: React.ReactNode;\n children: React.ReactNode;\n divider?: boolean;\n label: string;\n}) {\n return (\n \n \n
{label}
\n {action}\n \n {children}\n \n );\n}\n\nfunction Sheet({\n children,\n onClose,\n title,\n}: {\n children: React.ReactNode;\n onClose: () => void;\n title: string;\n}) {\n return (\n \n \n \n \n
{title}
\n \n \n \n \n \n {children}\n \n \n \n );\n}\n\nfunction SheetSearchInput({\n onChange,\n placeholder,\n value,\n}: {\n onChange: (value: string) => void;\n placeholder: string;\n value: string;\n}) {\n const [focused, setFocused] = React.useState(false);\n return (\n \n \n setFocused(false)}\n onChange={(event) => onChange(event.target.value)}\n onFocus={() => setFocused(true)}\n placeholder={placeholder}\n style={{\n backgroundColor: \"transparent\",\n border: \"none\",\n color: theme.colors.textStrong,\n flex: \"1 1 0%\",\n fontFamily: theme.fonts.sans,\n fontSize: \"14px\",\n lineHeight: \"18px\",\n minWidth: 0,\n outline: \"none\",\n padding: 0,\n }}\n value={value}\n />\n {value && (\n onChange(\"\")}\n style={{\n alignItems: \"center\",\n backgroundColor: \"transparent\",\n border: \"none\",\n cursor: \"pointer\",\n display: \"flex\",\n padding: \"2px\",\n }}\n type=\"button\"\n >\n \n \n )}\n \n );\n}\n\nfunction SelectRow({\n icon,\n onClick,\n primary,\n selected,\n subtitle,\n title,\n value,\n}: {\n icon: React.ReactNode;\n onClick: () => void;\n primary?: boolean;\n selected: boolean;\n subtitle?: string;\n title: string;\n value?: string;\n}) {\n return (\n \n {icon}\n \n \n \n {title}\n \n {primary && (\n \n Best rate\n \n )}\n \n {subtitle && (\n \n {subtitle}\n \n )}\n \n {value && (\n \n {value}\n \n )}\n \n {selected && (\n \n )}\n \n \n );\n}\n\nfunction EmptySheetMessage({ children }: { children: React.ReactNode }) {\n return (\n \n {children}\n \n );\n}\n\nfunction SkeletonBlock({\n borderRadius = \"8px\",\n height,\n width,\n}: {\n borderRadius?: string;\n height: string;\n width: string;\n}) {\n return (\n \n );\n}\n\nfunction QuoteDetailsSkeleton({ showFees = true }: { showFees?: boolean }) {\n return (\n
\n \n }\n label=\"Payment Method\"\n >\n
\n \n \n
\n \n\n \n }\n divider\n label=\"Payment Partner\"\n >\n
\n \n
\n \n \n
\n
\n \n\n {showFees && (\n \n
\n \n \n
\n
\n \n \n
\n
\n )}\n \n );\n}\n\nconst formatTokenAmountDisplay = (amount: unknown, symbol?: string) => {\n const suffix = symbol ? ` ${symbol}` : \"\";\n if (amount === null || amount === undefined || amount === \"\") {\n return suffix ? `--${suffix}` : \"--\";\n }\n return `${formatNumberDisplay(amount, 6)}${suffix}`;\n};\n\nconst getOnrampSessionSubtitle = (\n state: string | null | undefined,\n opportunity?: NexusWidgetDepositOpportunityConfig,\n) => {\n const normalized = getNormalizedOnrampState(state);\n if (normalized === \"SETTLED\" || isOnrampDepositSuccessState(normalized)) {\n return `The amount was deposited on ${getDepositTargetLabel(opportunity)}`;\n }\n if (isOnrampDepositFailedState(normalized)) {\n return \"The funds are in your wallet, but the deposit transaction could not be performed automatically.\";\n }\n if (isOnrampDepositProcessingState(normalized)) {\n return \"Usually takes 20 seconds\";\n }\n if (normalized === \"FAILED\") return \"The payment could not be completed\";\n if (normalized === \"CANCELLED\") return \"No payment was completed\";\n if (normalized === \"REFUNDED\") return \"The payment was refunded\";\n if (normalized === \"EXPIRED\") return \"Start a new payment to continue\";\n if (isOnrampProcessingState(normalized)) return \"Usually takes 2 - 5 min\";\n return \"The payment processor requires some information\";\n};\n\nconst getOnrampSummaryLabel = (\n state: string | null | undefined,\n provider?: string,\n) => {\n const partner = getProviderLabel(provider);\n const normalized = getNormalizedOnrampState(state);\n if (normalized === \"SETTLED\") return `Payment received from ${partner}`;\n if (normalized === \"FAILED\") return `Payment failed with ${partner}`;\n if (normalized === \"CANCELLED\") return `Payment cancelled with ${partner}`;\n if (normalized === \"REFUNDED\") return `Payment refunded by ${partner}`;\n if (normalized === \"EXPIRED\") return `Payment expired with ${partner}`;\n if (isOnrampProcessingState(normalized)) {\n return `Payment to be received by ${partner}`;\n }\n return `Payment to receive from ${partner}`;\n};\n\nconst getDepositTargetLabel = (\n opportunity?: NexusWidgetDepositOpportunityConfig,\n) =>\n opportunity?.title ||\n opportunity?.label ||\n opportunity?.protocol ||\n \"the selected market\";\n\nconst getDepositChainName = (\n opportunity?: NexusWidgetDepositOpportunityConfig,\n token?: SwapTokenOption,\n) =>\n token?.chainName ?? opportunity?.subtitle?.replace(/^on\\s+/i, \"\") ?? \"chain\";\n\nconst getExplorerBaseUrl = (chainId?: number) => {\n switch (chainId) {\n case 1:\n return \"https://etherscan.io/tx/\";\n case 10:\n return \"https://optimistic.etherscan.io/tx/\";\n case 56:\n return \"https://bscscan.com/tx/\";\n case 137:\n return \"https://polygonscan.com/tx/\";\n case 8453:\n return \"https://basescan.org/tx/\";\n case 42161:\n return \"https://arbiscan.io/tx/\";\n case 43114:\n return \"https://snowtrace.io/tx/\";\n default:\n return undefined;\n }\n};\n\nconst getOnrampExplorerUrl = (\n session: OnrampSessionResponse,\n opportunity?: NexusWidgetDepositOpportunityConfig,\n token?: SwapTokenOption,\n) => {\n if (session.deposit?.explorerUrl) return session.deposit.explorerUrl;\n if (opportunity?.explorerUrl) return opportunity.explorerUrl;\n const txHash = session.deposit?.txHash ?? session.transaction?.txHash;\n const baseUrl = getExplorerBaseUrl(token?.chainId ?? opportunity?.chainId);\n return txHash && baseUrl ? `${baseUrl}${txHash}` : undefined;\n};\n\nfunction OnrampStatusArtwork() {\n return (\n \n \n \n );\n}\n\nfunction SafeCloseNotice() {\n return (\n \n \n Safe to close - we'll notify you when complete\n \n );\n}\n\nfunction OnrampStatusButton({\n children,\n onClick,\n primaryButtonForeground,\n variant = \"primary\",\n}: {\n children: React.ReactNode;\n onClick: () => void;\n primaryButtonForeground: string;\n variant?: \"primary\" | \"secondary\";\n}) {\n const isPrimary = variant === \"primary\";\n return (\n \n {children}\n \n );\n}\n\nfunction WarningStatusIcon({ tone = \"red\" }: { tone?: \"orange\" | \"red\" }) {\n const color = tone === \"orange\" ? \"#F59E0B\" : \"#E8453C\";\n const backgroundColor = tone === \"orange\" ? \"#FFF7ED\" : \"#FEF2F2\";\n return (\n \n \n \n \n \n \n );\n}\n\nfunction OnrampActionStatusPanel({\n description,\n onPrimary,\n onSecondary,\n primaryButtonForeground,\n primaryLabel,\n secondaryLabel,\n title,\n}: {\n description: string;\n onPrimary: () => void;\n onSecondary: () => void;\n primaryButtonForeground: string;\n primaryLabel: string;\n secondaryLabel: string;\n title: string;\n}) {\n return (\n \n \n
\n
{title}
\n
{description}
\n
\n \n \n {primaryLabel}\n \n \n {secondaryLabel}\n \n \n \n );\n}\n\nfunction TimelineMarker({\n isComplete,\n isLast,\n isPending,\n}: {\n isComplete?: boolean;\n isLast?: boolean;\n isPending?: boolean;\n}) {\n return (\n \n \n {isComplete ? (\n \n ) : (\n \n )}\n \n {!isLast && (\n \n )}\n \n );\n}\n\nconst primaryButtonContrastColor = \"#FFFFFE\";\n\nfunction TimelineStep({\n isComplete,\n isLast,\n isPending,\n subtitle,\n title,\n}: {\n isComplete?: boolean;\n isLast?: boolean;\n isPending?: boolean;\n subtitle: string;\n title: string;\n}) {\n return (\n \n \n \n \n {title}\n \n \n {subtitle}\n \n \n \n );\n}\n\ntype OnrampTimelineItem = {\n complete?: boolean;\n pending?: boolean;\n subtitle: string;\n title: string;\n};\n\nfunction OnrampExpandableTimelineCard({\n summaryAmount,\n summaryLabel,\n steps,\n}: {\n summaryAmount: string;\n summaryLabel: string;\n steps: OnrampTimelineItem[];\n}) {\n const [expanded, setExpanded] = React.useState(true);\n return (\n \n setExpanded((current) => !current)}\n style={{\n alignItems: \"center\",\n background: \"transparent\",\n border: \"none\",\n color: theme.colors.textStrong,\n cursor: \"pointer\",\n display: \"flex\",\n fontFamily: theme.fonts.sans,\n fontSize: \"14px\",\n gap: \"10px\",\n justifyContent: \"space-between\",\n lineHeight: \"20px\",\n padding: \"12px 14px\",\n textAlign: \"left\",\n width: \"100%\",\n }}\n type=\"button\"\n >\n {summaryLabel}\n \n {summaryAmount}\n \n \n \n {expanded && (\n \n {steps.map((step, index) => (\n \n ))}\n \n )}\n \n );\n}\n\nfunction OnrampProcessingTimelinePanel({\n destinationAmount,\n destinationChainName,\n destinationSymbol,\n isSettling,\n provider,\n sourceAmount,\n sourceCurrencyCode,\n targetLabel,\n}: {\n destinationAmount: string;\n destinationChainName: string;\n destinationSymbol?: string;\n isSettling: boolean;\n provider?: string;\n sourceAmount: string;\n sourceCurrencyCode: string;\n targetLabel: string;\n}) {\n const destinationDisplay = formatTokenAmountDisplay(\n destinationAmount,\n destinationSymbol,\n );\n const sourceDisplay = formatCurrencyAmount(sourceAmount, sourceCurrencyCode);\n return (\n <>\n \n Usually takes 2 - 5 min\n \n \n \n \n \n \n \n );\n}\n\nfunction OnrampCompletingDepositPanel({\n destinationAmount,\n destinationChainName,\n destinationSymbol,\n provider,\n sourceAmount,\n sourceCurrencyCode,\n targetLabel,\n}: {\n destinationAmount: string;\n destinationChainName: string;\n destinationSymbol?: string;\n provider?: string;\n sourceAmount: string;\n sourceCurrencyCode: string;\n targetLabel: string;\n}) {\n const destinationDisplay = formatTokenAmountDisplay(\n destinationAmount,\n destinationSymbol,\n );\n const sourceDisplay = formatCurrencyAmount(sourceAmount, sourceCurrencyCode);\n return (\n <>\n \n Usually takes 20 seconds\n \n \n \n \n \n \n \n );\n}\n\nfunction OnrampSuccessPanel({\n destinationAmount,\n destinationSymbol,\n explorerUrl,\n onDone,\n paymentMethod,\n primaryButtonForeground,\n provider,\n sourceAmount,\n sourceCurrencyCode,\n subtitle,\n}: {\n destinationAmount: string;\n destinationSymbol?: string;\n explorerUrl?: string;\n onDone: () => void;\n paymentMethod?: string;\n primaryButtonForeground: string;\n provider?: string;\n sourceAmount: string;\n sourceCurrencyCode: string;\n subtitle: string;\n}) {\n return (\n <>\n \n {subtitle}\n \n \n \n
Transaction Summary
\n \n \n \n \n \n \n \n {explorerUrl && (\n \n \n View Explorer\n \n \n )}\n \n Done\n \n \n );\n}\n\nfunction SummaryRow({ label, value }: { label: string; value: string }) {\n return (\n \n \n {label}\n \n \n {value}\n \n \n );\n}\n\nfunction OnrampHandoffPanel({\n destinationChainName,\n provider,\n summaryAmount,\n targetLabel,\n}: {\n destinationChainName: string;\n provider?: string;\n summaryAmount: string;\n targetLabel: string;\n}) {\n return (\n <>\n \n The payment processor requires some information\n \n \n \n \n \n \n );\n}\n\nfunction OnrampSessionStatusPanel({\n depositExecution,\n onCancel,\n onDone,\n onRetryDeposit,\n onRetryPayment,\n opportunity,\n primaryButtonForeground,\n quote,\n session,\n sessionCallbackReceived,\n sourceAmount,\n sourceCurrencyCode,\n toToken,\n}: {\n depositExecution: OnrampDepositExecutionState;\n onCancel: () => void;\n onDone: () => void;\n onRetryDeposit: () => void;\n onRetryPayment: () => void;\n opportunity?: NexusWidgetDepositOpportunityConfig;\n primaryButtonForeground: string;\n quote?: OnrampQuote;\n session: OnrampSessionResponse;\n sessionCallbackReceived: boolean;\n sourceAmount: string;\n sourceCurrencyCode: string;\n toToken?: SwapTokenOption;\n}) {\n const normalizedState =\n getNormalizedOnrampState(session.state) || \"AWAITING_USER\";\n const provider = session.provider ?? quote?.provider;\n const transaction = session.transaction;\n const destinationSymbol = toToken?.symbol ?? quote?.destinationCurrencyCode;\n const destinationAmount =\n depositExecution.amount ??\n transaction?.destinationAmount ??\n quote?.destinationAmount ??\n \"\";\n const sourceDisplayAmount =\n transaction?.sourceAmount ?? quote?.sourceAmount ?? sourceAmount;\n const sourceDisplayCurrency =\n transaction?.sourceCurrencyCode ??\n quote?.sourceCurrencyCode ??\n sourceCurrencyCode;\n const destinationChainName = getDepositChainName(opportunity, toToken);\n const targetLabel = getDepositTargetLabel(opportunity);\n const explorerUrl =\n depositExecution.explorerUrl ??\n getOnrampExplorerUrl(session, opportunity, toToken);\n const containerStyle: React.CSSProperties = {\n boxSizing: \"border-box\",\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"12px\",\n width: \"100%\",\n };\n\n if (normalizedState === \"FAILED\") {\n return (\n
\n \n
\n );\n }\n\n if ([\"CANCELLED\", \"EXPIRED\", \"REFUNDED\"].includes(normalizedState)) {\n const title =\n normalizedState === \"CANCELLED\"\n ? \"Payment cancelled\"\n : normalizedState === \"REFUNDED\"\n ? \"Payment refunded\"\n : \"Payment expired\";\n const description =\n normalizedState === \"CANCELLED\"\n ? \"No payment was completed.\"\n : normalizedState === \"REFUNDED\"\n ? `${getProviderLabel(provider)} refunded the payment.`\n : \"Start a new local currency payment to continue.\";\n return (\n
\n \n
\n );\n }\n\n if (\n depositExecution.status === \"failed\" ||\n isOnrampDepositFailedState(normalizedState)\n ) {\n return (\n
\n \n
\n );\n }\n\n if (\n depositExecution.status === \"success\" ||\n isOnrampDepositSuccessState(normalizedState)\n ) {\n return (\n
\n \n
\n );\n }\n\n if (\n normalizedState === \"SETTLED\" ||\n depositExecution.status === \"running\" ||\n isOnrampDepositProcessingState(normalizedState)\n ) {\n return (\n
\n \n
\n );\n }\n\n if (sessionCallbackReceived) {\n return (\n
\n \n
\n );\n }\n\n if (isOnrampProcessingState(normalizedState)) {\n return (\n
\n \n
\n );\n }\n\n return (\n
\n \n
\n );\n}\n\nexport function DepositOnrampFlow({\n baseUrl = getOnrampBaseUrl(),\n destinationTokens,\n onConnectWallet,\n onError,\n onSelectDestinationToken,\n onSessionStateChange,\n nexusSDK,\n ownerAddress,\n opportunity,\n primaryButtonForeground,\n publicClient,\n toToken,\n}: DepositOnrampFlowProps) {\n const [countryCode, setCountryCode] = React.useState(\"\");\n const [sourceCurrencyCode, setSourceCurrencyCode] = React.useState(\"\");\n const [sourceAmount, setSourceAmount] = React.useState(\"\");\n const [options, setOptions] = React.useState(\n null,\n );\n const [routes, setRoutes] = React.useState([]);\n const [selectedPaymentMethod, setSelectedPaymentMethod] = React.useState(\"\");\n const [quotes, setQuotes] = React.useState([]);\n const [selectedProvider, setSelectedProvider] = React.useState(\"\");\n const [activeSheet, setActiveSheet] = React.useState(null);\n const [currencySearch, setCurrencySearch] = React.useState(\"\");\n const [destinationSearch, setDestinationSearch] = React.useState(\"\");\n const [methodSearch, setMethodSearch] = React.useState(\"\");\n const [partnerSearch, setPartnerSearch] = React.useState(\"\");\n const [optionsLoading, setOptionsLoading] = React.useState(false);\n const [routesLoading, setRoutesLoading] = React.useState(false);\n const [quotesLoading, setQuotesLoading] = React.useState(false);\n const [sessionLoading, setSessionLoading] = React.useState(false);\n const [error, setError] = React.useState(null);\n const [blockedRateRequest, setBlockedRateRequest] =\n React.useState(null);\n const [session, setSession] = React.useState(\n null,\n );\n const [sessionCallbackReceived, setSessionCallbackReceived] =\n React.useState(false);\n const [depositExecution, setDepositExecution] =\n React.useState({ status: \"idle\" });\n const [quoteRefreshSeconds, setQuoteRefreshSeconds] = React.useState(\n QUOTE_REFRESH_SECONDS,\n );\n const quoteRunIdRef = React.useRef(0);\n const routeRunIdRef = React.useRef(0);\n const lastRouteRequestKeyRef = React.useRef(\"\");\n const lastQuoteRequestKeyRef = React.useRef(\"\");\n const quotesLoadingRef = React.useRef(false);\n const depositExecutionSessionRef = React.useRef(\"\");\n const normalizedSessionState = getNormalizedOnrampState(session?.state);\n\n const fiatCurrencyOptions = React.useMemo(\n () => getFiatCurrencyOptions(options),\n [options],\n );\n const selectedFiatCurrency = fiatCurrencyOptions.find(\n (currency) => currency.currencyCode === sourceCurrencyCode,\n );\n const selectedOnrampCryptoCurrency = React.useMemo(\n () => getOnrampCryptoCurrency(options, toToken),\n [options, toToken],\n );\n const hasBackendCryptoCurrencyList = Array.isArray(\n options?.selection?.cryptoCurrencies,\n );\n const isDestinationTokenUnsupported = Boolean(\n hasBackendCryptoCurrencyList &&\n toToken?.chainId &&\n toToken.contractAddress &&\n !selectedOnrampCryptoCurrency,\n );\n const destinationTokenUnsupportedMessage = isDestinationTokenUnsupported\n ? \"Token is not supported for purchase with local currency\"\n : null;\n const availableDestinationTokens = React.useMemo(() => {\n const tokens = destinationTokens?.length\n ? destinationTokens\n : toToken\n ? [toToken]\n : [];\n const byKey = new Map();\n for (const token of tokens) {\n const key = getOnrampTokenKey(token);\n if (!key) continue;\n const onrampCurrency = getOnrampCryptoCurrency(options, token);\n if (hasBackendCryptoCurrencyList && !onrampCurrency) continue;\n byKey.set(key, {\n ...token,\n logo: onrampCurrency?.symbolUrl ?? token.logo,\n name: token.name ?? onrampCurrency?.name,\n });\n }\n return Array.from(byKey.values());\n }, [destinationTokens, hasBackendCryptoCurrencyList, options, toToken]);\n const destinationRequestDetails = React.useMemo(\n () => getDestinationRequestDetails(options, toToken),\n [options, toToken],\n );\n const rateRequestKey = React.useMemo(\n () =>\n getOnrampRateRequestKey({\n countryCode,\n destinationChainId: destinationRequestDetails.destinationChainId,\n destinationCurrencyCode:\n destinationRequestDetails.destinationCurrencyCode,\n destinationToken: destinationRequestDetails.destinationToken,\n sourceCurrencyCode,\n }),\n [\n countryCode,\n destinationRequestDetails.destinationChainId,\n destinationRequestDetails.destinationCurrencyCode,\n destinationRequestDetails.destinationToken,\n sourceCurrencyCode,\n ],\n );\n const quoteRequestKey = React.useMemo(\n () =>\n [\n rateRequestKey,\n ownerAddress?.toLowerCase() ?? \"\",\n selectedPaymentMethod,\n sourceAmount.trim(),\n ].join(\"|\"),\n [ownerAddress, rateRequestKey, selectedPaymentMethod, sourceAmount],\n );\n const selectedRoute = React.useMemo(\n () =>\n routes.find((route) => route.provider === selectedProvider) ?? routes[0],\n [routes, selectedProvider],\n );\n const availablePaymentMethods = React.useMemo(() => {\n const map = new Map();\n const sourceRoutes = selectedRoute ? [selectedRoute] : routes;\n for (const route of sourceRoutes) {\n for (const method of route.paymentMethods ?? []) {\n if (!method.method || map.has(method.method)) continue;\n map.set(method.method, method);\n }\n }\n return Array.from(map.values());\n }, [routes, selectedRoute]);\n const selectedPaymentMethodDetails = availablePaymentMethods.find(\n (method) => method.method === selectedPaymentMethod,\n );\n const selectedQuote =\n quotes.find(\n (quote) =>\n quote.provider === selectedProvider &&\n (!selectedPaymentMethod ||\n quote.paymentMethodType === selectedPaymentMethod),\n ) ??\n quotes.find((quote) => quote.provider === selectedProvider) ??\n quotes.find((quote) => quote.paymentMethodType === selectedPaymentMethod) ??\n quotes[0];\n const filteredFiatCurrencyOptions = React.useMemo(\n () =>\n fiatCurrencyOptions.filter((currency) =>\n matchesSearch(currencySearch, [\n currency.currencyCode,\n currency.name,\n currency.symbol,\n ]),\n ),\n [currencySearch, fiatCurrencyOptions],\n );\n const filteredDestinationTokens = React.useMemo(\n () =>\n availableDestinationTokens.filter((token) =>\n matchesSearch(destinationSearch, [\n token.symbol,\n token.name,\n token.chainName,\n token.contractAddress,\n ]),\n ),\n [availableDestinationTokens, destinationSearch],\n );\n const filteredPaymentMethods = React.useMemo(\n () =>\n availablePaymentMethods.filter((method) =>\n matchesSearch(methodSearch, [\n method.method,\n getMethodLabel(method.method),\n getMethodSubtitle(method),\n ]),\n ),\n [availablePaymentMethods, methodSearch],\n );\n const providerOptions = React.useMemo(() => {\n if (quotes.length > 0) {\n return quotes.map((quote) => {\n const route = routes.find(\n (candidate) => candidate.provider === quote.provider,\n );\n const paymentMethod = route?.paymentMethods?.find(\n (method) => method.method === quote.paymentMethodType,\n );\n return {\n destinationAmount: quote.destinationAmount,\n paymentMethod,\n paymentMethodType: quote.paymentMethodType,\n provider: quote.provider,\n quote,\n route,\n };\n });\n }\n return routes\n .filter((route) => route.provider)\n .map((route) => ({\n paymentMethod: route.paymentMethods?.[0],\n paymentMethodType: route.paymentMethods?.[0]?.method,\n provider: route.provider,\n route,\n }));\n }, [quotes, routes]);\n const filteredProviderOptions = React.useMemo(\n () =>\n providerOptions.filter((option) =>\n matchesSearch(partnerSearch, [\n option.provider,\n getProviderLabel(option.provider),\n option.paymentMethodType,\n getMethodLabel(option.paymentMethodType),\n getMethodSubtitle(option.paymentMethod),\n ]),\n ),\n [partnerSearch, providerOptions],\n );\n const hasMultipleCurrencies = fiatCurrencyOptions.length > 1;\n const hasMultipleDestinationTokens = availableDestinationTokens.length > 1;\n const hasMultipleMethods = availablePaymentMethods.length > 1;\n const hasMultipleProviders = providerOptions.length > 1;\n const parsedSourceAmount = React.useMemo(\n () => parseDecimal(sourceAmount),\n [sourceAmount],\n );\n const hasPositiveSourceAmount = Boolean(parsedSourceAmount?.gt(0));\n const amountLimitMessage = (() => {\n const limits = selectedPaymentMethodDetails?.limits;\n if (!limits || !parsedSourceAmount?.gt(0)) return null;\n const min = parseDecimal(limits.min);\n const max = parseDecimal(limits.max);\n if (min && parsedSourceAmount.lt(min)) {\n return `Minimum ${formatCurrencyAmount(min, sourceCurrencyCode)}`;\n }\n if (max && parsedSourceAmount.gt(max)) {\n return `Maximum ${formatCurrencyAmount(max, sourceCurrencyCode)}`;\n }\n return null;\n })();\n\n const executeOnrampDeposit = React.useCallback(\n async (force = false) => {\n const sessionId = session?.sessionId;\n if (!sessionId || !opportunity || !ownerAddress || !toToken) return;\n if (\n !force &&\n depositExecutionSessionRef.current === sessionId &&\n depositExecution.status !== \"failed\"\n ) {\n return;\n }\n\n depositExecutionSessionRef.current = sessionId;\n setDepositExecution({ status: \"running\" });\n setSession((current) =>\n current?.sessionId === sessionId\n ? { ...current, state: \"COMPLETING_DEPOSIT\" }\n : current,\n );\n\n const account = ownerAddress as Address;\n const decimals = opportunity.tokenDecimals ?? toToken.decimals ?? 18;\n const receivedAmount =\n session.transaction?.destinationAmount ??\n selectedQuote?.destinationAmount ??\n \"\";\n const isSandbox = getOnrampRuntimeEnvironment(baseUrl) !== \"production\";\n const sandboxAmountRaw = getSandboxDepositAmountRaw(decimals);\n let amountRaw = getRawTokenAmount(receivedAmount, decimals);\n\n try {\n if (!amountRaw || amountRaw <= BigInt(0)) {\n throw new Error(\"Unable to resolve the onramp received amount.\");\n }\n\n if (isSandbox) {\n let balanceRaw: bigint | null = null;\n if (publicClient) {\n try {\n balanceRaw = isNativeAddress(opportunity.tokenAddress)\n ? await publicClient.getBalance({ address: account })\n : ((await publicClient.readContract({\n abi: erc20Abi,\n address: opportunity.tokenAddress,\n args: [account],\n functionName: \"balanceOf\",\n })) as bigint);\n } catch {\n balanceRaw = null;\n }\n }\n\n if (balanceRaw === null || balanceRaw <= sandboxAmountRaw) {\n const displayAmount = formatUnits(amountRaw, decimals);\n setDepositExecution({\n amount: displayAmount,\n skipped: true,\n status: \"success\",\n });\n setSession((current) =>\n current?.sessionId === sessionId\n ? {\n ...current,\n deposit: {\n ...current.deposit,\n state: \"DEPOSIT_SUCCESS\",\n },\n state: \"DEPOSIT_SUCCESS\",\n }\n : current,\n );\n return;\n }\n\n amountRaw = sandboxAmountRaw;\n }\n\n if (!nexusSDK) {\n throw new Error(\"Nexus SDK is not available for deposit.\");\n }\n\n const toChainId = toToken.chainId ?? opportunity.chainId;\n const executeParams = opportunity.executeDeposit(\n opportunity.tokenSymbol,\n opportunity.tokenAddress,\n amountRaw,\n opportunity.chainId,\n account,\n );\n\n if (!isPositiveGasLimit(executeParams.gas)) {\n throw new Error(\n \"Deposit config executeDeposit must return a positive gas limit.\",\n );\n }\n\n const executionResult = await nexusSDK.execute({\n ...executeParams,\n toChainId,\n });\n const txHash = getSdkExecuteTransactionHash(executionResult);\n const displayAmount = formatUnits(amountRaw, decimals);\n const explorerUrl =\n getSdkExecuteExplorerUrl(executionResult) ??\n getTransactionExplorerUrl(toChainId, txHash);\n setDepositExecution({\n amount: displayAmount,\n explorerUrl,\n status: \"success\",\n txHash,\n });\n setSession((current) =>\n current?.sessionId === sessionId\n ? {\n ...current,\n deposit: {\n ...current.deposit,\n explorerUrl,\n state: \"DEPOSIT_SUCCESS\",\n txHash,\n },\n state: \"DEPOSIT_SUCCESS\",\n }\n : current,\n );\n } catch (depositError) {\n const message = getErrorMessage(depositError);\n setDepositExecution({ error: message, status: \"failed\" });\n setSession((current) =>\n current?.sessionId === sessionId\n ? {\n ...current,\n deposit: {\n ...current.deposit,\n state: \"DEPOSIT_FAILED\",\n },\n state: \"DEPOSIT_FAILED\",\n }\n : current,\n );\n onError?.(message);\n }\n },\n [\n baseUrl,\n depositExecution.status,\n onError,\n opportunity,\n ownerAddress,\n publicClient,\n nexusSDK,\n selectedQuote,\n session,\n toToken,\n ],\n );\n\n const applyOptions = React.useCallback(\n (data: OnrampOptionsResponse, fallbackCountryCode: string) => {\n setOptions(data);\n setCountryCode(\n data.selection?.countryCode?.toUpperCase() ?? fallbackCountryCode,\n );\n setSourceCurrencyCode((current) => {\n const currencies = getFiatCurrencyOptions(data);\n if (\n current &&\n currencies.some((currency) => currency.currencyCode === current)\n ) {\n return current;\n }\n return getDefaultFiatCurrencyCode(data);\n });\n },\n [],\n );\n\n const loadOptions = React.useCallback(async () => {\n setOptionsLoading(true);\n setError(null);\n try {\n const loadOptionsForCountry = async (countryCodeToLoad: string) => {\n const requestedCountryCode = countryCodeToLoad.toUpperCase();\n const cached = readCachedOnrampOptions(baseUrl, requestedCountryCode);\n if (cached) {\n return { data: cached, requestedCountryCode };\n }\n\n const data = await fetchOnrampJson(\n baseUrl,\n `/api/v1/onramp/options?countryCode=${encodeURIComponent(\n requestedCountryCode,\n )}`,\n { method: \"GET\" },\n );\n writeCachedOnrampOptions(baseUrl, requestedCountryCode, data);\n\n const selectedCountryCode = data.selection?.countryCode?.toUpperCase();\n if (\n selectedCountryCode &&\n selectedCountryCode !== requestedCountryCode &&\n isCountryInOptionsList(data, selectedCountryCode)\n ) {\n writeCachedOnrampOptions(baseUrl, selectedCountryCode, data);\n }\n\n return { data, requestedCountryCode };\n };\n\n const resolvedCountryCode = await resolveOnrampCountryCode();\n let { data, requestedCountryCode } =\n await loadOptionsForCountry(resolvedCountryCode);\n\n if (!isCountryInOptionsList(data, requestedCountryCode)) {\n const fallbackCountryCode = getUnsupportedCountryFallbackCode(baseUrl);\n if (fallbackCountryCode !== requestedCountryCode) {\n const fallbackOptions =\n await loadOptionsForCountry(fallbackCountryCode);\n data = fallbackOptions.data;\n requestedCountryCode = fallbackOptions.requestedCountryCode;\n }\n }\n\n applyOptions(data, requestedCountryCode);\n } catch (requestError) {\n const message = getErrorMessage(requestError);\n setError(message);\n onError?.(message);\n } finally {\n setOptionsLoading(false);\n }\n }, [applyOptions, baseUrl, onError]);\n\n React.useEffect(() => {\n void loadOptions();\n }, [loadOptions]);\n\n const getRequestErrorMessage = React.useCallback(\n (requestError: unknown) =>\n getOnrampRequestErrorMessage(requestError, {\n countryCode,\n sourceCurrencyCode,\n token: toToken,\n }),\n [countryCode, sourceCurrencyCode, toToken],\n );\n\n React.useEffect(() => {\n setBlockedRateRequest((current) =>\n current && current.key !== rateRequestKey ? null : current,\n );\n }, [rateRequestKey]);\n\n React.useEffect(() => {\n if (activeSheet !== \"currency\") setCurrencySearch(\"\");\n if (activeSheet !== \"destination\") setDestinationSearch(\"\");\n if (activeSheet !== \"method\") setMethodSearch(\"\");\n if (activeSheet !== \"partner\") setPartnerSearch(\"\");\n }, [activeSheet]);\n\n React.useEffect(() => {\n quotesLoadingRef.current = quotesLoading;\n }, [quotesLoading]);\n\n React.useEffect(() => {\n if (blockedRateRequest?.key === rateRequestKey) {\n quoteRunIdRef.current += 1;\n lastRouteRequestKeyRef.current = rateRequestKey;\n lastQuoteRequestKeyRef.current = \"\";\n setRoutes([]);\n setSelectedPaymentMethod(\"\");\n setQuotes([]);\n setSelectedProvider(\"\");\n setError(blockedRateRequest.message);\n setRoutesLoading(false);\n setQuotesLoading(false);\n return;\n }\n if (isDestinationTokenUnsupported) {\n quoteRunIdRef.current += 1;\n lastRouteRequestKeyRef.current = \"\";\n lastQuoteRequestKeyRef.current = \"\";\n setRoutes([]);\n setSelectedPaymentMethod(\"\");\n setQuotes([]);\n setSelectedProvider(\"\");\n setError(null);\n setRoutesLoading(false);\n setQuotesLoading(false);\n return;\n }\n if (\n !countryCode ||\n !sourceCurrencyCode ||\n !destinationRequestDetails.destinationCurrencyCode\n ) {\n lastRouteRequestKeyRef.current = \"\";\n lastQuoteRequestKeyRef.current = \"\";\n setRoutes([]);\n setSelectedPaymentMethod(\"\");\n setQuotes([]);\n setSelectedProvider(\"\");\n setRoutesLoading(false);\n setQuotesLoading(false);\n return;\n }\n if (lastRouteRequestKeyRef.current === rateRequestKey) return;\n const routeRunId = routeRunIdRef.current + 1;\n routeRunIdRef.current = routeRunId;\n quoteRunIdRef.current += 1;\n lastQuoteRequestKeyRef.current = \"\";\n setRoutes([]);\n setSelectedPaymentMethod(\"\");\n setQuotes([]);\n setSelectedProvider(\"\");\n let cancelled = false;\n\n const loadRoutes = async () => {\n setRoutesLoading(true);\n setError(null);\n try {\n const params = new URLSearchParams({\n countryCode,\n destinationCurrencyCode:\n destinationRequestDetails.destinationCurrencyCode,\n sourceCurrencyCode,\n });\n if (destinationRequestDetails.destinationChainId) {\n params.set(\n \"destinationChainId\",\n destinationRequestDetails.destinationChainId,\n );\n }\n if (destinationRequestDetails.destinationToken) {\n params.set(\n \"destinationToken\",\n destinationRequestDetails.destinationToken,\n );\n }\n const data = await fetchOnrampJson(\n baseUrl,\n `/api/v1/onramp/routes?${params.toString()}`,\n { method: \"GET\" },\n );\n if (cancelled || routeRunIdRef.current !== routeRunId) return;\n const nextRoutes = data.routes ?? [];\n setRoutes(nextRoutes);\n const firstRoute = nextRoutes[0];\n setSelectedProvider(firstRoute?.provider ?? \"\");\n setSelectedPaymentMethod(firstRoute?.paymentMethods?.[0]?.method ?? \"\");\n lastRouteRequestKeyRef.current = rateRequestKey;\n } catch (requestError) {\n if (cancelled || routeRunIdRef.current !== routeRunId) return;\n const message = getRequestErrorMessage(requestError);\n quoteRunIdRef.current += 1;\n if (isTerminalOnrampRateError(requestError)) {\n setBlockedRateRequest({ key: rateRequestKey, message });\n }\n setRoutes([]);\n setSelectedPaymentMethod(\"\");\n setQuotes([]);\n setSelectedProvider(\"\");\n setError(message);\n onError?.(message);\n } finally {\n if (!cancelled && routeRunIdRef.current === routeRunId) {\n setRoutesLoading(false);\n }\n }\n };\n\n void loadRoutes();\n return () => {\n cancelled = true;\n };\n }, [\n baseUrl,\n blockedRateRequest?.key,\n blockedRateRequest?.message,\n countryCode,\n destinationRequestDetails.destinationChainId,\n destinationRequestDetails.destinationCurrencyCode,\n destinationRequestDetails.destinationToken,\n getRequestErrorMessage,\n isDestinationTokenUnsupported,\n onError,\n rateRequestKey,\n sourceCurrencyCode,\n ]);\n\n const fetchQuotes = React.useCallback(async () => {\n if (\n !countryCode ||\n !ownerAddress ||\n !sourceCurrencyCode ||\n !destinationRequestDetails.destinationCurrencyCode ||\n !selectedPaymentMethod ||\n !parsedSourceAmount?.gt(0) ||\n amountLimitMessage ||\n isDestinationTokenUnsupported\n ) {\n return;\n }\n if (blockedRateRequest?.key === rateRequestKey) {\n setError(blockedRateRequest.message);\n setQuotesLoading(false);\n return;\n }\n\n const runId = quoteRunIdRef.current + 1;\n quoteRunIdRef.current = runId;\n setQuotesLoading(true);\n setError(null);\n try {\n const data = await fetchOnrampJson(\n baseUrl,\n \"/api/v1/onramp/quote\",\n {\n body: JSON.stringify({\n countryCode,\n destination: {\n chainId: destinationRequestDetails.destinationChainId,\n currencyCode: destinationRequestDetails.destinationCurrencyCode,\n token: destinationRequestDetails.destinationToken,\n },\n destinationChainId: destinationRequestDetails.destinationChainId,\n destinationCurrencyCode:\n destinationRequestDetails.destinationCurrencyCode,\n destinationToken: destinationRequestDetails.destinationToken,\n paymentMethodType: selectedPaymentMethod,\n sourceAmount,\n sourceCurrencyCode,\n walletAddress: ownerAddress,\n }),\n method: \"POST\",\n },\n );\n if (quoteRunIdRef.current !== runId) return;\n const nextQuotes = sortQuotes(data.quotes ?? []);\n setQuotes(nextQuotes);\n setSelectedProvider((current) =>\n current && nextQuotes.some((quote) => quote.provider === current)\n ? current\n : (nextQuotes[0]?.provider ?? \"\"),\n );\n if (nextQuotes.length === 0) {\n setError(\n \"No local currency rates are available for this token and currency. Choose another deposit token or pay with wallet.\",\n );\n }\n } catch (requestError) {\n if (quoteRunIdRef.current !== runId) return;\n const message = getRequestErrorMessage(requestError);\n if (isTerminalOnrampRateError(requestError)) {\n setBlockedRateRequest({ key: rateRequestKey, message });\n }\n setQuotes([]);\n setError(message);\n onError?.(message);\n } finally {\n if (quoteRunIdRef.current === runId) {\n setQuotesLoading(false);\n setQuoteRefreshSeconds(QUOTE_REFRESH_SECONDS);\n }\n }\n }, [\n amountLimitMessage,\n baseUrl,\n countryCode,\n destinationRequestDetails.destinationChainId,\n destinationRequestDetails.destinationCurrencyCode,\n destinationRequestDetails.destinationToken,\n getRequestErrorMessage,\n blockedRateRequest,\n isDestinationTokenUnsupported,\n onError,\n ownerAddress,\n parsedSourceAmount,\n rateRequestKey,\n selectedPaymentMethod,\n sourceAmount,\n sourceCurrencyCode,\n ]);\n\n React.useEffect(() => {\n setSessionCallbackReceived(false);\n setSession(null);\n }, [\n destinationRequestDetails.destinationChainId,\n destinationRequestDetails.destinationCurrencyCode,\n destinationRequestDetails.destinationToken,\n ]);\n\n React.useEffect(() => {\n if (session?.sessionId) return;\n if (\n !ownerAddress ||\n !sourceCurrencyCode ||\n !destinationRequestDetails.destinationCurrencyCode ||\n !selectedPaymentMethod ||\n !parsedSourceAmount?.gt(0) ||\n amountLimitMessage ||\n isDestinationTokenUnsupported\n ) {\n lastQuoteRequestKeyRef.current = \"\";\n setQuotes([]);\n return;\n }\n if (lastQuoteRequestKeyRef.current === quoteRequestKey) return;\n setQuotes([]);\n const timer = window.setTimeout(() => {\n lastQuoteRequestKeyRef.current = quoteRequestKey;\n void fetchQuotes();\n }, 350);\n return () => {\n window.clearTimeout(timer);\n };\n }, [\n amountLimitMessage,\n destinationRequestDetails.destinationChainId,\n destinationRequestDetails.destinationCurrencyCode,\n destinationRequestDetails.destinationToken,\n fetchQuotes,\n isDestinationTokenUnsupported,\n ownerAddress,\n parsedSourceAmount,\n quoteRequestKey,\n selectedPaymentMethod,\n session?.sessionId,\n sourceCurrencyCode,\n ]);\n\n React.useEffect(() => {\n if (session?.sessionId || !selectedQuote || sessionLoading) {\n return;\n }\n setQuoteRefreshSeconds(QUOTE_REFRESH_SECONDS);\n const interval = window.setInterval(() => {\n setQuoteRefreshSeconds((current) => {\n if (current <= 1) {\n if (!quotesLoadingRef.current) void fetchQuotes();\n return QUOTE_REFRESH_SECONDS;\n }\n return current - 1;\n });\n }, 1000);\n return () => {\n window.clearInterval(interval);\n };\n }, [fetchQuotes, selectedQuote, session?.sessionId, sessionLoading]);\n\n React.useEffect(() => {\n const derivedSessionState =\n depositExecution.status === \"running\"\n ? \"COMPLETING_DEPOSIT\"\n : depositExecution.status === \"success\"\n ? \"DEPOSIT_SUCCESS\"\n : depositExecution.status === \"failed\"\n ? \"DEPOSIT_FAILED\"\n : normalizedSessionState || \"AWAITING_USER\";\n onSessionStateChange?.(session?.sessionId ? derivedSessionState : null);\n }, [\n depositExecution.status,\n normalizedSessionState,\n onSessionStateChange,\n session?.sessionId,\n ]);\n\n React.useEffect(\n () => () => {\n onSessionStateChange?.(null);\n },\n [onSessionStateChange],\n );\n\n const applyOnrampCallback = React.useCallback((payload: unknown) => {\n if (payload === ONRAMP_CALLBACK_SUCCESS_MESSAGE) {\n setSessionCallbackReceived(true);\n setError(null);\n return;\n }\n if (!isOnrampCallbackPayload(payload)) return;\n setSessionCallbackReceived(true);\n setSession((current) => {\n if (current?.sessionId && current.sessionId !== payload.sessionId) {\n return current;\n }\n return {\n ...current,\n ...payload.session,\n sessionId: payload.sessionId,\n state:\n payload.session?.state ??\n (payload.state || undefined) ??\n current?.state,\n };\n });\n setError(null);\n }, []);\n\n const applyManualOnrampSessionId = React.useCallback((sessionId: string) => {\n const normalizedSessionId = sessionId.trim();\n if (!normalizedSessionId) return;\n setSessionCallbackReceived(true);\n setSession({\n sessionId: normalizedSessionId,\n state: \"AWAITING_USER\",\n });\n setError(null);\n }, []);\n\n React.useEffect(() => {\n if (typeof window === \"undefined\") return;\n\n const handleMessage = (event: MessageEvent) => {\n applyOnrampCallback(event.data);\n };\n const handleStorage = (event: StorageEvent) => {\n if (event.key !== ONRAMP_CALLBACK_STORAGE_KEY || !event.newValue) return;\n try {\n applyOnrampCallback(JSON.parse(event.newValue));\n } catch {\n // Ignore malformed callback payloads from storage.\n }\n };\n\n window.addEventListener(\"message\", handleMessage);\n window.addEventListener(\"storage\", handleStorage);\n window.setRampSessionId = applyManualOnrampSessionId;\n\n let channel: BroadcastChannel | null = null;\n if (\"BroadcastChannel\" in window) {\n channel = new BroadcastChannel(ONRAMP_CALLBACK_CHANNEL);\n channel.onmessage = (event) => applyOnrampCallback(event.data);\n }\n\n return () => {\n window.removeEventListener(\"message\", handleMessage);\n window.removeEventListener(\"storage\", handleStorage);\n if (window.setRampSessionId === applyManualOnrampSessionId) {\n delete window.setRampSessionId;\n }\n channel?.close();\n };\n }, [applyManualOnrampSessionId, applyOnrampCallback]);\n\n React.useEffect(() => {\n const sessionId = session?.sessionId;\n if (!sessionId || isOnrampTerminalState(normalizedSessionState)) return;\n\n let cancelled = false;\n let inFlight = false;\n\n const pollSession = async () => {\n if (inFlight) return;\n inFlight = true;\n try {\n const data = await fetchOnrampJson(\n baseUrl,\n `/api/v1/onramp/sessions/${encodeURIComponent(sessionId)}`,\n { method: \"GET\" },\n );\n if (cancelled) return;\n setSession((current) =>\n current?.sessionId === sessionId ? { ...current, ...data } : current,\n );\n } catch (requestError) {\n if (cancelled) return;\n const message = getErrorMessage(requestError);\n setError(message);\n onError?.(message);\n } finally {\n inFlight = false;\n }\n };\n\n void pollSession();\n const interval = window.setInterval(pollSession, ONRAMP_SESSION_POLL_MS);\n return () => {\n cancelled = true;\n window.clearInterval(interval);\n };\n }, [baseUrl, normalizedSessionState, onError, session?.sessionId]);\n\n React.useEffect(() => {\n if (!session?.sessionId) {\n depositExecutionSessionRef.current = \"\";\n setDepositExecution({ status: \"idle\" });\n return;\n }\n if (\n depositExecutionSessionRef.current &&\n depositExecutionSessionRef.current !== session.sessionId\n ) {\n depositExecutionSessionRef.current = \"\";\n setDepositExecution({ status: \"idle\" });\n }\n }, [session?.sessionId]);\n\n React.useEffect(() => {\n if (\n !session?.sessionId ||\n normalizedSessionState !== \"SETTLED\" ||\n depositExecution.status !== \"idle\"\n ) {\n return;\n }\n void executeOnrampDeposit();\n }, [\n depositExecution.status,\n executeOnrampDeposit,\n normalizedSessionState,\n session?.sessionId,\n ]);\n\n const createSession = async () => {\n if (\n !selectedQuote ||\n !ownerAddress ||\n !selectedPaymentMethod ||\n isDestinationTokenUnsupported\n ) {\n return;\n }\n setSessionLoading(true);\n setError(null);\n setSessionCallbackReceived(false);\n depositExecutionSessionRef.current = \"\";\n setDepositExecution({ status: \"idle\" });\n const providerWindow = openOnrampProviderWindow();\n if (!providerWindow) {\n setSessionLoading(false);\n setError(\n \"Payment provider popup was blocked. Allow popups and try again.\",\n );\n return;\n }\n try {\n const data = await fetchOnrampJson(\n baseUrl,\n \"/api/v1/onramp/sessions\",\n {\n body: JSON.stringify({\n countryCode,\n destination: {\n chainId: destinationRequestDetails.destinationChainId,\n currencyCode:\n selectedQuote.destinationCurrencyCode ??\n destinationRequestDetails.destinationCurrencyCode,\n token: destinationRequestDetails.destinationToken,\n },\n destinationChainId: destinationRequestDetails.destinationChainId,\n destinationCurrencyCode:\n selectedQuote.destinationCurrencyCode ??\n destinationRequestDetails.destinationCurrencyCode,\n destinationToken: destinationRequestDetails.destinationToken,\n paymentMethodType: selectedPaymentMethod,\n provider: selectedQuote.provider,\n returnUrl: getOnrampReturnUrl(),\n sourceAmount: selectedQuote.sourceAmount,\n sourceCurrencyCode: selectedQuote.sourceCurrencyCode,\n walletAddress: ownerAddress,\n }),\n headers: {\n \"Idempotency-Key\": createIdempotencyKey(),\n },\n method: \"POST\",\n },\n );\n setSession(data);\n const widgetUrl = data.widgetUrl ?? data.fallbackWidgetUrl;\n if (widgetUrl) {\n providerWindow.location.href = widgetUrl;\n } else {\n providerWindow.close();\n setError(\"Payment provider URL was not returned. Try again.\");\n }\n } catch (requestError) {\n providerWindow?.close();\n const message = getRequestErrorMessage(requestError);\n setError(message);\n onError?.(message);\n } finally {\n setSessionLoading(false);\n }\n };\n\n const resetSession = () => {\n setError(null);\n setSessionCallbackReceived(false);\n depositExecutionSessionRef.current = \"\";\n setDepositExecution({ status: \"idle\" });\n setSession(null);\n };\n\n const completeSession = () => {\n setError(null);\n setSessionCallbackReceived(false);\n depositExecutionSessionRef.current = \"\";\n setDepositExecution({ status: \"idle\" });\n setQuotes([]);\n setSelectedProvider(\"\");\n setSession(null);\n setSourceAmount(\"\");\n };\n\n const handleCurrencySelect = (currencyCode: string) => {\n setSourceCurrencyCode(currencyCode);\n setSelectedPaymentMethod(\"\");\n setSelectedProvider(\"\");\n setQuotes([]);\n setRoutes([]);\n setActiveSheet(null);\n };\n\n const handleDestinationTokenSelect = (token: SwapTokenOption) => {\n if (!isSameOnrampToken(token, toToken)) {\n onSelectDestinationToken?.(token);\n setSelectedPaymentMethod(\"\");\n setSelectedProvider(\"\");\n setQuotes([]);\n setRoutes([]);\n setSessionCallbackReceived(false);\n setSession(null);\n }\n setActiveSheet(null);\n };\n\n const handleProviderSelect = (option: OnrampProviderOption) => {\n setSelectedProvider(option.provider);\n if (option.paymentMethodType) {\n setSelectedPaymentMethod(option.paymentMethodType);\n }\n if (!option.quote) {\n setQuotes([]);\n }\n setActiveSheet(null);\n };\n\n const handleMethodSelect = (method: string) => {\n const nextRoute =\n routes.find(\n (route) =>\n route.provider === selectedProvider &&\n route.paymentMethods?.some(\n (candidate) => candidate.method === method,\n ),\n ) ??\n routes.find((route) =>\n route.paymentMethods?.some((candidate) => candidate.method === method),\n );\n setSelectedPaymentMethod(method);\n setSelectedProvider(nextRoute?.provider ?? selectedProvider);\n setQuotes([]);\n setActiveSheet(null);\n };\n\n const receiveAmount = selectedQuote?.destinationAmount;\n const receiveUsd = selectedQuote?.destinationAmount;\n const feeTotal = selectedQuote?.fees?.total;\n const displayProvider = selectedQuote?.provider ?? selectedProvider;\n const providerSubtitle = selectedQuote ? \"Best available quote\" : \"\";\n const hasRouteDetails = Boolean(\n selectedPaymentMethodDetails || selectedPaymentMethod || displayProvider,\n );\n const rateRequestLoading = routesLoading || quotesLoading;\n const fetchingBestRates =\n hasPositiveSourceAmount &&\n !selectedQuote &&\n !amountLimitMessage &&\n !error &&\n !destinationTokenUnsupportedMessage &&\n Boolean(destinationRequestDetails.destinationCurrencyCode) &&\n (rateRequestLoading || Boolean(selectedPaymentMethod));\n const ctaRateLoading =\n !error &&\n !destinationTokenUnsupportedMessage &&\n !selectedQuote &&\n (fetchingBestRates || (hasPositiveSourceAmount && rateRequestLoading));\n const quoteDetailsLoading =\n fetchingBestRates || (routesLoading && !hasRouteDetails);\n const shouldShowQuoteDetails =\n !destinationTokenUnsupportedMessage &&\n (quoteDetailsLoading || hasRouteDetails || Boolean(selectedQuote));\n const shouldShowQuoteTimer = Boolean(selectedQuote);\n const ctaDisabled =\n !ownerAddress ||\n !hasPositiveSourceAmount ||\n !selectedQuote ||\n Boolean(destinationTokenUnsupportedMessage) ||\n Boolean(amountLimitMessage) ||\n ctaRateLoading ||\n sessionLoading;\n\n if (!ownerAddress) {\n return (\n
\n \n \n Connect wallet to continue\n
\n \n Your wallet address is required for the hosted onramp session.\n \n void onConnectWallet()}\n style={{\n backgroundColor: brand,\n border: \"none\",\n borderRadius: theme.radius.primaryButton,\n color: primaryButtonForeground,\n cursor: \"pointer\",\n fontFamily: theme.fonts.sans,\n fontSize: \"14px\",\n fontWeight: 500,\n height: \"40px\",\n paddingInline: \"16px\",\n }}\n type=\"button\"\n >\n Connect Wallet\n \n \n \n );\n }\n\n if (session?.sessionId) {\n return (\n void executeOnrampDeposit(true)}\n onRetryPayment={() => void createSession()}\n opportunity={opportunity}\n primaryButtonForeground={primaryButtonForeground}\n quote={selectedQuote}\n session={session}\n sessionCallbackReceived={sessionCallbackReceived}\n sourceAmount={sourceAmount}\n sourceCurrencyCode={sourceCurrencyCode}\n toToken={toToken}\n />\n );\n }\n\n return (\n \n {shouldShowQuoteTimer && (\n \n \n {quotesLoading || routesLoading ? \"...\" : `${quoteRefreshSeconds}s`}\n \n )}\n\n
\n \n \n \n
You Pay
\n \n setSourceAmount(normalizeAmountInput(event.target.value))\n }\n placeholder=\"0\"\n style={{\n background: \"transparent\",\n border: \"none\",\n color: theme.colors.textStrong,\n fontFamily: theme.fonts.display,\n fontSize: \"31px\",\n fontWeight: 500,\n letterSpacing: \"0\",\n lineHeight: \"36px\",\n outline: \"none\",\n padding: 0,\n cursor: destinationTokenUnsupportedMessage\n ? \"not-allowed\"\n : undefined,\n width: \"100%\",\n }}\n value={sourceAmount}\n />\n \n {hasPositiveSourceAmount\n ? formatCurrencyAmount(\n selectedQuote?.sourceAmount ?? sourceAmount,\n selectedQuote?.sourceCurrencyCode ?? sourceCurrencyCode,\n )\n : \"Select currency\"}\n
\n \n setActiveSheet(\"currency\")}\n >\n \n \n {sourceCurrencyCode || \"---\"}\n \n \n \n\n {selectedPaymentMethodDetails?.limits && (\n \n \n \n Limits\n {selectedPaymentMethodDetails.limits.min\n ? ` · Min ${formatCurrencyAmount(\n selectedPaymentMethodDetails.limits.min,\n sourceCurrencyCode,\n )}`\n : \"\"}\n {selectedPaymentMethodDetails.limits.max\n ? ` · Max ${formatCurrencyAmount(\n selectedPaymentMethodDetails.limits.max,\n sourceCurrencyCode,\n )}`\n : \"\"}\n \n \n )}\n\n \n\n \n \n
Receive
\n \n {receiveAmount ? formatNumberDisplay(receiveAmount, 6) : \"0\"}\n \n \n {receiveAmount ? formatUsdDisplay(receiveUsd) : \"$0.00 USD\"}\n \n \n setActiveSheet(\"destination\")}\n >\n \n \n {toToken?.symbol ?? \"Token\"}\n \n \n \n \n \n\n {shouldShowQuoteDetails &&\n (quoteDetailsLoading ? (\n \n ) : hasRouteDetails ? (\n
\n setActiveSheet(\"method\")}\n />\n }\n label=\"Payment Method\"\n >\n \n \n \n {getMethodLabel(selectedPaymentMethod)}\n
\n \n \n\n setActiveSheet(\"partner\")}\n />\n }\n divider\n label=\"Payment Partner\"\n >\n \n \n \n \n {getProviderLabel(displayProvider)}\n \n {providerSubtitle && (\n \n {providerSubtitle}\n \n )}\n \n \n \n\n {selectedQuote && (\n setActiveSheet(\"fees\")}\n style={{\n alignItems: \"center\",\n backgroundColor: \"transparent\",\n border: \"none\",\n borderTop: `1px solid ${theme.colors.divider}`,\n boxSizing: \"border-box\",\n cursor: \"pointer\",\n display: \"flex\",\n justifyContent: \"space-between\",\n padding: \"14px\",\n textAlign: \"left\",\n width: \"100%\",\n }}\n type=\"button\"\n >\n
\n \n Total Fees\n
\n \n Inclusive of fees\n \n \n \n \n {feeTotal\n ? formatCurrencyAmount(feeTotal, sourceCurrencyCode)\n : \"--\"}\n \n \n View breakdown\n \n \n \n )}\n \n ) : null)}\n\n {(destinationTokenUnsupportedMessage ||\n error ||\n amountLimitMessage ||\n session?.state) && (\n \n {destinationTokenUnsupportedMessage ??\n error ??\n amountLimitMessage ??\n `Onramp session ${session?.state?.toLowerCase() ?? \"created\"}.`}\n \n )}\n\n void createSession()}\n style={{\n alignItems: \"center\",\n backgroundColor: ctaDisabled ? theme.colors.surfaceCool : brand,\n border: \"none\",\n borderRadius: theme.radius.primaryButton,\n boxShadow: ctaDisabled ? \"none\" : theme.shadows.primaryButton,\n color: ctaDisabled ? theme.colors.muted : primaryButtonForeground,\n cursor: ctaDisabled ? \"default\" : \"pointer\",\n display: \"flex\",\n fontFamily: theme.fonts.sans,\n fontSize: \"14px\",\n fontWeight: 500,\n gap: \"8px\",\n height: \"44px\",\n justifyContent: \"center\",\n lineHeight: \"18px\",\n width: \"100%\",\n }}\n type=\"button\"\n >\n {sessionLoading || ctaRateLoading ? (\n \n ) : selectedQuote ? (\n \n ) : null}\n {sessionLoading\n ? \"Opening provider...\"\n : ctaRateLoading\n ? \"Fetching best rates...\"\n : destinationTokenUnsupportedMessage\n ? \"Token not supported\"\n : selectedQuote\n ? `Pay ${formatCurrencyAmount(sourceAmount, sourceCurrencyCode)}`\n : \"Enter amount\"}\n \n\n {activeSheet === \"fees\" && (\n setActiveSheet(null)}\n title={`Buying ${formatNumberDisplay(receiveAmount, 6)} ${toToken?.symbol ?? \"\"}`}\n >\n \n 1 {toToken?.symbol ?? \"token\"} ≈{\" \"}\n {selectedQuote &&\n parseDecimal(selectedQuote.destinationAmount)?.gt(0)\n ? formatCurrencyAmount(\n parseDecimal(selectedQuote.sourceAmount)?.div(\n parseDecimal(selectedQuote.destinationAmount) ??\n new Decimal(1),\n ),\n sourceCurrencyCode,\n )\n : \"--\"}\n \n
\n {[\n [\"Provider fee\", selectedQuote?.fees?.provider],\n [\"Network fee\", selectedQuote?.fees?.network],\n [\"Partner fee\", selectedQuote?.fees?.partner],\n ].map(([label, value], index) => (\n 0 ? `1px solid ${theme.colors.divider}` : undefined,\n display: \"flex\",\n justifyContent: \"space-between\",\n padding: \"12px 14px\",\n }}\n >\n \n {label}\n \n \n {value\n ? formatCurrencyAmount(value, sourceCurrencyCode)\n : \"--\"}\n \n
\n ))}\n \n \n Total you pay\n \n \n {formatCurrencyAmount(sourceAmount, sourceCurrencyCode)}\n \n \n \n \n )}\n\n {activeSheet === \"partner\" && (\n setActiveSheet(null)} title=\"Payment partner\">\n \n {filteredProviderOptions.length > 0 ? (\n \n {filteredProviderOptions.map((option) => {\n const optionIndex = providerOptions.findIndex(\n (candidate) =>\n candidate.provider === option.provider &&\n candidate.paymentMethodType === option.paymentMethodType,\n );\n const methodSubtitle = getMethodSubtitle(option.paymentMethod);\n const optionSubtitle = option.paymentMethodType\n ? [getMethodLabel(option.paymentMethodType), methodSubtitle]\n .filter(Boolean)\n .join(\" · \")\n : \"Available route\";\n return (\n }\n key={`${option.provider}-${option.paymentMethodType ?? \"route\"}`}\n onClick={() => handleProviderSelect(option)}\n primary={optionIndex === 0}\n selected={option.provider === displayProvider}\n subtitle={optionSubtitle}\n title={getProviderLabel(option.provider)}\n value={\n option.destinationAmount\n ? `${formatNumberDisplay(option.destinationAmount, 6)} ${toToken?.symbol ?? \"\"}`\n : undefined\n }\n />\n );\n })}\n \n ) : (\n No payment partners found.\n )}\n \n )}\n\n {activeSheet === \"currency\" && (\n setActiveSheet(null)} title=\"Select currency\">\n \n
Fiat currencies
\n {filteredFiatCurrencyOptions.length > 0 ? (\n \n {filteredFiatCurrencyOptions.map((currency) => (\n }\n key={currency.currencyCode}\n onClick={() => handleCurrencySelect(currency.currencyCode)}\n selected={currency.currencyCode === sourceCurrencyCode}\n subtitle={getFiatCurrencyName(\n currency.currencyCode,\n currency,\n )}\n title={currency.currencyCode}\n />\n ))}\n \n ) : (\n No currencies found.\n )}\n
\n )}\n\n {activeSheet === \"method\" && (\n setActiveSheet(null)} title=\"Payment method\">\n \n {filteredPaymentMethods.length > 0 ? (\n \n {filteredPaymentMethods.map((method) => (\n }\n key={method.method}\n onClick={() => handleMethodSelect(method.method)}\n selected={method.method === selectedPaymentMethod}\n subtitle={getMethodSubtitle(method)}\n title={getMethodLabel(method.method)}\n />\n ))}\n \n ) : (\n No payment methods found.\n )}\n \n )}\n\n {activeSheet === \"destination\" && (\n setActiveSheet(null)} title=\"Select token\">\n \n {filteredDestinationTokens.length > 0 ? (\n \n {filteredDestinationTokens.map((token) => (\n }\n key={getOnrampTokenKey(token)}\n onClick={() => handleDestinationTokenSelect(token)}\n selected={isSameOnrampToken(token, toToken)}\n subtitle={token.chainName}\n title={token.symbol}\n value={token.balanceInFiat}\n />\n ))}\n \n ) : (\n No tokens found.\n )}\n \n )}\n \n );\n}\n", "type": "registry:component", "target": "components/nexus/components/deposit-onramp-flow.tsx" }, @@ -110,7 +110,7 @@ }, { "path": "registry/avail-widgets/nexus-widget/nexus-widget.tsx", - "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\"use client\";\n\nimport {\n ERROR_CODES,\n type EthereumProvider,\n} from \"@avail-project/nexus-core\";\nimport Decimal from \"decimal.js\";\nimport { AlertCircle, ArrowLeft, ChevronDown, Loader2 } from \"lucide-react\";\nimport React, {\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { flushSync } from \"react-dom\";\nimport {\n createPublicClient,\n encodeFunctionData,\n erc20Abi,\n http,\n isAddress,\n parseUnits,\n zeroAddress,\n} from \"viem\";\nimport { mainnet } from \"viem/chains\";\nimport { normalize } from \"viem/ens\";\nimport {\n useAccount,\n useConnect,\n useConnectorClient,\n usePublicClient,\n useWalletClient,\n} from \"wagmi\";\nimport { ErrorBoundary } from \"../common/components/ErrorBoundary\";\nimport { useTransactionSteps } from \"../common/tx/useTransactionSteps\";\nimport type {\n BridgeStepType,\n SwapStepType,\n} from \"../common/types/transaction-flow\";\nimport {\n CHAIN_METADATA,\n getShortChainName,\n isSwapSupportedBySdkChainList,\n SUPPORTED_CHAINS,\n TOKEN_CONTRACT_ADDRESSES,\n TOKEN_METADATA,\n} from \"../common/utils/constant\";\nimport { type UserAsset, useNexus } from \"../nexus/NexusProvider\";\nimport { Button } from \"../ui/button\";\nimport { Dialog, DialogContent, DialogTrigger } from \"../ui/dialog\";\nimport { DepositFundingMethod } from \"./components/deposit-funding-method\";\nimport { DepositIdleForm } from \"./components/deposit-idle-form\";\nimport { DepositOnrampFlow } from \"./components/deposit-onramp-flow\";\nimport {\n type NexusWidgetProgressEvent,\n NexusWidgetProgressScreen,\n} from \"./components/nexus-widget-progress-screen\";\nimport {\n getCachedReceiveTokenMatch,\n preloadReceiveTokens,\n ReceiveAssetSelector,\n} from \"./components/receive-asset-selector\";\nimport { RecipientInput } from \"./components/recipient-input\";\nimport { SendIdleForm } from \"./components/send-idle-form\";\nimport { StatusAlert } from \"./components/status-alerts\";\nimport {\n deriveTokenOptions,\n SwapAssetSelector,\n type SwapTokenOption,\n} from \"./components/swap-asset-selector\";\nimport { SwapIdleForm } from \"./components/swap-idle-form\";\nimport {\n type BridgeProvider,\n type SwapIntentData,\n type SwapIntentDestination,\n type SwapIntentSource,\n SwapIntentPreview,\n} from \"./components/swap-intent-preview\";\nimport {\n NEXUS_WIDGET_DEFAULT_PRIMARY_COLOR,\n NEXUS_WIDGET_FAST_SPINNER_STYLE,\n nexusWidgetTheme,\n} from \"./theme\";\nimport {\n type NexusWidgetAppearance,\n type NexusWidgetConfig,\n type NexusWidgetDepositConfig,\n type NexusWidgetDestination,\n type NexusWidgetDestinationToken,\n type NexusWidgetDepositOpportunityConfig,\n type NexusWidgetDepositOpportunityMetadata,\n type NexusWidgetMode,\n type NexusWidgetPrefillToken,\n type NexusWidgetProps,\n type NexusWidgetRuntimePrefill,\n type SwapType,\n} from \"./types\";\nimport { findCitreaReceiveToken } from \"./utils/citrea-tokens\";\nimport {\n type DepositSourceFilter,\n getDepositSourceId,\n resolveDepositSourceSelection,\n} from \"./utils/deposit-source-selection\";\n\n// ---------------------------------------------------------------------------\n// Types for swap step machine\n// ---------------------------------------------------------------------------\n\ntype SwapStep =\n | \"idle\" // main screen\n | \"choose-swap-asset\" // pick source token\n | \"choose-receive-asset\" // pick receive token\n | \"enter-recipient\" // pick recipient (send mode)\n | \"preview-intent\" // intent preview card\n | \"progress\" // transaction in flight\n | \"success\" // completed seamlessly\n | \"failed\" // failed swap receipt\n | \"history\"; // transaction history\n\ntype DepositFundingStep = \"method\" | \"wallet\" | \"onramp\";\n\ntype SourceFilterTab = \"all\" | \"native\" | \"stables\";\n\ntype SwapHistoryStatus =\n | \"pending\"\n | \"fulfilled\"\n | \"failed\"\n | \"timeout\"\n | \"refund-initiated\";\n\ninterface SwapHistoryEntry {\n autoRefundAvailable?: boolean;\n createdAt: number;\n durationSeconds?: number;\n endedAt?: number;\n error?: string;\n failedStepType?: string;\n failureDescription?: string;\n failureMessage?: string;\n feeUsd?: string;\n finalExplorerUrl?: string | null;\n fromTokens: SwapTokenOption[];\n id: string;\n intentData: SwapIntentData | null;\n intentExplorerUrl?: string | null;\n intentId?: number;\n mode: NexusWidgetMode;\n opportunity?: NexusWidgetDepositOpportunityMetadata;\n recipientAddress?: string;\n requestedToAmount?: string;\n requestedToValue?: string;\n sourceExplorerUrl?: string | null;\n startedAt: number;\n status: SwapHistoryStatus;\n toToken?: SwapTokenOption;\n}\n\ntype HistorySourceRow = {\n amount: string;\n chainId?: number;\n chainLogo?: string;\n chainName: string;\n contractAddress?: string;\n key: string;\n symbol: string;\n tokenLogo?: string;\n value?: unknown;\n};\n\ntype SwapQuoteIssue = {\n type: \"insufficientSources\";\n message: string;\n missingUsd?: string;\n};\n\ntype ReceiveAmountIssue = {\n ctaLabel: string;\n message: string;\n type: \"configuredAmountLimit\" | \"receiveLimitExceeded\" | \"unpricedReceiveToken\";\n};\n\ntype CachedIntentUsdRate = {\n amount: string;\n rate: string;\n updatedAt: number;\n value: string;\n};\n\ntype PredictiveQuote = {\n key: string;\n mode: \"exactIn\" | \"exactOut\";\n sources?: SwapTokenOption[];\n toAmount?: string;\n toUsd?: string;\n};\n\ntype IntentFetchTiming = {\n background: boolean;\n endTimeMs?: number;\n fetchDurationMs?: number;\n operation: string;\n quoteInputKey: string;\n runId: number;\n sourceCount?: number;\n startTimeMs: number;\n walletAddress?: string | null;\n};\n\ntype PredictiveQuoteBaseline = {\n destinationUsdRate: string;\n exactInDestinationAmountPerSourceUsd?: string;\n exactOutSourceUsdPerDestinationUsd?: string;\n updatedAt: number;\n};\n\nconst DESTINATION_RECEIVE_LIMIT_USD_BY_CHAIN_ID: Record = {\n [SUPPORTED_CHAINS.MEGAETH]: 10_000,\n [SUPPORTED_CHAINS.CITREA]: 2000,\n [SUPPORTED_CHAINS.SCROLL]: 500,\n};\n\nconst SCIENTIFIC_DECIMAL_REGEX = /^-?(?:\\d+\\.?\\d*|\\.\\d+)e[+-]?\\d+$/i;\n\nconst QUOTE_REFRESH_INTERVAL_MS = 30000;\nconst EXACT_OUT_INPUT_DEBOUNCE_MS = 1300;\nconst DRAWER_CLOSE_MS = 220;\nconst BALANCE_REFRESH_AFTER_TERMINAL_MS = 3000;\nconst MODAL_HEIGHT_TRANSITION_MS = 220;\nconst ROOT_HEIGHT_TRANSITION_MS = 140;\nconst ASSET_SELECTOR_DRAWER_HEIGHT = \"90%\";\nconst TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT = 620;\nconst CONFIGURED_RECEIVE_SELECTOR_BASE_HEIGHT = 170;\nconst CONFIGURED_RECEIVE_SELECTOR_ROW_HEIGHT = 62;\nconst BASIS_POINTS = 10000;\nconst PREDICTIVE_EXACT_IN_DISCOUNT_BPS = 50;\nconst PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_PCT = 0.1;\nconst PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_MAX_USD = 2;\nconst PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_PCT = 0.02;\nconst PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_MAX_USD = 1;\nconst PREDICTIVE_QUOTE_DISPLAY_DECIMALS = 8;\nconst DEPOSIT_TOKEN_DISPLAY_DECIMALS = 8;\nconst ETHEREUM_MAINNET_CHAIN_ID = 1;\nconst SDK_EXACT_OUT_STABLE_SYMBOLS = new Set([\n \"USDC\",\n \"USDT\",\n \"DAI\",\n \"BUSD\",\n \"TUSD\",\n \"FRAX\",\n \"LUSD\",\n \"USDD\",\n \"USDP\",\n \"GUSD\",\n]);\nconst SWAP_HISTORY_STORAGE_KEY_PREFIX = \"nexus-widget-transaction-history-v1\";\nconst TIMEOUT_LABEL = \"Timed Out\";\nconst PROGRESS_EVENT_NAMES = {\n BRIDGE_PLAN_LIST: \"bridge_plan_list\",\n BRIDGE_PLAN_PROGRESS: \"bridge_plan_progress\",\n SWAP_PLAN_LIST: \"swap_plan_list\",\n SWAP_PLAN_PROGRESS: \"swap_plan_progress\",\n} as const;\nconst PLAN_FINAL_STATES = new Set([\"completed\", \"confirmed\", \"submitted\"]);\nconst PLAN_STEP_FUNDS_MAY_HAVE_MOVED_STATES = new Set([\n \"completed\",\n \"confirmed\",\n \"submitted\",\n \"failed\",\n]);\nconst theme = nexusWidgetTheme;\nconst tooltipSurface = theme.colors.surface;\nconst tooltipText = theme.colors.textStrong;\nconst tooltipBorder = theme.colors.border;\nconst uiFont = theme.fonts.sans;\nconst modalHeightTransitionStyle = {\n interpolateSize: \"allow-keywords\",\n} as React.CSSProperties;\n\nconst getCappedTokenDisplayDecimals = (decimals?: number) => {\n const parsedDecimals = Number(decimals);\n if (!Number.isFinite(parsedDecimals) || parsedDecimals < 0) {\n return DEPOSIT_TOKEN_DISPLAY_DECIMALS;\n }\n return Math.min(Math.floor(parsedDecimals), DEPOSIT_TOKEN_DISPLAY_DECIMALS);\n};\nconst modalHeightTransition = `height ${MODAL_HEIGHT_TRANSITION_MS}ms ease, max-height ${MODAL_HEIGHT_TRANSITION_MS}ms ease`;\n\nconst normalizeSdkExactOutStableSymbol = (symbol?: string) =>\n (symbol ?? \"\")\n .trim()\n .toUpperCase()\n .replaceAll(\"\\u20ae\", \"T\")\n .replaceAll(/[^A-Z0-9]/g, \"\");\n\nconst isSdkExactOutStableSymbol = (symbol?: string) =>\n SDK_EXACT_OUT_STABLE_SYMBOLS.has(normalizeSdkExactOutStableSymbol(symbol));\n\nconst applyPredictiveExactOutBufferUsd = (\n amount: Decimal,\n pct: number,\n maxUsd: number\n) => Decimal.min(amount.mul(pct), new Decimal(maxUsd));\n\nconst getPredictiveExactOutSourceTargetUsd = (\n destinationUsdNeedingSources: Decimal,\n cachedSourceUsdRatio?: Decimal\n) => {\n if (destinationUsdNeedingSources.lte(0)) return new Decimal(0);\n if (cachedSourceUsdRatio?.gt(0)) {\n return destinationUsdNeedingSources.mul(cachedSourceUsdRatio);\n }\n\n const destinationBuffer = applyPredictiveExactOutBufferUsd(\n destinationUsdNeedingSources,\n PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_PCT,\n PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_MAX_USD\n );\n const destinationBuffered = destinationUsdNeedingSources.plus(\n destinationBuffer\n );\n const sourceBuffer = applyPredictiveExactOutBufferUsd(\n destinationBuffered,\n PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_PCT,\n PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_MAX_USD\n );\n\n return destinationBuffered.plus(sourceBuffer);\n};\n\nconst getSwapHistoryStorageKey = (ownerAddress?: string) =>\n `${SWAP_HISTORY_STORAGE_KEY_PREFIX}:${ownerAddress?.toLowerCase() || \"anonymous\"}`;\n\nconst getTokenSelectionKey = (token?: SwapTokenOption | null) => {\n if (!token) return \"\";\n if (token.isUnified) {\n return `unified:${token.unifiedSymbol ?? token.symbol}`;\n }\n return `${token.chainId ?? \"unknown\"}:${token.contractAddress.toLowerCase()}`;\n};\n\nconst getDisplaySourceTokenKey = (token?: SwapTokenOption | null) => {\n if (!token) return \"\";\n const chainId = token.chainId ?? \"unknown\";\n const address = token.contractAddress?.toLowerCase();\n return address\n ? `${chainId}:${address}`\n : `${chainId}:symbol:${token.symbol.toUpperCase()}`;\n};\n\nconst sumDecimalStrings = (left?: string, right?: string) => {\n const leftAmount = parseDecimalLoose(left) ?? new Decimal(0);\n const rightAmount = parseDecimalLoose(right) ?? new Decimal(0);\n const sum = leftAmount.plus(rightAmount);\n return sum.gt(0) ? sum.toFixed() : undefined;\n};\n\nconst mergeDisplaySourceTokens = (tokens: SwapTokenOption[]) => {\n const merged = new Map();\n\n for (const token of tokens) {\n const key = getDisplaySourceTokenKey(token);\n if (!key) continue;\n const existing = merged.get(key);\n if (!existing) {\n merged.set(key, token);\n continue;\n }\n\n const userAmount = sumDecimalStrings(\n existing.userAmount || existing.balance,\n token.userAmount || token.balance\n );\n const userAmountUsd = sumDecimalStrings(\n existing.userAmountUsd || existing.balanceInFiat,\n token.userAmountUsd || token.balanceInFiat\n );\n\n merged.set(key, {\n ...existing,\n balance: userAmount ?? existing.balance,\n balanceInFiat: userAmountUsd ?? existing.balanceInFiat,\n chainLogo: existing.chainLogo || token.chainLogo,\n logo: existing.logo || token.logo,\n userAmount: userAmount ?? existing.userAmount,\n userAmountUsd: userAmountUsd ?? existing.userAmountUsd,\n });\n }\n\n return Array.from(merged.values());\n};\n\nconst getTokenQuoteKey = (token?: SwapTokenOption | null) => {\n if (!token) return \"\";\n return [\n getTokenSelectionKey(token),\n token.symbol ?? \"\",\n token.decimals ?? \"\",\n ].join(\":\");\n};\n\nconst getSourceTokensQuoteKey = (tokens: SwapTokenOption[]) =>\n tokens\n .filter((token) => {\n const amt = token.userAmount ?? \"\";\n const cleaned = amt.replaceAll(/[^0-9.]/g, \"\");\n const num = Number.parseFloat(cleaned);\n return !Number.isNaN(num) && num > 0;\n })\n .map((token) =>\n [\n getTokenSelectionKey(token),\n token.symbol ?? \"\",\n token.decimals ?? \"\",\n token.userAmount ?? \"\",\n token.userAmountUsd ?? \"\",\n token.userAmountMode ?? \"\",\n ].join(\":\")\n )\n .join(\"|\");\n\nconst getSourceTokenSyncKey = (token: SwapTokenOption) =>\n [\n getTokenSelectionKey(token),\n token.userAmount ?? \"\",\n token.userAmountUsd ?? \"\",\n token.balance ?? \"\",\n token.balanceInFiat ?? \"\",\n ].join(\":\");\n\nconst getSourceTokenListSyncKey = (tokens: SwapTokenOption[]) =>\n tokens.map(getSourceTokenSyncKey).join(\"|\");\n\nconst isSameTokenSelection = (\n a?: SwapTokenOption | null,\n b?: SwapTokenOption | null\n) => Boolean(a && b && getTokenSelectionKey(a) === getTokenSelectionKey(b));\n\nconst getDepositConfigIdentity = (deposit?: NexusWidgetDepositOpportunityMetadata | null) => {\n if (!deposit) return \"\";\n return [\n deposit.chainId,\n deposit.tokenAddress.toLowerCase(),\n deposit.tokenSymbol,\n deposit.tokenDecimals,\n deposit.protocol ?? \"\",\n deposit.title ?? \"\",\n ].join(\":\");\n};\n\nconst isSameDepositConfig = (\n a?: NexusWidgetDepositOpportunityConfig | null,\n b?: NexusWidgetDepositOpportunityConfig | null\n) => {\n if (!a || !b) return false;\n return getDepositConfigIdentity(a) === getDepositConfigIdentity(b);\n};\n\nconst getConfiguredReceiveSelectorRootHeight = (tokenCount: number) => {\n if (tokenCount <= 0) return TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT;\n const visibleRows = Math.min(Math.max(tokenCount, 1), 6);\n return Math.min(\n TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT,\n Math.ceil(\n (CONFIGURED_RECEIVE_SELECTOR_BASE_HEIGHT +\n visibleRows * CONFIGURED_RECEIVE_SELECTOR_ROW_HEIGHT) /\n 0.9\n )\n );\n};\n\nconst getConfiguredDeposit = (\n config: RuntimeNexusWidgetConfig\n): NexusWidgetDepositOpportunityConfig | undefined => config.deposit ?? config.deposits?.[0];\n\ntype RuntimeDestinationPair = {\n chain: number;\n decimals?: number;\n logo?: string;\n symbol?: string;\n token: `0x${string}`;\n};\n\ntype RuntimeNexusWidgetAppearance = NexusWidgetAppearance & {\n logoUrl?: string;\n themeMode?: \"auto\" | \"light\" | \"dark\";\n widgetHeading?: string;\n};\n\ntype RuntimeNexusWidgetConfig = {\n allowedDestinationChains?: number[];\n allowedDestinationPairs?: RuntimeDestinationPair[];\n allowedSourcePairs?: RuntimeDestinationPair[];\n appearance?: RuntimeNexusWidgetAppearance;\n deposit?: NexusWidgetDepositOpportunityConfig;\n deposits?: NexusWidgetDepositOpportunityConfig[];\n mode: NexusWidgetMode;\n prefill?: NexusWidgetRuntimePrefill;\n};\n\ntype RuntimeNexusWidgetAmountInput = {\n max?: string;\n min?: string;\n mode: \"fixed\" | \"user\";\n value?: string;\n};\n\ntype NormalizedNexusWidgetConfig = {\n activeMode: NexusWidgetMode;\n amountInput?: RuntimeNexusWidgetAmountInput;\n appearance?: RuntimeNexusWidgetAppearance;\n config: RuntimeNexusWidgetConfig;\n depositOptions: NexusWidgetDepositOpportunityConfig[];\n isAmountFixed: boolean;\n isRecipientLocked: boolean;\n};\n\nconst normalizeConfiguredString = (value: unknown) => {\n if (value === null || value === undefined) return undefined;\n const text = String(value).trim();\n return text || undefined;\n};\n\nconst normalizeConfiguredAddress = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n return text && isAddress(text) ? text : undefined;\n};\n\nconst isPositiveGasLimit = (value: unknown): value is bigint =>\n typeof value === \"bigint\" && value > BigInt(0);\n\nconst normalizePositiveNumericString = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n if (!text) return undefined;\n try {\n const decimal = new Decimal(text);\n return decimal.isFinite() && decimal.gt(0) ? text : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst normalizeNonNegativeNumericString = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n if (!text) return undefined;\n try {\n const decimal = new Decimal(text);\n return decimal.isFinite() && decimal.gte(0) ? text : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst normalizeHttpUrlString = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n if (!text) return undefined;\n return /^https?:\\/\\//i.test(text) ? text : undefined;\n};\n\nconst normalizeNexusWidgetPrimaryColor = (value?: string) => {\n return normalizeConfiguredString(value);\n};\n\nconst expandHexColor = (value: string) => {\n const text = value.trim();\n if (/^#[0-9a-fA-F]{6}$/.test(text)) return text;\n if (/^#[0-9a-fA-F]{3}$/.test(text)) {\n const [r, g, b] = text.slice(1);\n return `#${r}${r}${g}${g}${b}${b}`;\n }\n return undefined;\n};\n\nconst parseNumberComponent = (value: string, max: number) => {\n const text = value.trim();\n if (!text) return undefined;\n if (text.endsWith(\"%\")) {\n const percent = Number.parseFloat(text.slice(0, -1));\n return Number.isFinite(percent)\n ? Math.max(0, Math.min(max, (percent / 100) * max))\n : undefined;\n }\n const parsed = Number.parseFloat(text);\n return Number.isFinite(parsed) ? Math.max(0, Math.min(max, parsed)) : undefined;\n};\n\nconst parseAngleComponent = (value: string) => {\n const parsed = Number.parseFloat(value.trim());\n if (!Number.isFinite(parsed)) return undefined;\n return ((parsed % 360) + 360) % 360;\n};\n\nconst hslToRgb = (h: number, s: number, l: number): [number, number, number] => {\n const saturation = Math.max(0, Math.min(100, s)) / 100;\n const lightness = Math.max(0, Math.min(100, l)) / 100;\n const c = (1 - Math.abs(2 * lightness - 1)) * saturation;\n const hp = h / 60;\n const x = c * (1 - Math.abs((hp % 2) - 1));\n let r1 = 0;\n let g1 = 0;\n let b1 = 0;\n if (hp >= 0 && hp < 1) {\n r1 = c;\n g1 = x;\n } else if (hp >= 1 && hp < 2) {\n r1 = x;\n g1 = c;\n } else if (hp >= 2 && hp < 3) {\n g1 = c;\n b1 = x;\n } else if (hp >= 3 && hp < 4) {\n g1 = x;\n b1 = c;\n } else if (hp >= 4 && hp < 5) {\n r1 = x;\n b1 = c;\n } else {\n r1 = c;\n b1 = x;\n }\n const m = lightness - c / 2;\n return [\n Math.round((r1 + m) * 255),\n Math.round((g1 + m) * 255),\n Math.round((b1 + m) * 255),\n ];\n};\n\nconst parseCssColorToRgb = (\n color: string,\n allowBrowserResolution = true\n): [number, number, number] | undefined => {\n const text = normalizeConfiguredString(color);\n if (!text) return undefined;\n\n const hex = expandHexColor(text);\n if (hex) {\n return [\n Number.parseInt(hex.slice(1, 3), 16),\n Number.parseInt(hex.slice(3, 5), 16),\n Number.parseInt(hex.slice(5, 7), 16),\n ];\n }\n\n const rgbMatch = text.match(/^rgba?\\((.*)\\)$/i);\n if (rgbMatch) {\n const parts = rgbMatch[1].trim().split(/[\\s,\\/]+/).filter(Boolean);\n if (parts.length >= 3) {\n const r = parseNumberComponent(parts[0], 255);\n const g = parseNumberComponent(parts[1], 255);\n const b = parseNumberComponent(parts[2], 255);\n if (r !== undefined && g !== undefined && b !== undefined) {\n return [Math.round(r), Math.round(g), Math.round(b)];\n }\n }\n }\n\n const hslMatch = text.match(/^hsla?\\((.*)\\)$/i);\n if (hslMatch) {\n const parts = hslMatch[1].trim().split(/[\\s,\\/]+/).filter(Boolean);\n if (parts.length >= 3) {\n const h = parseAngleComponent(parts[0]);\n const s = parseNumberComponent(parts[1], 100);\n const l = parseNumberComponent(parts[2], 100);\n if (h !== undefined && s !== undefined && l !== undefined) {\n return hslToRgb(h, s, l);\n }\n }\n }\n\n if (allowBrowserResolution && typeof document !== \"undefined\") {\n const canvas = document.createElement(\"canvas\");\n const context = canvas.getContext(\"2d\");\n if (context) {\n const baseline = \"#010203\";\n context.fillStyle = baseline;\n context.fillStyle = text;\n const resolved = String(context.fillStyle);\n if (resolved !== baseline || text.toLowerCase() === baseline) {\n return parseCssColorToRgb(resolved, false);\n }\n }\n }\n\n return undefined;\n};\n\nconst getReadableTextColor = (background: string) => {\n const rgb = parseCssColorToRgb(background);\n if (!rgb) return \"#FFFFFE\";\n const [rValue, gValue, bValue] = rgb;\n const r = rValue / 255;\n const g = gValue / 255;\n const b = bValue / 255;\n const linear = [r, g, b].map((channel) =>\n channel <= 0.03928\n ? channel / 12.92\n : Math.pow((channel + 0.055) / 1.055, 2.4)\n );\n const luminance = 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];\n return luminance > 0.54 ? \"#161615\" : \"#FFFFFE\";\n};\n\nconst normalizeNexusWidgetAmountInput = (\n config: NexusWidgetConfig\n): RuntimeNexusWidgetAmountInput | undefined => {\n if (config.mode === \"swap\") return undefined;\n const value = normalizePositiveNumericString(\n \"amount\" in (config.prefill ?? {}) ? config.prefill?.amount : undefined\n );\n const min = normalizeNonNegativeNumericString(config.validation?.minAmount);\n const max = normalizePositiveNumericString(config.validation?.maxAmount);\n return value || min || max ? { mode: \"user\", value, min, max } : undefined;\n};\n\nconst normalizeNexusWidgetAppearance = (\n appearance?: NexusWidgetAppearance\n): RuntimeNexusWidgetAppearance | undefined => {\n if (!appearance) return undefined;\n const appName = normalizeConfiguredString(appearance.appName);\n const appLogoURL = normalizeHttpUrlString(appearance.appLogoURL);\n const heading = normalizeConfiguredString(appearance.heading);\n const mode =\n appearance.mode === \"dark\" || appearance.mode === \"light\"\n ? appearance.mode\n : \"system\";\n const primaryColor = normalizeConfiguredString(appearance.primaryColor);\n return {\n ...appearance,\n appLogoURL,\n appName,\n heading,\n logoUrl: appLogoURL,\n mode,\n primaryColor,\n themeMode: mode === \"system\" ? \"auto\" : mode,\n widgetHeading: heading,\n };\n};\n\nconst normalizeConfiguredChainId = (value: unknown) => {\n const chainId = Number(value);\n return Number.isFinite(chainId) && chainId > 0 ? chainId : undefined;\n};\n\nconst getConfiguredDestinationChainId = (\n destination?: NexusWidgetDestination\n) => normalizeConfiguredChainId(destination?.chain);\n\nconst getConfiguredDestinationTokenSymbol = (\n token: NexusWidgetDestinationToken\n) => normalizeConfiguredString(token.symbol);\n\nconst getConfiguredDestinationTokenAddress = (\n token: NexusWidgetDestinationToken\n) => normalizeConfiguredAddress(token.address);\n\nconst toSwapTokenOptionFromConfiguredDestinationToken = (\n token: NexusWidgetDestinationToken,\n chainId: number\n): SwapTokenOption | undefined => {\n const contractAddress = getConfiguredDestinationTokenAddress(token);\n const symbol = getConfiguredDestinationTokenSymbol(token);\n if (!contractAddress || !symbol) return undefined;\n const chainMeta = CHAIN_METADATA[chainId];\n return {\n balance: \"0\",\n balanceInFiat: \"$0.00\",\n chainId,\n chainLogo: chainMeta?.logo,\n chainName: getShortChainName(chainId, chainMeta?.name ?? `Chain ${chainId}`),\n contractAddress,\n decimals: token.decimals ?? 18,\n logo: token.logo ?? \"\",\n name: symbol,\n symbol,\n };\n};\n\nconst toSwapTokenOptionFromDepositConfig = (\n deposit: NexusWidgetDepositOpportunityConfig\n): SwapTokenOption => {\n const chainMeta = CHAIN_METADATA[deposit.chainId];\n return {\n balance: \"0\",\n balanceInFiat: \"$0.00\",\n chainId: deposit.chainId,\n chainLogo: chainMeta?.logo,\n chainName: getShortChainName(\n deposit.chainId,\n chainMeta?.name ?? `Chain ${deposit.chainId}`\n ),\n contractAddress: deposit.tokenAddress,\n decimals: deposit.tokenDecimals,\n logo: deposit.tokenLogo || deposit.logo || \"\",\n name: deposit.tokenSymbol,\n symbol: deposit.tokenSymbol,\n };\n};\n\nconst applyNexusWidgetAppearanceToDeposit = (\n deposit: NexusWidgetDepositOpportunityConfig,\n appearance?: RuntimeNexusWidgetAppearance\n): NexusWidgetDepositOpportunityConfig => {\n const appName = normalizeConfiguredString(appearance?.appName);\n const logoUrl = normalizeConfiguredString(appearance?.logoUrl);\n if (!appName && !logoUrl) return deposit;\n return {\n ...deposit,\n depositTargetLogo: logoUrl ?? deposit.depositTargetLogo,\n logo: logoUrl ?? deposit.logo,\n protocol: appName ?? deposit.protocol,\n title: appName ?? deposit.title,\n };\n};\n\nconst getConfiguredDestinationPairs = (\n destination?: NexusWidgetDestination\n) => {\n const pairs: RuntimeNexusWidgetConfig[\"allowedDestinationPairs\"] = [];\n const chain = getConfiguredDestinationChainId(destination);\n if (!chain) return pairs;\n for (const token of destination?.tokens ?? []) {\n const address = getConfiguredDestinationTokenAddress(token);\n if (!address) continue;\n pairs.push({ chain, token: address });\n }\n return pairs;\n};\n\nconst hasConfiguredDestinationTokenList = (\n destination?: NexusWidgetDestination\n) => Boolean(destination?.tokens?.length);\n\nconst getConfiguredPrefillTokenPair = (\n prefill?: { token?: NexusWidgetPrefillToken }\n): RuntimeDestinationPair | undefined => {\n const token = prefill?.token;\n if (!token) return undefined;\n const chain = normalizeConfiguredChainId(token.chain);\n const address = getConfiguredDestinationTokenAddress(token);\n const symbol = getConfiguredDestinationTokenSymbol(token);\n if (!chain || !address || !symbol) return undefined;\n return {\n chain,\n decimals: token.decimals,\n logo: normalizeConfiguredString(token.logo),\n symbol,\n token: address,\n };\n};\n\nconst getConfiguredDestinationChainIds = (\n destination?: NexusWidgetDestination\n) => {\n const chain = getConfiguredDestinationChainId(destination);\n return chain ? [chain] : [];\n};\n\nconst toDepositConfigFromDestination = (\n config: NexusWidgetDepositConfig,\n token: NexusWidgetDestinationToken,\n appearance?: RuntimeNexusWidgetAppearance\n): NexusWidgetDepositOpportunityConfig | undefined => {\n const chainId = getConfiguredDestinationChainId(config.destination);\n const depositAddress = normalizeConfiguredAddress(config.depositAddress);\n const tokenAddress = getConfiguredDestinationTokenAddress(token);\n const tokenSymbol = getConfiguredDestinationTokenSymbol(token);\n if (\n !chainId ||\n !depositAddress ||\n !tokenAddress ||\n !tokenSymbol ||\n typeof config.executeDeposit !== \"function\"\n ) {\n return undefined;\n }\n\n const chainMeta = CHAIN_METADATA[chainId];\n return {\n chainId,\n depositTargetLogo: appearance?.logoUrl,\n executeDeposit: config.executeDeposit,\n logo: appearance?.logoUrl ?? token.logo ?? undefined,\n protocol:\n appearance?.appName ??\n appearance?.widgetHeading ??\n \"Deposit\",\n subtitle:\n chainMeta?.name\n ? `on ${getShortChainName(chainId, chainMeta.name)}`\n : undefined,\n title: appearance?.appName ?? appearance?.widgetHeading ?? \"Deposit\",\n tokenAddress,\n tokenDecimals: token.decimals ?? 18,\n tokenLogo: token.logo,\n tokenSymbol,\n };\n};\n\nconst getNexusWidgetDepositOptions = (\n config: NexusWidgetConfig,\n appearance?: RuntimeNexusWidgetAppearance\n) => {\n if (config.mode !== \"deposit\") return [];\n return (config.destination.tokens ?? [])\n .map((token) => toDepositConfigFromDestination(config, token, appearance))\n .filter((deposit): deposit is NexusWidgetDepositOpportunityConfig => Boolean(deposit));\n};\n\nconst normalizeNexusWidgetConfig = (\n rawConfig: NexusWidgetConfig\n): NormalizedNexusWidgetConfig => {\n const appearance = normalizeNexusWidgetAppearance(rawConfig.appearance);\n const amountInput = normalizeNexusWidgetAmountInput(rawConfig);\n const prefill: NexusWidgetRuntimePrefill = {};\n let activeMode: NexusWidgetMode =\n rawConfig.mode === \"deposit\" ||\n rawConfig.mode === \"send\" ||\n rawConfig.mode === \"swap\"\n ? rawConfig.mode\n : \"swap\";\n let isRecipientLocked = false;\n const depositOptions = getNexusWidgetDepositOptions(rawConfig, appearance).map(\n (deposit) => applyNexusWidgetAppearanceToDeposit(deposit, appearance)\n );\n let allowedDestinationPairs:\n | RuntimeNexusWidgetConfig[\"allowedDestinationPairs\"]\n | undefined;\n let allowedDestinationChains:\n | RuntimeNexusWidgetConfig[\"allowedDestinationChains\"]\n | undefined;\n\n if (amountInput?.value) {\n prefill.amount = amountInput.value;\n }\n\n if (rawConfig.mode === \"deposit\") {\n activeMode = \"deposit\";\n const pairs = getConfiguredDestinationPairs(rawConfig.destination);\n if (pairs.length > 0) {\n allowedDestinationPairs = pairs;\n prefill.destination = pairs[0];\n }\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (chainIds.length > 0) {\n allowedDestinationChains = chainIds;\n }\n } else if (rawConfig.mode === \"send\") {\n const recipient = normalizeConfiguredAddress(rawConfig.recipientAddress);\n if (recipient) {\n prefill.recipient = recipient;\n isRecipientLocked = true;\n }\n\n const hasDestinationTokens = hasConfiguredDestinationTokenList(\n rawConfig.destination\n );\n const pairs = getConfiguredDestinationPairs(rawConfig.destination);\n if (pairs.length > 0) {\n allowedDestinationPairs = pairs;\n prefill.destination ??= pairs[0];\n } else if (!hasDestinationTokens) {\n const prefillToken = getConfiguredPrefillTokenPair(rawConfig.prefill);\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (\n prefillToken &&\n (!chainIds.length || chainIds.includes(prefillToken.chain))\n ) {\n prefill.destination = prefillToken;\n }\n }\n\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (chainIds.length > 0) {\n allowedDestinationChains = chainIds;\n }\n } else if (rawConfig.mode === \"swap\") {\n const recipient = normalizeConfiguredAddress(rawConfig.recipientAddress);\n if (recipient) {\n prefill.recipient = recipient;\n }\n\n const hasDestinationTokens = hasConfiguredDestinationTokenList(\n rawConfig.destination\n );\n const pairs = getConfiguredDestinationPairs(rawConfig.destination);\n if (pairs.length > 0) {\n allowedDestinationPairs = pairs;\n prefill.destination ??= pairs[0];\n } else if (!hasDestinationTokens) {\n const prefillToken = getConfiguredPrefillTokenPair(rawConfig.prefill);\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (\n prefillToken &&\n (!chainIds.length || chainIds.includes(prefillToken.chain))\n ) {\n prefill.destination = prefillToken;\n }\n }\n\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (chainIds.length > 0) {\n allowedDestinationChains = chainIds;\n }\n }\n\n const runtimeConfig: RuntimeNexusWidgetConfig = {\n allowedDestinationChains,\n allowedDestinationPairs,\n deposit:\n activeMode === \"deposit\"\n ? depositOptions[0]\n : undefined,\n deposits: activeMode === \"deposit\" ? depositOptions : [],\n appearance,\n mode: activeMode,\n prefill,\n };\n\n return {\n activeMode,\n amountInput,\n appearance,\n config: runtimeConfig,\n depositOptions,\n isAmountFixed: false,\n isRecipientLocked,\n };\n};\n\nconst getDepositForTokenSelection = (\n deposits: NexusWidgetDepositOpportunityConfig[],\n token?: SwapTokenOption | null\n) => {\n if (!token) return undefined;\n return deposits.find(\n (deposit) =>\n deposit.chainId === token.chainId &&\n deposit.tokenAddress.toLowerCase() ===\n token.contractAddress.toLowerCase()\n );\n};\n\nconst sanitizeOpportunityForHistory = (\n opportunity?: NexusWidgetDepositOpportunityMetadata\n): NexusWidgetDepositOpportunityMetadata | undefined => {\n if (!opportunity) return undefined;\n return {\n label: opportunity.label,\n protocol: opportunity.protocol,\n logo: opportunity.logo,\n title: opportunity.title,\n subtitle: opportunity.subtitle,\n chainId: opportunity.chainId,\n tokenSymbol: opportunity.tokenSymbol,\n tokenDecimals: opportunity.tokenDecimals,\n tokenLogo: opportunity.tokenLogo,\n tokenAddress: opportunity.tokenAddress,\n apy: opportunity.apy,\n description: opportunity.description,\n };\n};\n\nconst sanitizeHistoryEntry = (entry: SwapHistoryEntry): SwapHistoryEntry => ({\n ...entry,\n createdAt: entry.createdAt ?? entry.startedAt ?? Date.now(),\n failureMessage:\n entry.status === \"timeout\" ? TIMEOUT_LABEL : entry.failureMessage,\n opportunity: sanitizeOpportunityForHistory(entry.opportunity),\n});\n\nconst sortSwapHistoryEntries = (entries: SwapHistoryEntry[]) =>\n [...entries].sort(\n (a, b) =>\n (b.createdAt ?? b.startedAt ?? 0) - (a.createdAt ?? a.startedAt ?? 0)\n );\n\nconst isStoredHistoryStatus = (value: unknown): value is SwapHistoryStatus =>\n value === \"pending\" ||\n value === \"fulfilled\" ||\n value === \"failed\" ||\n value === \"timeout\" ||\n value === \"refund-initiated\";\n\nconst isStoredMode = (value: unknown): value is NexusWidgetMode =>\n value === \"swap\" || value === \"deposit\" || value === \"send\";\n\nconst normalizeStoredHistoryEntry = (\n value: unknown\n): SwapHistoryEntry | null => {\n if (!value || typeof value !== \"object\") return null;\n const entry = value as Partial;\n const startedAt =\n typeof entry.startedAt === \"number\" && Number.isFinite(entry.startedAt)\n ? entry.startedAt\n : undefined;\n const createdAt =\n typeof entry.createdAt === \"number\" && Number.isFinite(entry.createdAt)\n ? entry.createdAt\n : startedAt;\n\n if (\n !entry.id ||\n typeof entry.id !== \"string\" ||\n !isStoredMode(entry.mode) ||\n !isStoredHistoryStatus(entry.status) ||\n !createdAt ||\n !startedAt\n ) {\n return null;\n }\n\n return {\n ...entry,\n id: entry.id,\n mode: entry.mode,\n status: entry.status,\n createdAt,\n startedAt,\n failureMessage:\n entry.status === \"timeout\" ? TIMEOUT_LABEL : entry.failureMessage,\n intentData: entry.intentData ?? null,\n fromTokens: Array.isArray(entry.fromTokens) ? entry.fromTokens : [],\n opportunity: sanitizeOpportunityForHistory(entry.opportunity),\n } as SwapHistoryEntry;\n};\n\nconst readSwapHistoryFromStorage = (storageKey: string): SwapHistoryEntry[] => {\n if (typeof window === \"undefined\") return [];\n\n try {\n const raw = window.localStorage.getItem(storageKey);\n if (!raw) return [];\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n return sortSwapHistoryEntries(\n parsed\n .map(normalizeStoredHistoryEntry)\n .filter((entry): entry is SwapHistoryEntry => Boolean(entry))\n );\n } catch {\n return [];\n }\n};\n\nconst writeSwapHistoryToStorage = (\n storageKey: string,\n entries: SwapHistoryEntry[]\n) => {\n if (typeof window === \"undefined\") return;\n\n try {\n const persistableEntries =\n sortSwapHistoryEntries(entries).map(sanitizeHistoryEntry);\n window.localStorage.setItem(\n storageKey,\n JSON.stringify(persistableEntries, (_key, value) =>\n typeof value === \"bigint\" ? value.toString() : value\n )\n );\n } catch {\n // localStorage can be unavailable or full; in-memory history still works.\n }\n};\n\nfunction QuoteRefreshCountdown({\n progress,\n isRefreshing,\n secondsRemaining,\n}: {\n progress: number;\n isRefreshing: boolean;\n secondsRemaining: number;\n}) {\n const [showTooltip, setShowTooltip] = useState(false);\n const radius = 7;\n const circumference = 2 * Math.PI * radius;\n const clampedProgress = Math.max(0, Math.min(1, progress));\n const tooltipLabel = isRefreshing\n ? \"Refreshing quotes...\"\n : `Refreshing quotes in ${Math.max(0, secondsRemaining)} second${\n secondsRemaining === 1 ? \"\" : \"s\"\n }`;\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n style={{\n alignItems: \"center\",\n backgroundColor: \"#FFFFFE\",\n borderRadius: \"999px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n height: \"22px\",\n justifyContent: \"center\",\n outline: \"1px solid #E8E8E7\",\n position: \"relative\",\n width: \"22px\",\n }}\n tabIndex={0}\n >\n {showTooltip && (\n \n {tooltipLabel}\n \n )}\n \n \n \n \n \n );\n}\n\nconst normalizeDecimalInputText = (value: unknown) => {\n const raw = String(value).trim();\n if (!raw) return \"\";\n if (SCIENTIFIC_DECIMAL_REGEX.test(raw)) return raw;\n return raw.replace(/[^0-9.-]/g, \"\");\n};\n\nconst parseDecimalLoose = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = normalizeDecimalInputText(value);\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst toViemDecimalString = (value: unknown, decimals: number) => {\n const parsed = parseDecimalLoose(value);\n if (!parsed || parsed.lte(0)) return \"0\";\n return parsed\n .toDecimalPlaces(Math.max(0, decimals), Decimal.ROUND_DOWN)\n .toFixed();\n};\n\nconst formatDecimalDisplay = (\n value: unknown,\n options: { min?: number; max?: number } = {}\n) => {\n const amount = parseDecimalLoose(value) ?? new Decimal(0);\n const max = options.max ?? 2;\n return amount.toDecimalPlaces(max).toFixed();\n};\n\nconst formatUsdDisplay = (value: unknown) => {\n const amount = parseDecimalLoose(value) ?? new Decimal(0);\n if (amount.gt(0) && amount.lt(0.01)) return \"<$0.01\";\n return `$${formatDecimalDisplay(amount, { min: 2, max: 2 })}`;\n};\n\nconst formatTokenDisplay = (value: unknown) => {\n const amount = parseDecimalLoose(value) ?? new Decimal(0);\n return formatDecimalDisplay(amount, { max: 8 });\n};\n\nconst getSwapTokenUsdValue = (token: SwapTokenOption) =>\n parseDecimalLoose(token.userAmountUsd) ??\n parseDecimalLoose(token.balanceInFiat) ??\n new Decimal(0);\n\nconst getSwapTokenBalanceUsdValue = (token: SwapTokenOption) =>\n parseDecimalLoose(token.balanceInFiat) ?? new Decimal(0);\n\nconst sortSwapTokensByUsdDesc = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const usdDelta = getSwapTokenUsdValue(b).cmp(getSwapTokenUsdValue(a));\n if (usdDelta !== 0) return usdDelta;\n return (a.symbol ?? \"\").localeCompare(b.symbol ?? \"\");\n });\n\nconst sortDisplaySourcesByBalanceUsdDesc = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const balanceUsdDelta = getSwapTokenBalanceUsdValue(b).cmp(\n getSwapTokenBalanceUsdValue(a)\n );\n if (balanceUsdDelta !== 0) return balanceUsdDelta;\n\n const spendUsdDelta = getSwapTokenUsdValue(b).cmp(getSwapTokenUsdValue(a));\n if (spendUsdDelta !== 0) return spendUsdDelta;\n\n return `${a.symbol ?? \"\"} ${a.chainName ?? \"\"}`.localeCompare(\n `${b.symbol ?? \"\"} ${b.chainName ?? \"\"}`\n );\n });\n\nconst getIntentSourceUsdValue = (source: SwapIntentData[\"sources\"][number]) =>\n parseDecimalLoose(source.value) ?? new Decimal(0);\n\nconst sortIntentSourcesByUsdDesc = (sources: SwapIntentData[\"sources\"]) =>\n [...sources].sort((a, b) => {\n const usdDelta = getIntentSourceUsdValue(b).cmp(getIntentSourceUsdValue(a));\n if (usdDelta !== 0) return usdDelta;\n return (a.token?.symbol ?? \"\").localeCompare(b.token?.symbol ?? \"\");\n });\n\nconst extractIntentIdFromUrl = (url?: string | null) => {\n if (!url) return undefined;\n const match = url.match(/(\\d+)(?:\\/)?$/);\n if (!match) return undefined;\n const parsed = Number(match[1]);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;\n};\n\nconst getNonEmptyString = (...values: unknown[]) => {\n for (const value of values) {\n if (typeof value !== \"string\") continue;\n const trimmed = value.trim();\n if (trimmed.length > 0) return trimmed;\n }\n return null;\n};\n\nconst isHttpUrl = (value?: string | null): value is string =>\n Boolean(value && /^https?:\\/\\//i.test(value));\n\nconst hasValidIntentExplorer = (\n entry: Pick\n) => isHttpUrl(entry.intentExplorerUrl);\n\nconst getHistoryExplorerUrl = (\n entry: Pick<\n SwapHistoryEntry,\n \"finalExplorerUrl\" | \"intentExplorerUrl\" | \"sourceExplorerUrl\"\n >\n) =>\n [\n entry.intentExplorerUrl,\n entry.finalExplorerUrl,\n entry.sourceExplorerUrl,\n ].find(isHttpUrl) ?? null;\n\nconst getFiniteNumber = (...values: unknown[]) => {\n for (const value of values) {\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value !== \"string\") continue;\n const parsed = Number(value);\n if (Number.isFinite(parsed)) return parsed;\n }\n return undefined;\n};\n\nconst getObjectChainId = (value: any) =>\n getFiniteNumber(\n value?.chainId,\n value?.chain?.id,\n value?.chain?.chainId,\n value?.toChainId,\n value?.destinationChainId,\n value?.data?.chainId,\n value?.data?.chain?.id,\n value?.data?.chain?.chainId,\n value?.data?.toChainId,\n value?.data?.destinationChainId,\n value?.result?.chainId,\n value?.result?.chain?.id\n );\n\nconst getExplorerBaseUrl = (chainId?: number, ...candidates: unknown[]) => {\n const directCandidates = candidates.flatMap((candidate: any) => [\n candidate?.blockExplorerUrl,\n candidate?.blockExplorerURL,\n candidate?.chainBlockExplorerUrl,\n candidate?.explorerBaseUrl,\n candidate?.explorerUrlBase,\n candidate?.blockExplorerUrls?.[0],\n candidate?.blockExplorers?.default?.url,\n candidate?.chain?.blockExplorerUrl,\n candidate?.chain?.blockExplorerURL,\n candidate?.chain?.chainBlockExplorerUrl,\n candidate?.chain?.explorerBaseUrl,\n candidate?.chain?.explorerUrlBase,\n candidate?.chain?.blockExplorerUrls?.[0],\n candidate?.chain?.blockExplorers?.default?.url,\n candidate?.data?.blockExplorerUrl,\n candidate?.data?.blockExplorerURL,\n candidate?.data?.chainBlockExplorerUrl,\n candidate?.data?.blockExplorerUrls?.[0],\n candidate?.data?.blockExplorers?.default?.url,\n candidate?.data?.chain?.blockExplorerUrl,\n candidate?.data?.chain?.blockExplorerURL,\n candidate?.data?.chain?.chainBlockExplorerUrl,\n candidate?.data?.chain?.blockExplorerUrls?.[0],\n candidate?.data?.chain?.blockExplorers?.default?.url,\n ]);\n return getNonEmptyString(\n ...directCandidates,\n chainId ? CHAIN_METADATA[chainId]?.blockExplorerUrls?.[0] : undefined,\n chainId\n ? (CHAIN_METADATA[chainId] as any)?.blockExplorers?.default?.url\n : undefined\n );\n};\n\nconst getTransactionHash = (...values: unknown[]) => {\n for (const value of values) {\n if (typeof value !== \"string\") continue;\n const trimmed = value.trim();\n if (/^0x[a-fA-F0-9]{64}$/.test(trimmed)) return trimmed;\n }\n return null;\n};\n\nconst getIntentHash = (...values: unknown[]) => getTransactionHash(...values);\n\nconst getObjectIntentHash = (value: any) =>\n getIntentHash(\n value?.intentHash,\n value?.intent_hash,\n value?.intent?.hash,\n value?.intent?.intentHash,\n value?.intent?.intent_hash,\n value?.requestHash,\n value?.request_hash,\n value?.request?.hash,\n value?.request?.requestHash,\n value?.rffHash,\n value?.rff_hash,\n value?.rff?.hash,\n value?.data?.intentHash,\n value?.data?.intent_hash,\n value?.data?.intent?.hash,\n value?.data?.intent?.intentHash,\n value?.data?.intent?.intent_hash,\n value?.data?.requestHash,\n value?.data?.request_hash,\n value?.data?.request?.hash,\n value?.data?.request?.requestHash,\n value?.data?.rffHash,\n value?.data?.rff_hash,\n value?.data?.rff?.hash,\n value?.result?.intentHash,\n value?.result?.intent_hash,\n value?.result?.intent?.hash,\n value?.result?.requestHash,\n value?.result?.request_hash,\n value?.result?.rffHash,\n value?.result?.rff_hash\n );\n\nconst getNexusExplorerNetwork = (network?: unknown) => {\n const normalized =\n typeof network === \"string\" ? network.trim().toLowerCase() : \"\";\n if (normalized === \"canary\" || normalized === \"testnet\") return normalized;\n return \"mainnet\";\n};\n\nconst getRffExplorerUrl = (network: unknown, intentHash?: string | null) =>\n intentHash\n ? `https://nexus-v2.${getNexusExplorerNetwork(network)}.avail.so/rff/${intentHash}`\n : null;\n\nconst getObjectTransactionHash = (value: any) =>\n getTransactionHash(\n value?.txHash,\n value?.transactionHash,\n value?.executeTxHash,\n value?.executeTransactionHash,\n value?.transferTransactionHash,\n value?.receipt?.transactionHash,\n value?.tx?.hash,\n value?.transaction?.hash,\n value?.data?.txHash,\n value?.data?.transactionHash,\n value?.data?.executeTxHash,\n value?.data?.executeTransactionHash,\n value?.data?.transferTransactionHash,\n value?.data?.receipt?.transactionHash,\n value?.data?.tx?.hash,\n value?.data?.transaction?.hash,\n value?.result?.txHash,\n value?.result?.transactionHash,\n value?.result?.receipt?.transactionHash\n );\n\nconst getExplorerTxUrl = (\n chainId?: number,\n txHash?: string | null,\n ...candidates: unknown[]\n) => {\n if (!chainId || !txHash) return null;\n const baseUrl = getExplorerBaseUrl(chainId, ...candidates);\n return baseUrl ? `${String(baseUrl).replace(/\\/$/, \"\")}/tx/${txHash}` : null;\n};\n\nconst getSdkSwapResult = (result: any) => {\n const candidate = result?.swapResult ?? result?.result;\n return candidate && typeof candidate === \"object\" ? candidate : null;\n};\n\nconst getSdkTransactionHash = (result: any) =>\n getObjectTransactionHash(result) ||\n getObjectTransactionHash(result?.executeResponse) ||\n getObjectTransactionHash(result?.execute) ||\n getObjectTransactionHash(result?.transfer) ||\n getObjectTransactionHash(result?.swapResult) ||\n getObjectTransactionHash(result?.result) ||\n null;\n\nconst getSdkExplorerUrl = (result: any) =>\n getNonEmptyString(\n result?.explorerUrl,\n result?.explorerURL,\n result?.txExplorerUrl,\n result?.transactionExplorerUrl,\n result?.execute?.explorerUrl,\n result?.execute?.explorerURL,\n result?.execute?.txExplorerUrl,\n result?.execute?.transactionExplorerUrl,\n result?.executeResponse?.explorerUrl,\n result?.executeResponse?.explorerURL,\n result?.executeResponse?.txExplorerUrl,\n result?.executeResponse?.transactionExplorerUrl,\n result?.executeExplorerUrl,\n result?.transferExplorerUrl,\n result?.swapResult?.explorerUrl,\n result?.swapResult?.explorerURL,\n result?.swapResult?.txExplorerUrl,\n result?.swapResult?.transactionExplorerUrl,\n result?.result?.explorerUrl,\n result?.result?.explorerURL,\n result?.result?.txExplorerUrl,\n result?.result?.transactionExplorerUrl\n );\n\nconst getSdkIntentExplorerUrl = (result: any, swapResult?: any) =>\n getNonEmptyString(\n swapResult?.intentExplorerUrl,\n swapResult?.intentExplorerURL,\n swapResult?.intentUrl,\n swapResult?.intentURL,\n swapResult?.rffUrl,\n swapResult?.rffURL,\n swapResult?.rffExplorerUrl,\n swapResult?.rffExplorerURL,\n swapResult?.explorerUrl,\n swapResult?.explorerURL,\n result?.intentExplorerUrl,\n result?.intentExplorerURL,\n result?.intentUrl,\n result?.intentURL,\n result?.rffUrl,\n result?.rffURL,\n result?.rffExplorerUrl,\n result?.rffExplorerURL,\n result?.swapResult?.intentExplorerUrl,\n result?.swapResult?.intentExplorerURL,\n result?.swapResult?.intentUrl,\n result?.swapResult?.intentURL,\n result?.swapResult?.rffUrl,\n result?.swapResult?.rffURL,\n result?.swapResult?.rffExplorerUrl,\n result?.swapResult?.rffExplorerURL,\n result?.swapResult?.explorerUrl,\n result?.swapResult?.explorerURL,\n result?.result?.intentExplorerUrl,\n result?.result?.intentExplorerURL,\n result?.result?.intentUrl,\n result?.result?.intentURL,\n result?.result?.rffUrl,\n result?.result?.rffURL,\n result?.result?.rffExplorerUrl,\n result?.result?.rffExplorerURL,\n result?.result?.explorerUrl,\n result?.result?.explorerURL\n );\n\nconst getSdkIntentExplorerUrlForNetwork = (\n network: unknown,\n result: any,\n swapResult?: any\n) =>\n getSdkIntentExplorerUrl(result, swapResult) ||\n getRffExplorerUrl(\n network,\n getObjectIntentHash(swapResult) || getObjectIntentHash(result)\n );\n\nfunction MiniLogo({\n src,\n label,\n size = 30,\n fontSize = 13,\n outline,\n style,\n}: {\n src?: string;\n label?: string;\n size?: number;\n fontSize?: number;\n outline?: string;\n style?: React.CSSProperties;\n}) {\n const [failed, setFailed] = useState(!src);\n\n useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n background: \"#FFFFFE\",\n borderRadius: \"999px\",\n height: size,\n objectFit: \"cover\",\n outline,\n width: size,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {(label || \"?\").trim().slice(0, 1).toUpperCase()}\n \n );\n}\n\nfunction TokenLogoPair({\n tokenLogo,\n chainLogo,\n tokenSymbol,\n chainName,\n tokenOutline,\n size = 34,\n}: {\n tokenLogo?: string;\n chainLogo?: string;\n tokenSymbol?: string;\n chainName?: string;\n tokenOutline?: string;\n size?: number;\n}) {\n return (\n \n \n {chainLogo && (\n \n )}\n \n );\n}\n\nfunction SourceLogoStack({\n sources,\n size = 24,\n maxVisible = 3,\n}: {\n sources: HistorySourceRow[];\n size?: number;\n maxVisible?: number;\n}) {\n const visibleSources = sources.slice(0, maxVisible);\n const hiddenCount = Math.max(0, sources.length - visibleSources.length);\n\n return (\n \n {visibleSources.map((source, index) => (\n \n \n \n ))}\n {hiddenCount > 0 && (\n \n +{hiddenCount}\n \n )}\n \n );\n}\n\nfunction TruncatedAddress({\n address,\n color = \"var(--foreground-brand)\",\n}: {\n address: string;\n color?: string;\n}) {\n const [showTooltip, setShowTooltip] = useState(false);\n const label =\n address.length > 12\n ? `${address.slice(0, 6)}...${address.slice(-4)}`\n : address;\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n style={{\n color,\n display: \"inline-flex\",\n fontFamily: uiFont,\n fontSize: \"15px\",\n fontWeight: 500,\n lineHeight: \"20px\",\n outline: \"none\",\n position: \"relative\",\n }}\n tabIndex={0}\n >\n {label}\n {showTooltip && (\n \n {address}\n \n )}\n \n );\n}\n\nconst getDisplayDestinationSourceRow = (\n entry: SwapHistoryEntry\n): HistorySourceRow | null => {\n if (entry.mode !== \"deposit\" && entry.mode !== \"send\") return null;\n if (!entry.toToken || !entry.requestedToAmount) return null;\n\n const requestedAmount = parseDecimalLoose(entry.requestedToAmount);\n const intentDestinationAmount = parseDecimalLoose(\n entry.intentData?.destination.amount\n );\n const destinationBalanceAmount = parseDecimalLoose(\n entry.toToken.balance?.replace(entry.toToken.symbol, \"\")\n );\n if (\n !requestedAmount ||\n !destinationBalanceAmount ||\n requestedAmount.lte(0) ||\n destinationBalanceAmount.lte(0)\n ) {\n return null;\n }\n\n const intentCoversAmount = intentDestinationAmount ?? new Decimal(0);\n const displayAmount = Decimal.min(\n destinationBalanceAmount,\n Decimal.max(0, requestedAmount.minus(intentCoversAmount))\n );\n if (displayAmount.lte(0)) return null;\n\n const requestedValue = parseDecimalLoose(entry.requestedToValue);\n const destinationValue = parseDecimalLoose(\n entry.intentData?.destination.value\n );\n const rate =\n requestedValue && requestedAmount.gt(0)\n ? requestedValue.div(requestedAmount)\n : destinationValue && intentCoversAmount.gt(0)\n ? destinationValue.div(intentCoversAmount)\n : undefined;\n\n return {\n key: `destination-balance-${entry.toToken.chainId}-${entry.toToken.contractAddress}`,\n chainId: entry.toToken.chainId,\n contractAddress: entry.toToken.contractAddress,\n tokenLogo: entry.intentData?.destination.token.logo || entry.toToken.logo,\n chainLogo: entry.toToken.chainLogo,\n symbol: entry.toToken.symbol,\n chainName: getShortChainName(\n entry.toToken.chainId,\n entry.toToken.chainName\n ),\n amount: displayAmount\n .toDecimalPlaces(\n Math.max(0, entry.toToken.decimals ?? 18),\n Decimal.ROUND_DOWN\n )\n .toFixed(),\n value: rate\n ? displayAmount.mul(rate).toFixed()\n : entry.toToken.balanceInFiat,\n };\n};\n\nconst getHistorySourceRowMergeKey = (row: HistorySourceRow) => {\n const chainKey = row.chainId ?? row.chainName;\n const address = row.contractAddress?.toLowerCase();\n return address\n ? `${chainKey}:${address}`\n : `${chainKey}:symbol:${row.symbol.toUpperCase()}`;\n};\n\nconst mergeHistorySourceRows = (rows: HistorySourceRow[]) => {\n const merged = new Map();\n\n for (const row of rows) {\n const key = getHistorySourceRowMergeKey(row);\n const existing = merged.get(key);\n if (!existing) {\n merged.set(key, row);\n continue;\n }\n\n merged.set(key, {\n ...existing,\n amount: sumDecimalStrings(existing.amount, row.amount) ?? existing.amount,\n chainLogo: existing.chainLogo || row.chainLogo,\n key: existing.key,\n tokenLogo: existing.tokenLogo || row.tokenLogo,\n value: sumDecimalStrings(\n String(existing.value ?? \"\"),\n String(row.value ?? \"\")\n ),\n });\n }\n\n return Array.from(merged.values());\n};\n\nconst getProgressStepType = (step?: SwapStepType | BridgeStepType | null) =>\n String((step as any)?.type ?? (step as any)?.typeID ?? \"\").toUpperCase();\n\nconst isSourceMovementProgressStep = (type: string, rawStepType: string) =>\n rawStepType === \"source_swap\" ||\n rawStepType === \"eoa_to_ephemeral_transfer\" ||\n rawStepType === \"bridge_deposit\" ||\n rawStepType === \"vault_deposit\" ||\n type.includes(\"SOURCE_SWAP\") ||\n type.includes(\"EOA_TO_EPHEMERAL_TRANSFER\") ||\n type.includes(\"BRIDGE_DEPOSIT\") ||\n type.includes(\"VAULT_DEPOSIT\");\n\nconst isBridgeRefundStepType = (type: string) =>\n type.includes(\"BRIDGE_INTENT_SUBMISSION\") || type.includes(\"BRIDGE_DEPOSIT\");\n\nconst isSwapSkippedStepType = (type: string) => type.includes(\"SWAP_SKIPPED\");\n\nconst isAutoRefundAvailableProgressEvent = (event?: NexusWidgetProgressEvent) =>\n event?.name === PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS &&\n isBridgeRefundStepType(getProgressStepType(event.step));\n\nconst normalizeBridgeProvider = (\n value: unknown\n): BridgeProvider | undefined => {\n if (value === \"nexus\" || value === \"mayan\" || value === null) {\n return value;\n }\n return undefined;\n};\n\nconst normalizePlanStepType = (stepType: unknown, state?: unknown) => {\n const normalized = String(stepType ?? \"\").toLowerCase();\n const normalizedState = String(state ?? \"\").toLowerCase();\n\n if (normalized === \"execute_transaction\") {\n return normalizedState === \"confirmed\" || normalizedState === \"completed\"\n ? \"TRANSACTION_CONFIRMED\"\n : \"TRANSACTION_SENT\";\n }\n\n const mapped: Record = {\n allowance_approval: \"APPROVAL\",\n bridge_deposit: \"BRIDGE_DEPOSIT\",\n bridge_fill: \"BRIDGE_FILL\",\n bridge_intent_submission: \"BRIDGE_INTENT_SUBMISSION\",\n destination_swap: \"DESTINATION_SWAP\",\n eoa_to_ephemeral_transfer: \"EOA_TO_EPHEMERAL_TRANSFER\",\n execute_approval: \"APPROVAL\",\n request_signing: \"REQUEST_SIGNING\",\n request_submission: \"REQUEST_SUBMISSION\",\n source_swap: \"SOURCE_SWAP\",\n vault_deposit: \"BRIDGE_DEPOSIT\",\n };\n\n return mapped[normalized] ?? normalized.toUpperCase();\n};\n\nconst normalizePlanStep = (\n stepLike: unknown,\n fallbackStepType?: unknown,\n state?: unknown,\n completed?: boolean\n): SwapStepType | BridgeStepType => {\n const source =\n stepLike && typeof stepLike === \"object\" ? (stepLike as any) : {};\n const rawStepType = fallbackStepType ?? source.stepType ?? source.type;\n const progressType = normalizePlanStepType(\n rawStepType ?? source.typeID,\n state\n );\n const progressKey =\n source.id ?? source.stepId ?? source.typeID ?? progressType;\n\n return {\n ...source,\n completed,\n rawType: rawStepType,\n type: progressType,\n typeID: String(progressKey),\n } as SwapStepType | BridgeStepType;\n};\n\nconst getPlanStepChainId = (event: any, step: any) =>\n getObjectChainId(event) ?? getObjectChainId(step);\n\nconst getPlanStepTransactionHash = (event: any, step: any) =>\n getObjectTransactionHash(event) ?? getObjectTransactionHash(step);\n\nconst getPlanStepExplorerUrl = (event: any, step: any) => {\n const directExplorerUrl = getNonEmptyString(\n event?.explorerUrl,\n event?.explorerURL,\n event?.txExplorerUrl,\n event?.transactionExplorerUrl,\n step?.explorerUrl,\n step?.explorerURL,\n step?.txExplorerUrl,\n step?.transactionExplorerUrl,\n step?.data?.explorerUrl,\n step?.data?.explorerURL,\n step?.data?.txExplorerUrl,\n step?.data?.transactionExplorerUrl\n );\n if (directExplorerUrl) return directExplorerUrl;\n\n return getExplorerTxUrl(\n getPlanStepChainId(event, step),\n getPlanStepTransactionHash(event, step),\n event,\n step\n );\n};\n\nconst getPlanStepIntentExplorerUrl = (event: any, step: any) =>\n getNonEmptyString(\n event?.intentExplorerUrl,\n event?.intentExplorerURL,\n event?.intentUrl,\n event?.intentURL,\n event?.rffUrl,\n event?.rffURL,\n event?.rffExplorerUrl,\n event?.rffExplorerURL,\n step?.intentExplorerUrl,\n step?.intentExplorerURL,\n step?.intentUrl,\n step?.intentURL,\n step?.rffUrl,\n step?.rffURL,\n step?.rffExplorerUrl,\n step?.rffExplorerURL,\n step?.data?.intentExplorerUrl,\n step?.data?.intentExplorerURL,\n step?.data?.intentUrl,\n step?.data?.intentURL,\n step?.data?.rffUrl,\n step?.data?.rffURL,\n step?.data?.rffExplorerUrl,\n step?.data?.rffExplorerURL\n );\n\nconst isIntentSubmissionLikeEvent = (event: any, step?: any) => {\n const text = [\n event?.type,\n event?.event,\n event?.name,\n event?.status,\n event?.stepType,\n step?.type,\n step?.typeID,\n step?.rawType,\n ]\n .filter((value) => value !== null && value !== undefined)\n .join(\" \")\n .toLowerCase();\n\n return (\n text.includes(\"intent\") ||\n text.includes(\"request_submission\") ||\n text.includes(\"rff\")\n );\n};\n\nconst getGenericEventHash = (event: any, step?: any) =>\n getTransactionHash(\n event?.hash,\n event?.data?.hash,\n event?.result?.hash,\n step?.hash,\n step?.data?.hash,\n step?.result?.hash\n );\n\nconst getEventIntentExplorerUrl = (\n network: unknown,\n event: any,\n step?: any\n) => {\n const directUrl = step\n ? getPlanStepIntentExplorerUrl(event, step)\n : getNonEmptyString(\n event?.intentExplorerUrl,\n event?.intentExplorerURL,\n event?.intentUrl,\n event?.intentURL,\n event?.rffUrl,\n event?.rffURL,\n event?.rffExplorerUrl,\n event?.rffExplorerURL,\n event?.data?.intentExplorerUrl,\n event?.data?.intentExplorerURL,\n event?.data?.intentUrl,\n event?.data?.intentURL,\n event?.data?.rffUrl,\n event?.data?.rffURL,\n event?.data?.rffExplorerUrl,\n event?.data?.rffExplorerURL\n );\n if (directUrl) return directUrl;\n\n return getRffExplorerUrl(\n network,\n getObjectIntentHash(event) ||\n getObjectIntentHash(step) ||\n (isIntentSubmissionLikeEvent(event, step)\n ? getGenericEventHash(event, step)\n : null)\n );\n};\n\nconst isTimeoutLikeError = (error: unknown) => {\n const err = error as {\n code?: unknown;\n name?: unknown;\n message?: unknown;\n shortMessage?: unknown;\n };\n const text = [\n err?.code,\n err?.name,\n err?.message,\n err?.shortMessage,\n typeof error === \"string\" ? error : undefined,\n ]\n .filter((value) => value !== null && value !== undefined)\n .join(\" \");\n\n return /timeout|timed out|time out|deadline exceeded|expired while waiting|wait.*expired|poll.*expired/i.test(\n text\n );\n};\n\nconst getSdkEventType = (event: any) =>\n event?.type ?? event?.name ?? event?.event ?? \"unknown\";\n\nconst summarizeSdkProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n index?: number\n) => ({\n completed: (step as any)?.completed,\n index,\n rawType: (step as any)?.rawType ?? (step as any)?.stepType,\n state: (step as any)?.state,\n type: getProgressStepType(step),\n});\n\nconst logSdkSwapEvent = (\n label: string,\n event: any,\n meta?: Record\n) => {\n console.log(`[NexusWidget SDK][swap] ${label}`, {\n event,\n eventType: getSdkEventType(event),\n ...meta,\n });\n};\n\nconst logSdkIntentEvent = (\n label: string,\n data: any,\n meta?: Record\n) => {\n console.log(`[NexusWidget SDK][intent] ${label}`, {\n hasAllow: typeof data?.allow === \"function\",\n hasDeny: typeof data?.deny === \"function\",\n hasRefresh: typeof data?.refresh === \"function\",\n intent: data?.intent,\n raw: data,\n ...meta,\n });\n};\n\nconst logSdkIntentInput = (\n operation: string,\n input: unknown,\n meta?: Record\n) => {\n console.log(`[NexusWidget SDK][intent input] ${operation}`, {\n input,\n ...meta,\n });\n};\n\nconst normalizeSdkIntentString = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n return String(value);\n};\n\nconst normalizeSdkIntentAmount = (\n value: unknown,\n fallback: string = \"0\"\n) => normalizeSdkIntentString(value) ?? fallback;\n\nconst getLogoFromMetadata = (metadata: any) =>\n metadata?.logo ??\n metadata?.logoURI ??\n metadata?.logoUri ??\n metadata?.logoUrl ??\n metadata?.icon ??\n \"\";\n\nconst normalizeSdkIntentChain = (chain: any) => {\n const id = Number(chain?.id ?? chain?.chainId);\n if (!Number.isFinite(id)) return undefined;\n const chainMeta = CHAIN_METADATA[id];\n return {\n id,\n logo: getLogoFromMetadata(chain) || chainMeta?.logo || \"\",\n name: chain?.name ?? chainMeta?.name ?? \"\",\n };\n};\n\nconst normalizeSdkIntentToken = (token: any, chainId?: number) => {\n const chainMeta = chainId ? CHAIN_METADATA[chainId] : undefined;\n const decimals = Number(token?.decimals ?? chainMeta?.nativeCurrency.decimals);\n return {\n contractAddress:\n token?.contractAddress ??\n token?.address ??\n token?.tokenAddress ??\n zeroAddress,\n decimals: Number.isFinite(decimals) ? decimals : 18,\n logo: getLogoFromMetadata(token) || undefined,\n symbol: token?.symbol ?? token?.tokenSymbol ?? \"\",\n };\n};\n\nconst normalizeSdkIntentGas = (\n gas: any,\n chainId?: number\n): SwapIntentDestination[\"gas\"] => ({\n amount: normalizeSdkIntentAmount(gas?.amount),\n value: normalizeSdkIntentString(gas?.value),\n token: normalizeSdkIntentToken(gas?.token ?? gas, chainId),\n});\n\nconst normalizeSdkIntentSource = (\n source: any\n): SwapIntentSource | undefined => {\n const chain = normalizeSdkIntentChain(source?.chain);\n if (!chain) return undefined;\n return {\n ...source,\n amount: normalizeSdkIntentAmount(source?.amount),\n chain,\n token: normalizeSdkIntentToken(source?.token, chain.id),\n value: normalizeSdkIntentString(source?.value),\n };\n};\n\nconst normalizeSdkIntentDestination = (\n destination: any\n): SwapIntentDestination | undefined => {\n const chain = normalizeSdkIntentChain(destination?.chain);\n if (!chain) return undefined;\n return {\n ...destination,\n amount: normalizeSdkIntentAmount(destination?.amount),\n chain,\n gas: normalizeSdkIntentGas(destination?.gas, chain.id),\n token: normalizeSdkIntentToken(destination?.token, chain.id),\n value: normalizeSdkIntentString(destination?.value),\n };\n};\n\nconst normalizeSwapIntentData = (intent: any): SwapIntentData | null => {\n const destination = normalizeSdkIntentDestination(intent?.destination);\n if (!destination) return null;\n\n return {\n ...intent,\n destination,\n sources: Array.isArray(intent?.sources)\n ? intent.sources\n .map(normalizeSdkIntentSource)\n .filter(\n (source: SwapIntentSource | undefined): source is SwapIntentSource =>\n Boolean(source)\n )\n : [],\n };\n};\n\nconst normalizeSwapAndExecuteRequirementIntent = (\n intent: any\n): SwapIntentData | null => {\n const requirement = intent?.executeRequirement ?? intent?.executionRequirement;\n if (!requirement) return null;\n const destination = normalizeSdkIntentDestination({\n amount: requirement?.token?.amount,\n chain: requirement?.chain,\n gas: requirement?.gas,\n token: requirement?.token,\n value: requirement?.token?.value,\n });\n if (!destination) return null;\n\n return {\n ...intent,\n bridgeProvider: normalizeBridgeProvider(intent?.bridgeProvider),\n destination,\n feesAndBuffer: intent?.feesAndBuffer,\n sources: [],\n };\n};\n\nconst normalizeRenderableSwapIntentData = (\n rawIntent: any,\n bridgeProvider?: BridgeProvider\n): SwapIntentData | null => {\n const direct = normalizeSwapIntentData(rawIntent);\n const normalizedIntent = direct\n ? null\n : normalizeSwapIntentData(rawIntent?.normalizedIntent);\n const nestedSwap =\n direct || normalizedIntent ? null : normalizeSwapIntentData(rawIntent?.swap);\n const requirement = direct || normalizedIntent || nestedSwap\n ? null\n : normalizeSwapAndExecuteRequirementIntent(rawIntent);\n const normalized = direct ?? normalizedIntent ?? nestedSwap ?? requirement;\n if (!normalized) return null;\n\n return bridgeProvider === undefined\n ? normalized\n : { ...normalized, bridgeProvider };\n};\n\nconst logSwapPlanSteps = (\n eventType: \"plan_preview\" | \"plan_confirmed\",\n stepList: Array,\n rawSteps: unknown\n) => {\n console.log(`[NexusWidget SDK][swap] ${eventType} step list`, {\n count: stepList.length,\n eventType,\n rawSteps,\n steps: stepList.map((step, index) => summarizeSdkProgressStep(step, index)),\n });\n};\n\nconst logSwapPlanProgress = (\n event: any,\n step: SwapStepType | BridgeStepType,\n eventName: string,\n completed: boolean\n) => {\n console.log(\"[NexusWidget SDK][swap] plan_progress\", {\n completed,\n eventName,\n eventType: getSdkEventType(event),\n normalizedStep: summarizeSdkProgressStep(step),\n rawEvent: event,\n rawStep: event?.step,\n state: event?.state,\n stepType: event?.stepType,\n });\n};\n\nconst getFailureMessageForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n mode: NexusWidgetMode,\n autoRefundAvailable = false\n) => {\n if (autoRefundAvailable) {\n return \"Swap Failed. Refund Initiated\";\n }\n\n const type = getProgressStepType(step);\n if (\n type.includes(\"CREATE_PERMIT_FOR_SOURCE_SWAP\") ||\n type.includes(\"CREATE_PERMIT_EOA_TO_EPHEMERAL\") ||\n type.includes(\"EOA_EXECUTE_CALL\") ||\n type.includes(\"SOURCE_SWAP\") ||\n type.includes(\"COLLECTION\")\n ) {\n return \"Collection Failed\";\n }\n if (type.includes(\"DESTINATION_SWAP\") || type.includes(\"FULFIL\")) {\n return \"Destination Swap Failed\";\n }\n if (\n type.includes(\"TRANSACTION\") ||\n type.includes(\"APPROVAL\") ||\n type.includes(\"DEPOSIT\")\n ) {\n return mode === \"send\"\n ? \"Send failed. Funds are in your wallet\"\n : mode === \"deposit\"\n ? \"Deposit failed. Funds are in your wallet\"\n : \"Swap Failed\";\n }\n if (\n type.includes(\"SWAP\") ||\n type.includes(\"BRIDGE\") ||\n type.includes(\"INTENT\") ||\n type.includes(\"DETERMINING\")\n ) {\n return \"Swap Failed\";\n }\n return mode === \"send\"\n ? \"Send failed. Funds are in your wallet\"\n : mode === \"deposit\"\n ? \"Deposit failed. Funds are in your wallet\"\n : \"Swap Failed\";\n};\n\nconst getBridgeTokenSymbolForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined\n) => {\n const rawStep = step as any;\n return (\n getNonEmptyString(\n rawStep?.bridgeToken?.symbol,\n rawStep?.data?.bridgeToken?.symbol,\n rawStep?.bridgeTokenSymbol,\n rawStep?.data?.bridgeTokenSymbol,\n rawStep?.swaps?.[0]?.input?.symbol,\n rawStep?.data?.swaps?.[0]?.input?.symbol,\n rawStep?.input?.symbol,\n rawStep?.data?.input?.symbol,\n rawStep?.asset?.symbol,\n rawStep?.data?.asset?.symbol\n ) ?? \"USDC\"\n );\n};\n\nconst getFailureDescriptionForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n autoRefundAvailable = false\n) => {\n if (autoRefundAvailable) return undefined;\n const type = getProgressStepType(step);\n if (!type.includes(\"DESTINATION_SWAP\")) return undefined;\n const bridgeTokenSymbol = getBridgeTokenSymbolForProgressStep(step);\n return `${bridgeTokenSymbol} has been bridged and you have those funds in your wallet.`;\n};\n\nconst getSourceRows = (entry: SwapHistoryEntry): HistorySourceRow[] => {\n const sources = entry.intentData?.sources ?? [];\n const displayDestinationSourceRow = getDisplayDestinationSourceRow(entry);\n if (sources.length > 0) {\n const sourceRows = sources.map((source, index) => {\n const fallback = entry.fromTokens.find(\n (token) =>\n token.chainId === source.chain.id &&\n (token.contractAddress?.toLowerCase() ===\n source.token.contractAddress?.toLowerCase() ||\n token.symbol === source.token.symbol)\n );\n\n return {\n key: `${source.chain.id}-${source.token.contractAddress}-${index}`,\n chainId: source.chain.id,\n contractAddress: source.token.contractAddress,\n tokenLogo: source.token.logo || fallback?.logo,\n chainLogo: source.chain.logo || fallback?.chainLogo,\n symbol: source.token.symbol,\n chainName: getShortChainName(source.chain.id, source.chain.name),\n amount: source.amount,\n value: source.value,\n };\n });\n\n return mergeHistorySourceRows(\n displayDestinationSourceRow\n ? [displayDestinationSourceRow, ...sourceRows]\n : sourceRows\n );\n }\n\n const fallbackRows = entry.fromTokens.map((token, index) => ({\n key: `${token.chainId}-${token.contractAddress}-${index}`,\n chainId: token.chainId,\n contractAddress: token.contractAddress,\n tokenLogo: token.logo,\n chainLogo: token.chainLogo,\n symbol: token.symbol,\n chainName: getShortChainName(token.chainId, token.chainName),\n amount: token.userAmount || \"0\",\n value: token.balanceInFiat,\n }));\n\n return mergeHistorySourceRows(\n displayDestinationSourceRow\n ? [displayDestinationSourceRow, ...fallbackRows]\n : fallbackRows\n );\n};\n\nfunction SourceRowsList({\n entry,\n maxHeight = 236,\n borderTopFirst = true,\n scrollAfterRows = 4,\n}: {\n entry: SwapHistoryEntry;\n maxHeight?: number;\n borderTopFirst?: boolean;\n scrollAfterRows?: number;\n}) {\n const rows = getSourceRows(entry);\n const shouldScroll = rows.length > scrollAfterRows;\n const scrollRef = useRef(null);\n\n return (\n
\n \n {rows.map((row, index) => (\n 0 ? \"1px solid #E8E8E7\" : \"none\",\n display: \"flex\",\n justifyContent: \"space-between\",\n minHeight: \"64px\",\n padding: \"10px 20px\",\n }}\n >\n \n \n
\n \n {row.symbol}\n \n \n on {row.chainName || \"Unknown chain\"}\n \n
\n
\n \n \n {formatTokenDisplay(row.amount)} {row.symbol}\n \n \n {formatUsdDisplay(row.value)}\n \n \n \n ))}\n \n {shouldScroll && (\n \n scrollRef.current?.scrollBy({ top: 72, behavior: \"smooth\" })\n }\n style={{\n alignItems: \"center\",\n background: \"#FFFFFE\",\n border: \"1px solid #E8E8E7\",\n borderRadius: \"999px\",\n bottom: \"6px\",\n boxShadow: \"0 2px 8px rgba(22,22,21,0.08)\",\n display: \"flex\",\n height: \"22px\",\n justifyContent: \"center\",\n left: \"50%\",\n padding: 0,\n position: \"absolute\",\n transform: \"translateX(-50%)\",\n width: \"22px\",\n }}\n type=\"button\"\n >\n \n \n )}\n \n );\n}\n\nfunction SwapReceiptPanel({\n entry,\n onDone,\n}: {\n entry: SwapHistoryEntry;\n onDone: () => void;\n}) {\n const [showSourceDetails, setShowSourceDetails] = useState(false);\n const destination = entry.intentData?.destination;\n const isFailed = entry.status === \"failed\";\n const isTimeout = entry.status === \"timeout\";\n const isDeposit = entry.mode === \"deposit\";\n const isSend = entry.mode === \"send\";\n const isRecipientTransfer = isSend || Boolean(entry.recipientAddress);\n const tokenSymbol = destination?.token.symbol || entry.toToken?.symbol || \"\";\n const chainName = getShortChainName(\n destination?.chain.id ?? entry.toToken?.chainId,\n destination?.chain.name || entry.toToken?.chainName || \"\"\n );\n const depositVenue =\n entry.opportunity?.title || entry.opportunity?.protocol || chainName;\n const amount = destination?.amount || \"\";\n const destinationLogo =\n destination?.token.logo ||\n entry.toToken?.logo ||\n entry.opportunity?.tokenLogo ||\n (isDeposit ? entry.opportunity?.logo : undefined);\n const requestedExactOutAmount =\n (isDeposit || isSend) && entry.requestedToAmount\n ? entry.requestedToAmount\n : undefined;\n const requestedExactOutValue =\n (isDeposit || isSend) && entry.requestedToValue\n ? entry.requestedToValue\n : undefined;\n const value = requestedExactOutValue || destination?.value;\n const displayAmount = requestedExactOutAmount || amount;\n const showIntentExplorer = hasValidIntentExplorer(entry);\n const intentLabel = entry.intentId\n ? `Intent #${entry.intentId}`\n : \"View Explorer\";\n const sourceRows = getSourceRows(entry);\n const sourceCount = sourceRows.length;\n const sourceTotalUsd = sourceRows.reduce(\n (sum, source) => sum.plus(parseDecimalLoose(source.value) ?? 0),\n new Decimal(0)\n );\n const defaultSwapFailureHeadline = entry.autoRefundAvailable\n ? \"Swap Failed. Refund Initiated\"\n : \"Swap Failed\";\n const entryFailureMessage =\n entry.status === \"timeout\" ? TIMEOUT_LABEL : entry.failureMessage;\n const storedFailureMessage =\n !entry.autoRefundAvailable && entryFailureMessage?.includes(\"Refund\")\n ? undefined\n : entryFailureMessage;\n const failureHeadline =\n storedFailureMessage ||\n (isDeposit\n ? \"Deposit failed. Funds are in your wallet\"\n : isRecipientTransfer\n ? \"Send failed. Funds are in your wallet\"\n : defaultSwapFailureHeadline);\n const failureDescription = isFailed ? entry.failureDescription : undefined;\n const timeoutHeadline = TIMEOUT_LABEL;\n const timeoutDescription = isTimeout\n ? entry.failureDescription ||\n \"This transaction is still pending. Check the intent explorer for the latest status.\"\n : undefined;\n const receiptLocation = isDeposit ? depositVenue : chainName;\n const receiptSummary = receiptLocation ? `on ${receiptLocation}` : \"\";\n\n return (\n \n \n \n \n \n {isFailed ? \"x\" : isTimeout ? \"!\" : \"✓\"}\n \n \n
\n {isTimeout\n ? timeoutHeadline\n : isFailed\n ? failureHeadline\n : isDeposit\n ? \"You deposited\"\n : isRecipientTransfer\n ? \"You sent\"\n : \"You received\"}\n
\n {(failureDescription || timeoutDescription) && (\n \n {failureDescription || timeoutDescription}\n \n )}\n \n {displayAmount ? formatTokenDisplay(displayAmount) : \"--\"}\n \n {tokenSymbol}\n \n \n
\n ≈ {formatUsdDisplay(value)}\n
\n {receiptSummary && (\n \n {receiptSummary}\n \n )}\n \n\n \n \n \n {isDeposit || isSend ? \"You Paid\" : \"You Swapped\"}\n \n \n \n {formatUsdDisplay(sourceTotalUsd)}\n \n setShowSourceDetails((current) => !current)}\n style={{\n alignItems: \"center\",\n background: \"transparent\",\n border: \"none\",\n color: \"var(--foreground-brand)\",\n cursor: \"pointer\",\n display: \"inline-flex\",\n fontFamily: uiFont,\n fontSize: \"12px\",\n gap: \"4px\",\n padding: 0,\n }}\n type=\"button\"\n >\n {showSourceDetails\n ? \"Hide Details\"\n : `${sourceCount} asset${sourceCount === 1 ? \"\" : \"s\"}`}\n \n \n \n \n \n
\n \n
\n \n {isRecipientTransfer && entry.recipientAddress && (\n \n \n Recipient\n \n \n \n )}\n {showIntentExplorer && (\n \n \n Intent Explorer\n \n \n {intentLabel} ↗\n \n \n )}\n {entry.finalExplorerUrl && (\n \n \n Final Transaction\n \n \n View Explorer ↗\n \n \n )}\n \n \n Total Fees\n \n \n {formatUsdDisplay(entry.feeUsd)}\n \n \n \n\n \n Done\n \n \n );\n}\n\nconst getRelativeTime = (time: number, now: number) => {\n const seconds = Math.max(1, Math.floor((now - time) / 1000));\n if (seconds < 60) return `${seconds} second${seconds === 1 ? \"\" : \"s\"} ago`;\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes} minute${minutes === 1 ? \"\" : \"s\"} ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours} hour${hours === 1 ? \"\" : \"s\"} ago`;\n const days = Math.floor(hours / 24);\n return `${days} day${days === 1 ? \"\" : \"s\"} ago`;\n};\n\nfunction HistoryStatusPill({ status }: { status: SwapHistoryStatus }) {\n const config =\n status === \"fulfilled\"\n ? { label: \"Fulfilled\", bg: \"#E8F6EF\", fg: \"#168A47\" }\n : status === \"pending\"\n ? { label: \"Pending\", bg: \"#FFF3DE\", fg: \"#B7791F\" }\n : status === \"timeout\"\n ? { label: TIMEOUT_LABEL, bg: \"#FFF3DE\", fg: \"#B7791F\" }\n : status === \"refund-initiated\"\n ? { label: \"Refund Initiated\", bg: \"#FFF3DE\", fg: \"#B7791F\" }\n : { label: \"Failed\", bg: \"#FFE6EA\", fg: \"#E92C2C\" };\n\n return (\n \n {config.label}\n \n );\n}\n\nfunction SwapHistoryPanel({\n entries,\n now,\n}: {\n entries: SwapHistoryEntry[];\n now: number;\n}) {\n if (entries.length === 0) {\n return (\n \n \n \n ↻\n \n \n \n No transactions yet\n \n \n Your transaction history will appear here once you make your first\n swap, deposit, or send.\n \n \n );\n }\n\n const sortedEntries = sortSwapHistoryEntries(entries);\n const shouldScroll = sortedEntries.length > 5;\n\n return (\n \n {sortedEntries.map((entry) => {\n const destination = entry.intentData?.destination;\n const destinationLogo =\n destination?.token.logo ||\n entry.toToken?.logo ||\n entry.opportunity?.tokenLogo ||\n \"\";\n const destinationChainLogo =\n destination?.chain.logo || entry.toToken?.chainLogo || \"\";\n const destinationChainName = getShortChainName(\n destination?.chain.id ?? entry.toToken?.chainId,\n destination?.chain.name || entry.toToken?.chainName || \"\"\n );\n const destinationSymbol =\n destination?.token.symbol || entry.toToken?.symbol || \"\";\n const destinationValue =\n (entry.mode === \"deposit\" || entry.mode === \"send\") &&\n entry.requestedToValue\n ? entry.requestedToValue\n : destination?.value;\n const destinationAmount =\n (entry.mode === \"deposit\" || entry.mode === \"send\") &&\n entry.requestedToAmount\n ? entry.requestedToAmount\n : destination?.amount || \"\";\n const canShowRefund =\n entry.status === \"failed\" && Boolean(entry.autoRefundAvailable);\n const status = canShowRefund ? \"refund-initiated\" : entry.status;\n const sourceRows = getSourceRows(entry);\n const historyExplorerUrl = getHistoryExplorerUrl(entry);\n\n return (\n \n \n \n \n
\n \n {destinationAmount\n ? formatTokenDisplay(destinationAmount)\n : \"--\"}\n \n {destinationSymbol}\n \n
\n \n ≈ {formatUsdDisplay(destinationValue)}\n \n \n \n \n \n \n {getRelativeTime(entry.createdAt ?? entry.startedAt, now)}\n \n \n \n\n {canShowRefund && (\n \n \n Refund Initiated\n \n \n )}\n\n \n \n {sourceRows.length > 0 && (\n \n )}\n \n →\n \n \n \n {historyExplorerUrl && (\n \n \n View\n \n \n \n \n \n \n )}\n \n \n );\n })}\n \n );\n}\n\n// ---------------------------------------------------------------------------\n// NexusWidget\n// ---------------------------------------------------------------------------\n\nexport function NexusWidget(props: NexusWidgetProps) {\n return (\n \n
\n Something went wrong\n
\n \n An unexpected error occurred. Please refresh the page or try\n resetting the widget.\n \n window.location.reload()}\n style={{\n backgroundColor: \"var(--foreground-brand)\",\n border: \"none\",\n borderRadius: \"8px\",\n color: \"#FFFFFE\",\n cursor: \"pointer\",\n fontSize: \"15px\",\n fontWeight: 500,\n padding: \"8px 16px\",\n transition: \"background-color 0.15s ease-out\",\n }}\n type=\"button\"\n >\n Reload Page\n \n \n }\n >\n \n \n );\n}\n\nfunction NexusWidgetInner({\n config: rawConfig,\n embed = true,\n className,\n connectedAddress,\n open: controlledOpen,\n onOpenChange,\n defaultOpen = false,\n onComplete,\n onConnectClick,\n onStart,\n onError,\n onReceiveAssetChange,\n onClose,\n onConnectWallet,\n}: NexusWidgetProps) {\n const {\n nexusSDK,\n bridgableBalance,\n swapBalance,\n swapBalanceLoading,\n getFiatValue,\n resolveTokenUsdRate,\n swapSupportedChainsAndTokens,\n supportedChainsAndTokens,\n fetchSwapBalance,\n handleInit,\n swapIntent: providerSwapIntent,\n network,\n loading: nexusLoading,\n } = useNexus();\n const appConfig = useMemo(\n () => ({\n nexusNetwork: network ?? \"mainnet\",\n }),\n [network]\n );\n const isSwapBalancePending =\n Boolean(nexusSDK) && (swapBalanceLoading || swapBalance === null);\n\n const normalizedWidgetConfig = useMemo(\n () => normalizeNexusWidgetConfig(rawConfig),\n [rawConfig]\n );\n const config = normalizedWidgetConfig.config;\n const activeMode = normalizedWidgetConfig.activeMode;\n const amountInputConfig = normalizedWidgetConfig.amountInput;\n const appearanceConfig = normalizedWidgetConfig.appearance;\n const configuredDepositOptions = normalizedWidgetConfig.depositOptions;\n const isConfiguredAmountFixed = normalizedWidgetConfig.isAmountFixed;\n const isConfiguredRecipientLocked =\n normalizedWidgetConfig.isRecipientLocked;\n const primaryColor = normalizeNexusWidgetPrimaryColor(\n appearanceConfig?.primaryColor\n );\n const primaryButtonBackground =\n primaryColor ?? NEXUS_WIDGET_DEFAULT_PRIMARY_COLOR;\n const primaryButtonForeground = getReadableTextColor(\n primaryButtonBackground\n );\n const theme = useMemo(\n () => ({\n ...nexusWidgetTheme,\n colors: {\n ...nexusWidgetTheme.colors,\n primary: \"var(--foreground-brand)\",\n primaryText: \"var(--foreground-brand)\",\n },\n }),\n [primaryColor]\n );\n const configuredDeposit = getConfiguredDeposit(config);\n const configuredDepositIdentity = getDepositConfigIdentity(configuredDeposit);\n const configuredDestinationTokenOptions = useMemo(() => {\n const destination = rawConfig.destination;\n const destinationChainId = getConfiguredDestinationChainId(destination);\n const destinationTokens =\n rawConfig.mode === \"deposit\"\n ? configuredDepositOptions.map(toSwapTokenOptionFromDepositConfig)\n : destination?.tokens && destinationChainId\n ? destination.tokens\n .map((token) =>\n toSwapTokenOptionFromConfiguredDestinationToken(\n token,\n destinationChainId\n )\n )\n .filter((token): token is SwapTokenOption => Boolean(token))\n : [];\n\n const tokensByKey = new Map();\n for (const token of destinationTokens) {\n tokensByKey.set(getTokenSelectionKey(token), token);\n }\n return Array.from(tokensByKey.values());\n }, [configuredDepositOptions, rawConfig.destination]);\n if (activeMode === \"deposit\" && !configuredDeposit) {\n throw new Error(\n \"NexusWidget deposit mode requires destination.chain, at least one destination token, depositAddress, and executeDeposit.\"\n );\n }\n const showCloseButton = !embed && Boolean(onClose);\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const isControlledOpen = controlledOpen !== undefined;\n const isModalOpen = isControlledOpen ? controlledOpen : internalOpen;\n\n // Preload receive tokens once SDK is available\n useEffect(() => {\n if (nexusSDK) {\n console.log(\n \"[preloadReceiveTokens] Calling preloadReceiveTokens from NexusWidget useEffect (nexusSDK available)\"\n );\n preloadReceiveTokens();\n }\n }, [nexusSDK]);\n\n const {\n address: accountAddress,\n connector,\n status: walletStatus,\n } = useAccount();\n const {\n connectors,\n connectAsync,\n isPending: isWalletConnectPending,\n } = useConnect();\n const { data: walletClient } = useWalletClient();\n const { data: connectorClient } = useConnectorClient();\n const publicClient = usePublicClient();\n const walletClientAddress = walletClient?.account?.address;\n const ownerAddress =\n connectedAddress &&\n isAddress(connectedAddress) &&\n connectedAddress.toLowerCase() !== zeroAddress\n ? connectedAddress\n : accountAddress &&\n isAddress(accountAddress) &&\n accountAddress.toLowerCase() !== zeroAddress\n ? accountAddress\n : walletClientAddress &&\n isAddress(walletClientAddress) &&\n walletClientAddress.toLowerCase() !== zeroAddress\n ? walletClientAddress\n : undefined;\n const getEffectiveWalletProvider = useCallback(\n async (activeConnector = connector) => {\n let connectorProvider: unknown;\n try {\n connectorProvider = await activeConnector?.getProvider();\n } catch {\n connectorProvider = undefined;\n }\n\n const connectorClientProvider = connectorClient\n ? {\n request: (args: unknown) => connectorClient.request(args as any),\n }\n : undefined;\n const walletClientProvider = walletClient\n ? {\n request: (args: unknown) => walletClient.request(args as any),\n }\n : undefined;\n const windowProvider =\n typeof window !== \"undefined\"\n ? (window as Window & { ethereum?: EthereumProvider }).ethereum\n : undefined;\n const effectiveProvider =\n connectorProvider &&\n typeof (connectorProvider as EthereumProvider).request === \"function\"\n ? (connectorProvider as EthereumProvider)\n : (connectorClientProvider ?? walletClientProvider ?? windowProvider);\n\n return effectiveProvider &&\n typeof effectiveProvider.request === \"function\"\n ? (effectiveProvider as EthereumProvider)\n : undefined;\n },\n [connector, connectorClient, walletClient]\n );\n const historyStorageKey = getSwapHistoryStorageKey(ownerAddress);\n\n useEffect(() => {\n if (walletStatus !== \"connected\" || !ownerAddress) {\n return;\n }\n\n let cancelled = false;\n void (async () => {\n const effectiveProvider = await getEffectiveWalletProvider();\n if (cancelled || !effectiveProvider) {\n return;\n }\n await handleInit(effectiveProvider, ownerAddress);\n })().catch((error) => {\n if (!cancelled) {\n console.error(\"Unable to initialize Nexus for wallet address:\", error);\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [getEffectiveWalletProvider, handleInit, ownerAddress, walletStatus]);\n\n // Global form state\n const [amount, setAmount] = useState(\"\");\n const [recipientAddress, setRecipientAddress] = useState(\"\");\n const [editingAssetIndex, setEditingAssetIndex] = useState(\n null\n );\n const [txError, setTxError] = useState(null);\n const [walletActionPending, setWalletActionPending] = useState(false);\n const defaultRecipientAddress = ownerAddress ?? \"\";\n const effectiveRecipientAddress =\n activeMode === \"swap\"\n ? recipientAddress || defaultRecipientAddress\n : recipientAddress;\n const hasSameOwnerSendRecipient =\n activeMode === \"send\" &&\n Boolean(\n ownerAddress &&\n recipientAddress &&\n isAddress(recipientAddress) &&\n recipientAddress.toLowerCase() === ownerAddress.toLowerCase()\n );\n const hasCustomSwapRecipient =\n activeMode === \"swap\" &&\n Boolean(\n recipientAddress &&\n (!defaultRecipientAddress ||\n recipientAddress.toLowerCase() !==\n defaultRecipientAddress.toLowerCase())\n );\n const transferRecipientAddress =\n activeMode === \"send\"\n ? recipientAddress\n : hasCustomSwapRecipient\n ? recipientAddress\n : undefined;\n\n // Swap-specific\n const [swapType, setSwapType] = useState(\"exactIn\");\n const [swapStep, setSwapStep] = useState(\"idle\");\n const [depositFundingStep, setDepositFundingStep] =\n useState(\"method\");\n const [depositOnrampSessionState, setDepositOnrampSessionState] = useState<\n string | null\n >(null);\n const drawerCloseTimerRef = useRef | null>(\n null\n );\n const terminalBalanceRefreshTimerRef = useRef | null>(null);\n const [closingDrawerStep, setClosingDrawerStep] = useState(\n null\n );\n const rootContentRef = useRef(null);\n const [rootContentHeight, setRootContentHeight] = useState(\n null\n );\n const rootContentHeightRef = useRef(null);\n const [hasMeasuredRootContent, setHasMeasuredRootContent] = useState(false);\n const [shouldAnimateRootHeight, setShouldAnimateRootHeight] = useState(false);\n const [isPreviewTransitioning, setIsPreviewTransitioning] = useState(false);\n const rootHeightTransitionTimerRef = useRef | null>(null);\n const [fromTokens, setFromTokens] = useState([]);\n const [sourceSelectionTouched, setSourceSelectionTouched] = useState(false);\n const [sourceSelectionRevision, setSourceSelectionRevision] = useState(0);\n const [\n sourceSelectionRequiredUsdDisplay,\n setSourceSelectionRequiredUsdDisplay,\n ] = useState(undefined);\n const [sourcePickerDraftTokens, setSourcePickerDraftTokens] = useState<\n SwapTokenOption[] | null\n >(null);\n const sourcePickerDraftTokensRef = useRef(null);\n const sourcePickerDraftDepositFilterRef = useRef(\"all\");\n const sourcePickerDraftTouchedRef = useRef(false);\n const sourcePickerDraftModeRef = useRef<\"all\" | \"selected\">(\"all\");\n const [exactOutQuoteSourceMode, setExactOutQuoteSourceMode] = useState<\n \"all\" | \"selected\"\n >(\"all\");\n const exactOutQuoteSourceModeRef = useRef<\"all\" | \"selected\">(\"all\");\n const [toToken, setToToken] = useState(\n undefined\n );\n const [fromTokensQuoteKey, setFromTokensQuoteKey] = useState(\"\");\n\n useEffect(() => {\n setDepositFundingStep(activeMode === \"deposit\" ? \"method\" : \"wallet\");\n }, [activeMode]);\n\n useEffect(() => {\n const key = getSourceTokensQuoteKey(\n activeMode === \"swap\" && swapType === \"exactIn\"\n ? getReadyExactInSourceTokens(fromTokens)\n : fromTokens\n );\n setFromTokensQuoteKey(key);\n }, [activeMode, swapType, fromTokens]);\n const toTokenQuoteKey = getTokenQuoteKey(toToken);\n const appliedTokenPrefillRef = useRef(null);\n\n useEffect(() => {\n if (!toToken?.chainId || !toToken.contractAddress) return;\n\n let active = true;\n const selectedTokenKey = getTokenSelectionKey(toToken);\n const applyLoadedReceiveToken = () => {\n if (!active) return;\n const loadedToken = getCachedReceiveTokenMatch(toToken);\n if (!loadedToken) return;\n\n setToToken((current) => {\n if (!current || getTokenSelectionKey(current) !== selectedTokenKey) {\n return current;\n }\n\n const chainMeta = current.chainId\n ? CHAIN_METADATA[current.chainId]\n : undefined;\n const next = {\n ...current,\n chainLogo:\n loadedToken.chainLogo || current.chainLogo || chainMeta?.logo,\n chainName: getShortChainName(\n current.chainId,\n loadedToken.chainName || current.chainName || chainMeta?.name\n ),\n decimals: loadedToken.decimals ?? current.decimals,\n logo: loadedToken.logo || current.logo,\n name: loadedToken.name || current.name,\n priceUSD: loadedToken.priceUSD ?? current.priceUSD,\n symbol: loadedToken.symbol || current.symbol,\n };\n\n if (\n current.decimals === next.decimals &&\n current.chainLogo === next.chainLogo &&\n current.chainName === next.chainName &&\n current.logo === next.logo &&\n current.name === next.name &&\n current.priceUSD === next.priceUSD &&\n current.symbol === next.symbol\n ) {\n return current;\n }\n\n return next;\n });\n };\n\n applyLoadedReceiveToken();\n const receiveTokensPromise = preloadReceiveTokens();\n receiveTokensPromise?.then(applyLoadedReceiveToken).catch((error) => {\n if (active) {\n console.warn(\"Unable to refresh receive token metadata\", error);\n }\n });\n\n return () => {\n active = false;\n };\n }, [\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n const setExactOutQuoteSourceModeValue = useCallback(\n (mode: \"all\" | \"selected\") => {\n exactOutQuoteSourceModeRef.current = mode;\n setExactOutQuoteSourceMode(mode);\n },\n []\n );\n\n useEffect(() => {\n if (!nexusSDK) return;\n void fetchSwapBalance();\n }, [fetchSwapBalance, nexusSDK]);\n\n useEffect(() => {\n setSourceSelectionTouched(false);\n setExactOutQuoteSourceModeValue(\"all\");\n }, [activeMode, setExactOutQuoteSourceModeValue]);\n\n useEffect(() => {\n if (activeMode !== \"swap\") return;\n\n setRecipientAddress((current) => {\n if (!defaultRecipientAddress) return current ? \"\" : current;\n return current.toLowerCase() === defaultRecipientAddress.toLowerCase()\n ? current\n : defaultRecipientAddress;\n });\n }, [activeMode, defaultRecipientAddress]);\n\n const {\n steps,\n seed,\n onStepsList,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n const [progressEvents, setProgressEvents] = useState(\n []\n );\n const progressEventsRef = useRef([]);\n const swapStepsListRef = useRef([]);\n const [failedProgressStep, setFailedProgressStep] = useState<\n SwapStepType | BridgeStepType | null\n >(null);\n const [explorerUrls, setExplorerUrls] = useState<{\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n }>({ sourceExplorerUrl: null, destinationExplorerUrl: null });\n const swapRunIdRef = useRef(0);\n\n const widgetSessionIdRef = useRef(\n typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`\n );\n const widgetAttemptIdRef = useRef(null);\n const widgetOpenedTsRef = useRef(Date.now());\n const previewViewedTsRef = useRef(null);\n const previewConfirmedTsRef = useRef(null);\n const attemptCountRef = useRef(0);\n const fundsMovedRef = useRef(false);\n const intentUrlRef = useRef(null);\n const hadSimulationSuccessRef = useRef(false);\n const hadPreviewViewedRef = useRef(false);\n const widgetOpenedFiredRef = useRef(false);\n const reachedTerminalRef = useRef(false);\n const lastIntentSourceTokensRef = useRef([]);\n const lastAutoIntentSourceTokensRef = useRef([]);\n const immediateQuoteAfterSourceEditRef = useRef(false);\n const amountEnteredLastValueRef = useRef(\"\");\n const lastInputMethodRef = useRef<\n | \"typed\"\n | \"percent_20\"\n | \"percent_25\"\n | \"percent_50\"\n | \"percent_75\"\n | \"percent_max\"\n >(\"typed\");\n const prevSourceTouchedRef = useRef(false);\n const previousAutoSourceCountRef = useRef(0);\n const analyticsRef = useRef<{\n track: (event: string, properties?: Record) => void;\n } | null>(null);\n const selectedOpportunityRef = useRef(\n undefined\n );\n\n const newAttemptId = useCallback(() => {\n return typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`;\n }, []);\n\n const rotateAttempt = useCallback(() => {\n widgetAttemptIdRef.current = newAttemptId();\n previewViewedTsRef.current = null;\n previewConfirmedTsRef.current = null;\n fundsMovedRef.current = false;\n intentUrlRef.current = null;\n hadSimulationSuccessRef.current = false;\n hadPreviewViewedRef.current = false;\n reachedTerminalRef.current = false;\n }, [newAttemptId]);\n const [intentToAmount, setIntentToAmount] = useState(\n undefined\n );\n const [intentFeeUsd, setIntentFeeUsd] = useState(\n undefined\n );\n const [intentLoading, setIntentLoading] = useState(false);\n const [quoteRefreshing, setQuoteRefreshing] = useState(false);\n const [receiveMaxCalculating, setReceiveMaxCalculating] = useState(false);\n const [maxCalculationPercent, setMaxCalculationPercent] = useState<\n number | null\n >(null);\n const intentDestinationUsdRateCacheRef = useRef<\n Record\n >({});\n const intentSymbolUsdRateCacheRef = useRef<\n Record\n >({});\n const predictiveQuoteCacheRef = useRef<\n Record\n >({});\n const predictiveQuoteRunRef = useRef(0);\n const latestIntentFetchRunIdRef = useRef(0);\n const intentFetchTimingRef = useRef(null);\n const intentUiRenderLoggedRunIdRef = useRef(null);\n const [intentFetchTiming, setIntentFetchTiming] =\n useState(null);\n const [predictiveQuote, setPredictiveQuote] =\n useState(null);\n const maxPercentRunRef = useRef(0);\n const [previewQuoteRefreshing, setPreviewQuoteRefreshing] = useState(false);\n const [quoteRefreshProgress, setQuoteRefreshProgress] = useState(0);\n const [quoteRefreshSecondsRemaining, setQuoteRefreshSecondsRemaining] =\n useState(0);\n const [intentData, setIntentData] = useState(null);\n const [swapQuoteIssue, setSwapQuoteIssue] = useState(\n null\n );\n const [receiveAmountIssue, setReceiveAmountIssue] =\n useState(null);\n const receiveAmountIssueRef = useRef(null);\n const receiveAmountIssueKeyRef = useRef(\"\");\n const [transferExplorerUrl, setTransferExplorerUrl] = useState(\n null\n );\n const swapStepRef = useRef(swapStep);\n const syncingIntentSourcesRef = useRef(false);\n const lastSwapIntentRefreshAtRef = useRef(0);\n const [destinationBalance, setDestinationBalance] = useState(\n null\n );\n const [swapHistory, setSwapHistory] = useState(() =>\n readSwapHistoryFromStorage(historyStorageKey)\n );\n const [currentSwapId, setCurrentSwapId] = useState(null);\n const [historyNow, setHistoryNow] = useState(() => Date.now());\n const currentSwapIdRef = useRef(null);\n const currentSwapStartedAtRef = useRef(0);\n const historyStorageKeyRef = useRef(historyStorageKey);\n const skipNextHistoryPersistRef = useRef(false);\n const explorerUrlsRef = useRef<{\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n }>({ sourceExplorerUrl: null, destinationExplorerUrl: null });\n const activeQuoteInputKeyRef = useRef(\"\");\n\n // Ref to store swap intent hook allow/deny callbacks\n const swapIntentRef = useRef<{\n intent?: SwapIntentData;\n allow: () => void;\n deny: () => void;\n refresh: () => Promise;\n runId?: number;\n quoteInputKey?: string;\n } | null>(null);\n\n const startIntentFetchTiming = useCallback(\n ({\n background,\n operation,\n quoteInputKey,\n runId,\n }: {\n background: boolean;\n operation: string;\n quoteInputKey: string;\n runId: number;\n }) => {\n const startTimeMs = Date.now();\n const timing: IntentFetchTiming = {\n background,\n operation,\n quoteInputKey,\n runId,\n startTimeMs,\n walletAddress: ownerAddress ?? null,\n };\n\n latestIntentFetchRunIdRef.current = runId;\n intentFetchTimingRef.current = timing;\n intentUiRenderLoggedRunIdRef.current = null;\n setIntentFetchTiming(timing);\n console.log(\"[NexusWidget] intent fetch start\", {\n runId,\n walletAddress: timing.walletAddress,\n operation,\n quoteInputKey,\n background,\n dateNow: startTimeMs,\n date: new Date(startTimeMs),\n });\n return timing;\n },\n [ownerAddress]\n );\n\n const finishIntentFetchTiming = useCallback(\n (runId: number, status: \"resolved\" | \"failed\" = \"resolved\") => {\n const timing = intentFetchTimingRef.current;\n if (!timing || timing.runId !== runId) return;\n if (timing.endTimeMs !== undefined) return;\n\n const endTimeMs = Date.now();\n if (latestIntentFetchRunIdRef.current !== runId) {\n return;\n }\n\n const fetchDurationMs = endTimeMs - timing.startTimeMs;\n const completedTiming: IntentFetchTiming = {\n ...timing,\n endTimeMs,\n fetchDurationMs,\n };\n intentFetchTimingRef.current = completedTiming;\n setIntentFetchTiming(completedTiming);\n console.log(\"[NexusWidget] intent fetch end\", {\n runId,\n walletAddress: timing.walletAddress,\n operation: timing.operation,\n quoteInputKey: timing.quoteInputKey,\n status,\n dateNow: endTimeMs,\n date: new Date(endTimeMs),\n });\n console.log(\"[NexusWidget] intent fetch total\", {\n runId,\n walletAddress: timing.walletAddress,\n operation: timing.operation,\n quoteInputKey: timing.quoteInputKey,\n totalTimeSeconds: fetchDurationMs / 1000,\n elapsedMs: fetchDurationMs,\n dateNow: endTimeMs,\n });\n },\n []\n );\n\n useEffect(() => {\n swapStepRef.current = swapStep;\n }, [swapStep]);\n\n useEffect(() => {\n return () => {\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n }\n if (terminalBalanceRefreshTimerRef.current) {\n clearTimeout(terminalBalanceRefreshTimerRef.current);\n }\n if (rootHeightTransitionTimerRef.current) {\n clearTimeout(rootHeightTransitionTimerRef.current);\n }\n };\n }, []);\n\n const isQuoteEditLocked = useCallback(\n () => swapStepRef.current === \"choose-swap-asset\",\n []\n );\n\n const getQuoteRequestDelay = useCallback(() => {\n if (immediateQuoteAfterSourceEditRef.current) {\n immediateQuoteAfterSourceEditRef.current = false;\n return 0;\n }\n return EXACT_OUT_INPUT_DEBOUNCE_MS;\n }, []);\n\n const startRootHeightTransition = useCallback(() => {\n setShouldAnimateRootHeight(true);\n if (rootHeightTransitionTimerRef.current) {\n clearTimeout(rootHeightTransitionTimerRef.current);\n }\n rootHeightTransitionTimerRef.current = setTimeout(() => {\n setShouldAnimateRootHeight(false);\n rootHeightTransitionTimerRef.current = null;\n }, ROOT_HEIGHT_TRANSITION_MS);\n }, []);\n\n const closeDrawerToIdle = useCallback(() => {\n const isDrawerStep =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n swapStep === \"enter-recipient\";\n\n if (!isDrawerStep) {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n return;\n }\n\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n }\n\n setClosingDrawerStep(swapStep);\n drawerCloseTimerRef.current = setTimeout(() => {\n if (\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\"\n ) {\n startRootHeightTransition();\n }\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n setClosingDrawerStep(null);\n drawerCloseTimerRef.current = null;\n }, DRAWER_CLOSE_MS);\n }, [startRootHeightTransition, swapStep]);\n\n const openDrawerStep = useCallback((nextStep: SwapStep) => {\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n drawerCloseTimerRef.current = null;\n }\n if (\n nextStep === \"choose-swap-asset\" ||\n nextStep === \"choose-receive-asset\"\n ) {\n startRootHeightTransition();\n }\n setClosingDrawerStep(null);\n swapStepRef.current = nextStep;\n setSwapStep(nextStep);\n }, [startRootHeightTransition]);\n\n const syncRootContentHeight = useCallback((animate = false) => {\n const element = rootContentRef.current;\n if (!element) return;\n\n const nextHeight = Math.ceil(\n Math.max(element.getBoundingClientRect().height, element.scrollHeight)\n );\n if (nextHeight <= 0) return;\n\n if (rootContentHeightRef.current === nextHeight) {\n setHasMeasuredRootContent(true);\n return;\n }\n\n rootContentHeightRef.current = nextHeight;\n setShouldAnimateRootHeight(animate);\n if (rootHeightTransitionTimerRef.current) {\n clearTimeout(rootHeightTransitionTimerRef.current);\n rootHeightTransitionTimerRef.current = null;\n }\n if (animate) {\n rootHeightTransitionTimerRef.current = setTimeout(() => {\n setShouldAnimateRootHeight(false);\n rootHeightTransitionTimerRef.current = null;\n }, ROOT_HEIGHT_TRANSITION_MS);\n }\n setRootContentHeight(nextHeight);\n setHasMeasuredRootContent(true);\n }, []);\n\n useLayoutEffect(() => {\n syncRootContentHeight(true);\n\n const element = rootContentRef.current;\n if (!element || typeof ResizeObserver === \"undefined\") return;\n\n let frame = 0;\n const observer = new ResizeObserver(() => {\n if (frame) {\n window.cancelAnimationFrame(frame);\n }\n frame = window.requestAnimationFrame(() => {\n syncRootContentHeight(false);\n });\n });\n\n observer.observe(element);\n\n return () => {\n if (frame) {\n window.cancelAnimationFrame(frame);\n }\n observer.disconnect();\n };\n }, [activeMode, swapStep, syncRootContentHeight]);\n\n useEffect(() => {\n currentSwapIdRef.current = currentSwapId;\n }, [currentSwapId]);\n\n useEffect(() => {\n if (historyStorageKeyRef.current === historyStorageKey) return;\n historyStorageKeyRef.current = historyStorageKey;\n skipNextHistoryPersistRef.current = true;\n setSwapHistory(readSwapHistoryFromStorage(historyStorageKey));\n }, [historyStorageKey]);\n\n useEffect(() => {\n if (skipNextHistoryPersistRef.current) {\n skipNextHistoryPersistRef.current = false;\n return;\n }\n\n writeSwapHistoryToStorage(historyStorageKey, swapHistory);\n }, [historyStorageKey, swapHistory]);\n\n useEffect(() => {\n if (swapStep !== \"history\") return;\n const timer = window.setInterval(() => setHistoryNow(Date.now()), 30000);\n return () => window.clearInterval(timer);\n }, [swapStep]);\n\n const normalizeAddress = (value?: string | null) =>\n (value ?? \"\").toLowerCase();\n\n const getTokenLookupAddress = (value?: string | null) => {\n if (!value) return \"\";\n return isNativeTokenAddress(value) ? zeroAddress : value.toLowerCase();\n };\n\n const findBalanceTokenLogo = (\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId) return \"\";\n const lookupAddress = getTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n let symbolMatchLogo = \"\";\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== chainId) continue;\n const breakdownAddress = getTokenLookupAddress(\n breakdown.contractAddress\n );\n const addressMatches =\n lookupAddress && breakdownAddress === lookupAddress;\n const symbolMatches =\n lookupSymbol &&\n [breakdown.symbol, asset.symbol]\n .filter(Boolean)\n .some((candidate) => candidate.toUpperCase() === lookupSymbol);\n const logo = getLogoFromMetadata(breakdown) || getLogoFromMetadata(asset);\n\n if (addressMatches && logo) return logo;\n if (!symbolMatchLogo && symbolMatches && logo) {\n symbolMatchLogo = logo;\n }\n }\n }\n\n return symbolMatchLogo;\n };\n\n const findSupportedTokenLogo = (\n chains: any[] | null | undefined,\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId) return \"\";\n const chain = chains?.find(\n (item: any) => Number(item?.id ?? item?.chainId) === chainId\n );\n const tokens = chain?.tokens ?? chain?.assets ?? [];\n const lookupAddress = getTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n\n const addressMatch =\n lookupAddress &&\n tokens.find((token: any) => {\n const tokenAddress = getTokenLookupAddress(\n token?.contractAddress ?? token?.address ?? token?.tokenAddress\n );\n return tokenAddress === lookupAddress;\n });\n const symbolMatch =\n lookupSymbol &&\n tokens.find((token: any) => {\n const tokenSymbol = (token?.symbol ?? token?.tokenSymbol ?? \"\")\n .toString()\n .toUpperCase();\n return tokenSymbol === lookupSymbol;\n });\n\n return getLogoFromMetadata(addressMatch) || getLogoFromMetadata(symbolMatch);\n };\n\n const resolveTokenLogo = (\n token:\n | {\n address?: string;\n contractAddress?: string;\n logo?: string;\n symbol?: string;\n tokenAddress?: string;\n tokenSymbol?: string;\n }\n | undefined,\n chainId?: number\n ) => {\n const explicitLogo = getLogoFromMetadata(token);\n if (explicitLogo) return explicitLogo;\n\n const contractAddress =\n token?.contractAddress ?? token?.address ?? token?.tokenAddress;\n const symbol = token?.symbol ?? token?.tokenSymbol;\n const citreaToken = findCitreaReceiveToken({\n address: contractAddress,\n chainId,\n symbol,\n });\n const tokenMeta = symbol\n ? TOKEN_METADATA[symbol as keyof typeof TOKEN_METADATA]\n : undefined;\n\n return (\n findBalanceTokenLogo(chainId, contractAddress, symbol) ||\n findSupportedTokenLogo(\n swapSupportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol\n ) ||\n findSupportedTokenLogo(\n supportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol\n ) ||\n citreaToken?.logo ||\n tokenMeta?.logo ||\n (contractAddress && isNativeTokenAddress(contractAddress)\n ? CHAIN_METADATA[chainId ?? 0]?.logo\n : \"\") ||\n \"\"\n );\n };\n\n const enrichIntentToken = <\n T extends {\n address?: string;\n contractAddress?: string;\n logo?: string;\n symbol?: string;\n tokenAddress?: string;\n tokenSymbol?: string;\n },\n >(\n token: T,\n chainId?: number\n ): T => {\n const logo = resolveTokenLogo(token, chainId);\n return logo && logo !== token.logo ? { ...token, logo } : token;\n };\n\n const enrichTokenOptionLogo = (\n token?: SwapTokenOption\n ): SwapTokenOption | undefined => {\n if (!token) return undefined;\n\n const chainMeta = token.chainId ? CHAIN_METADATA[token.chainId] : undefined;\n const logo = resolveTokenLogo(token, token.chainId);\n const next = {\n ...token,\n chainLogo: token.chainLogo || chainMeta?.logo,\n logo: token.logo || logo,\n };\n\n if (next.chainLogo === token.chainLogo && next.logo === token.logo) {\n return token;\n }\n\n return next;\n };\n\n const enrichSwapIntentTokenMetadata = (\n intent: SwapIntentData\n ): SwapIntentData => ({\n ...intent,\n destination: {\n ...intent.destination,\n token: enrichIntentToken(\n intent.destination.token,\n intent.destination.chain.id\n ),\n },\n sources: (intent.sources ?? []).map((source) => ({\n ...source,\n token: enrichIntentToken(source.token, source.chain.id),\n })),\n });\n\n const buildIntentSourceToken = (\n source: SwapIntentData[\"sources\"][number]\n ): SwapTokenOption => {\n let matchedAsset: any;\n let matchedBreakdown: any;\n const sourceAddress = normalizeAddress(source.token.contractAddress);\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const addressMatches =\n normalizeAddress(breakdown.contractAddress) === sourceAddress;\n const symbolMatches =\n breakdown.symbol === source.token.symbol ||\n asset.symbol === source.token.symbol;\n if (\n breakdown.chain?.id === source.chain.id &&\n (addressMatches || symbolMatches)\n ) {\n matchedAsset = asset;\n matchedBreakdown = breakdown;\n break;\n }\n }\n if (matchedBreakdown) break;\n }\n\n const chainMeta = CHAIN_METADATA[source.chain.id];\n const sourceValue = Number((source as any).value ?? 0);\n const isNativeSource = isNativeTokenAddress(source.token.contractAddress);\n const nativeCurrency = chainMeta?.nativeCurrency;\n const sourceSymbol =\n isNativeSource && (!source.token.symbol || !matchedAsset?.logo)\n ? nativeCurrency?.symbol || source.token.symbol\n : source.token.symbol || nativeCurrency?.symbol || \"\";\n const sourceDecimals =\n isNativeSource && nativeCurrency?.decimals !== undefined\n ? nativeCurrency.decimals\n : source.token.decimals;\n const sourceLogo = resolveTokenLogo(source.token, source.chain.id);\n\n return {\n contractAddress: source.token.contractAddress,\n symbol: sourceSymbol,\n name: sourceSymbol,\n logo: sourceLogo ?? \"\",\n decimals: sourceDecimals,\n balance: matchedBreakdown?.balance\n ? `${matchedBreakdown.balance} ${sourceSymbol}`\n : `${source.amount} ${sourceSymbol}`,\n balanceInFiat:\n matchedBreakdown?.balanceInFiat != null\n ? `$${Number(matchedBreakdown.balanceInFiat).toFixed(2)}`\n : Number.isFinite(sourceValue)\n ? `$${sourceValue.toFixed(2)}`\n : \"$0.00\",\n chainId: source.chain.id,\n chainName: getShortChainName(\n source.chain.id,\n chainMeta?.name ?? source.chain.name\n ),\n chainLogo: chainMeta?.logo ?? source.chain.logo,\n userAmount: source.amount,\n userAmountUsd: Number.isFinite(sourceValue) ? source.value : undefined,\n userAmountMode: \"token\",\n };\n };\n\n const clearPendingSwapIntent = (\n clearQuote = true,\n options: { keepQuoteRefreshing?: boolean } = {}\n ) => {\n swapRunIdRef.current += 1;\n swapIntentRef.current?.deny();\n swapIntentRef.current = null;\n setIntentLoading(false);\n setTxError(null);\n if (!options.keepQuoteRefreshing) {\n setQuoteRefreshing(false);\n }\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setSwapQuoteIssue(null);\n resetProgressEvents();\n if (swapStepsListRef.current.length > 0 || steps.length > 0) {\n swapStepsListRef.current = [];\n resetSteps();\n } else {\n swapStepsListRef.current = [];\n }\n if (clearQuote) {\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n if (!options.keepQuoteRefreshing) {\n setPredictiveQuote(null);\n }\n }\n };\n\n const clearSelectedSources = () => {\n setFromTokens((current) => (current.length === 0 ? current : []));\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setExactOutQuoteSourceModeValue(\"all\");\n };\n\n const resetExactOutSourcesToAuto = () => {\n setFromTokens((current) => (current.length === 0 ? current : []));\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setExactOutQuoteSourceModeValue(\"all\");\n setSourceSelectionRevision((current) => current + 1);\n };\n\n const getSourceAmountInput = (tokens: SwapTokenOption[]) => {\n const total = tokens.reduce(\n (sum, token) => sum + Number(token.userAmount || 0),\n 0\n );\n return total > 0 ? String(total) : \"\";\n };\n\n const parseFiatNumber = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = normalizeDecimalInputText(value);\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n };\n\n const minimumSourceUsd = new Decimal(1);\n const hasMinimumSourceUsdValue = (value: unknown) =>\n (parseFiatNumber(value) ?? new Decimal(0)).gte(minimumSourceUsd);\n const hasMinimumSourceUsdBalance = (\n token: Pick\n ) => hasMinimumSourceUsdValue(token.balanceInFiat);\n const filterMinimumSourceUsdTokens = (tokens: SwapTokenOption[]) =>\n tokens.filter(hasMinimumSourceUsdBalance);\n\n const getTokenUsdRateCacheKeyFromParts = (\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId || !symbol) return \"\";\n return [\n chainId,\n (contractAddress || zeroAddress).toLowerCase(),\n symbol.toUpperCase(),\n ].join(\":\");\n };\n\n const getTokenUsdRateCacheKey = (\n token?: Pick\n ) =>\n getTokenUsdRateCacheKeyFromParts(\n token?.chainId,\n token?.contractAddress,\n token?.symbol\n );\n\n const getSymbolUsdRateCacheKey = (symbol?: string) =>\n symbol ? symbol.trim().toUpperCase() : \"\";\n\n const getCachedIntentUsdRate = (\n token?: Pick\n ) => {\n const tokenKey = getTokenUsdRateCacheKey(token);\n const cached = tokenKey\n ? intentDestinationUsdRateCacheRef.current[tokenKey]\n : undefined;\n const rate = parseFiatNumber(cached?.rate);\n return rate && rate.gt(0) ? rate : undefined;\n };\n\n const cacheDestinationUsdRateFromIntent = (\n intent?: SwapIntentData | null\n ) => {\n const destination = intent?.destination;\n const amount = parseFiatNumber(destination?.amount);\n const value = parseFiatNumber(destination?.value);\n const chainId = destination?.chain?.id;\n const symbol = destination?.token?.symbol;\n\n if (\n !amount ||\n !value ||\n amount.lte(0) ||\n value.lte(0) ||\n !chainId ||\n !symbol\n ) {\n return;\n }\n\n const rate = value.div(amount);\n if (!rate.isFinite() || rate.lte(0)) return;\n\n const cached: CachedIntentUsdRate = {\n amount: amount.toFixed(),\n rate: rate.toDecimalPlaces(18).toFixed(),\n updatedAt: Date.now(),\n value: value.toFixed(),\n };\n const tokenKey = getTokenUsdRateCacheKeyFromParts(\n chainId,\n destination?.token?.contractAddress,\n symbol\n );\n if (tokenKey) {\n intentDestinationUsdRateCacheRef.current[tokenKey] = cached;\n }\n\n const symbolKey = getSymbolUsdRateCacheKey(symbol);\n if (symbolKey) {\n intentSymbolUsdRateCacheRef.current[symbolKey] = cached;\n }\n };\n\n const getSwapBalanceTotalUsd = () =>\n (swapBalance ?? []).reduce((sum, asset) => {\n const breakdown = asset.breakdown ?? [];\n if (breakdown.length > 0) {\n return sum.plus(\n breakdown.reduce((breakdownSum, item) => {\n const value = parseFiatNumber(item.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd)\n ? breakdownSum.plus(value)\n : breakdownSum;\n }, new Decimal(0))\n );\n }\n\n const value = parseFiatNumber(asset.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd) ? sum.plus(value) : sum;\n }, new Decimal(0));\n\n const getTokenUsdRate = (token: SwapTokenOption) => {\n const tokenBalance = parseFiatNumber(token.balance) ?? new Decimal(0);\n const fiatBalance = parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n if (tokenBalance.gt(0) && fiatBalance.gt(0)) {\n return fiatBalance.div(tokenBalance);\n }\n\n const priceUsd = parseFiatNumber(token.priceUSD);\n if (priceUsd && priceUsd.gt(0)) {\n return priceUsd;\n }\n\n const fallbackRate = getFiatValue(1, token.symbol);\n if (Number.isFinite(fallbackRate) && fallbackRate > 0) {\n return new Decimal(fallbackRate);\n }\n\n return getCachedIntentUsdRate(token) ?? new Decimal(0);\n };\n const getUsdRateForSymbol = (symbol?: string) => {\n if (!symbol) return new Decimal(0);\n const fiat = getFiatValue(1, symbol);\n if (Number.isFinite(fiat) && fiat > 0) {\n return new Decimal(fiat);\n }\n\n const cached =\n intentSymbolUsdRateCacheRef.current[getSymbolUsdRateCacheKey(symbol)];\n const rate = parseFiatNumber(cached?.rate);\n return rate && rate.gt(0) ? rate : new Decimal(0);\n };\n\n const getFeeTokenLookupAddress = (value?: string | null) => {\n const lower = (value ?? \"\").toLowerCase();\n if (!lower || lower === \"0x\" || isNativeTokenAddress(lower)) {\n return zeroAddress;\n }\n return lower;\n };\n\n const getTokenUsdRateFromBalances = (\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId) return undefined;\n const lookupAddress = getFeeTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== chainId) continue;\n const breakdownAddress = getFeeTokenLookupAddress(\n breakdown.contractAddress\n );\n const addressMatches =\n lookupAddress && breakdownAddress === lookupAddress;\n const symbolMatches =\n lookupSymbol &&\n [breakdown.symbol, asset.symbol]\n .filter(Boolean)\n .some((candidate) => candidate.toUpperCase() === lookupSymbol);\n if (!addressMatches && !symbolMatches) continue;\n\n const balance = parseFiatNumber(breakdown.balance);\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n if (balance && fiatBalance && balance.gt(0) && fiatBalance.gt(0)) {\n return fiatBalance.div(balance);\n }\n }\n }\n\n return undefined;\n };\n\n const getTokenUsdRateFromSupportedChains = (\n chains: any[] | null | undefined,\n chainId?: number,\n contractAddress?: string,\n symbol?: string\n ) => {\n if (!chainId) return undefined;\n const chain = chains?.find(\n (item: any) => Number(item?.id ?? item?.chainId) === chainId\n );\n const tokens = chain?.tokens ?? chain?.assets ?? [];\n const lookupAddress = getFeeTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n const matchedToken = tokens.find((token: any) => {\n const tokenAddress = getFeeTokenLookupAddress(\n token?.contractAddress ?? token?.address ?? token?.tokenAddress\n );\n const addressMatches = lookupAddress && tokenAddress === lookupAddress;\n const tokenSymbol = (token?.symbol ?? token?.tokenSymbol ?? \"\")\n .toString()\n .toUpperCase();\n const symbolMatches = lookupSymbol && tokenSymbol === lookupSymbol;\n return addressMatches || symbolMatches;\n });\n const priceUsd = parseFiatNumber(\n matchedToken?.priceUSD ??\n matchedToken?.priceUsd ??\n matchedToken?.usdPrice ??\n matchedToken?.price\n );\n return priceUsd && priceUsd.gt(0) ? priceUsd : undefined;\n };\n\n const getIntentDestinationGasUsdValue = (\n intent?: SwapIntentData | null\n ) => {\n const gas = intent?.destination?.gas;\n const explicitValue = parseFiatNumber(gas?.value);\n if (explicitValue && explicitValue.gt(0)) return explicitValue;\n\n const amount = parseFiatNumber(gas?.amount);\n if (!amount || amount.lte(0)) return undefined;\n\n const chainId = intent?.destination?.chain?.id;\n const contractAddress = gas?.token?.contractAddress;\n const symbol = gas?.token?.symbol;\n const rate =\n getTokenUsdRateFromBalances(chainId, contractAddress, symbol) ??\n getTokenUsdRateFromSupportedChains(\n swapSupportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol\n ) ??\n getTokenUsdRateFromSupportedChains(\n supportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol\n ) ??\n getUsdRateForSymbol(symbol);\n\n return rate.gt(0) ? amount.mul(rate) : undefined;\n };\n\n const getCachedDestinationUsdRate = (token?: SwapTokenOption) => {\n return getCachedIntentUsdRate(token);\n };\n\n const resolveUsdRateForSymbol = async (symbol?: string) => {\n if (!symbol) return new Decimal(0);\n\n const localRate = getUsdRateForSymbol(symbol);\n if (localRate.gt(0)) return localRate;\n\n try {\n const resolvedRate = await resolveTokenUsdRate(symbol);\n return resolvedRate && resolvedRate > 0\n ? new Decimal(resolvedRate)\n : new Decimal(0);\n } catch {\n return new Decimal(0);\n }\n };\n\n const getTokenUsdValue = (\n token: SwapTokenOption,\n fallbackAmount?: string\n ) => {\n const amountNumber =\n parseFiatNumber(token.userAmount || fallbackAmount) ?? new Decimal(0);\n if (amountNumber.lte(0)) return new Decimal(0);\n const quotedUsd = parseFiatNumber(token.userAmountUsd);\n if (quotedUsd && quotedUsd.gte(0)) return quotedUsd;\n if (token.userAmountMode === \"usd\") return amountNumber;\n\n const rate = getTokenUsdRate(token);\n return rate.gt(0) ? amountNumber.mul(rate) : new Decimal(0);\n };\n\n const getTokenBalanceAmount = (token: SwapTokenOption) =>\n parseFiatNumber(token.balance) ?? new Decimal(0);\n\n const getTokenBalanceUsd = (token: SwapTokenOption) =>\n parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n\n const getSdkExactOutSourcePriority = (token: SwapTokenOption) => {\n const isSameChain =\n Boolean(token.chainId && toToken?.chainId) &&\n token.chainId === toToken?.chainId;\n const isSameToken =\n Boolean(token.contractAddress && toToken?.contractAddress) &&\n ((isNativeTokenAddress(token.contractAddress) &&\n isNativeTokenAddress(toToken?.contractAddress)) ||\n token.contractAddress.toLowerCase() ===\n toToken?.contractAddress.toLowerCase());\n const isStable = isSdkExactOutStableSymbol(token.symbol);\n const isGas = isNativeTokenAddress(token.contractAddress);\n const isEthereum = token.chainId === ETHEREUM_MAINNET_CHAIN_ID;\n\n if (isSameChain) {\n if (isSameToken) return 1;\n if (isStable) return 2;\n if (isGas) return 3;\n return 4;\n }\n\n if (isEthereum) {\n if (isSameToken) return 8;\n if (isStable) return 9;\n if (isGas) return 10;\n return 11;\n }\n\n if (isSameToken) return 5;\n if (isStable) return 6;\n return 7;\n };\n\n const sortExactOutSourcesBySdkPriority = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const priorityDiff =\n getSdkExactOutSourcePriority(a) - getSdkExactOutSourcePriority(b);\n if (priorityDiff !== 0) return priorityDiff;\n\n const usdDiff = getTokenBalanceUsd(b).cmp(getTokenBalanceUsd(a));\n if (usdDiff !== 0) return usdDiff;\n\n return `${a.symbol} ${a.chainName ?? \"\"}`.localeCompare(\n `${b.symbol} ${b.chainName ?? \"\"}`\n );\n });\n\n const getTokenAmountForUsd = (token: SwapTokenOption, usdAmount: Decimal) => {\n const rate = getTokenUsdRate(token);\n if (rate.lte(0) || usdAmount.lte(0)) return new Decimal(0);\n return usdAmount.div(rate);\n };\n\n const getUsdForTokenAmount = (\n token: SwapTokenOption,\n tokenAmount: Decimal\n ) => {\n const rate = getTokenUsdRate(token);\n if (rate.lte(0) || tokenAmount.lte(0)) return new Decimal(0);\n return tokenAmount.mul(rate);\n };\n\n const getExactOutDestinationBalanceCoverage = ({\n requestedAmount,\n requestedUsd,\n producedAmount,\n producedUsd,\n token = toToken,\n }: {\n requestedAmount?: Decimal;\n requestedUsd?: Decimal;\n producedAmount?: Decimal;\n producedUsd?: Decimal;\n token?: SwapTokenOption;\n }) => {\n if (\n (activeMode !== \"deposit\" && activeMode !== \"send\") ||\n !token ||\n !requestedAmount ||\n requestedAmount.lte(0)\n ) {\n return null;\n }\n\n const balanceAmount =\n parseFiatNumber(destinationBalance) ??\n parseFiatNumber(token.balance) ??\n new Decimal(0);\n if (balanceAmount.lte(0)) return null;\n\n const externalAmount =\n producedAmount && producedAmount.gt(0) ? producedAmount : new Decimal(0);\n const uncoveredAmount = Decimal.max(\n requestedAmount.minus(externalAmount),\n new Decimal(0)\n );\n const coveredAmount = Decimal.min(balanceAmount, uncoveredAmount);\n if (coveredAmount.lte(0)) return null;\n\n const requestedRate =\n requestedUsd && requestedUsd.gt(0)\n ? requestedUsd.div(requestedAmount)\n : undefined;\n const producedRate =\n producedUsd && producedUsd.gt(0) && producedAmount && producedAmount.gt(0)\n ? producedUsd.div(producedAmount)\n : undefined;\n const fallbackRate = getTokenUsdRate(token);\n const usdRate =\n requestedRate && requestedRate.gt(0)\n ? requestedRate\n : producedRate && producedRate.gt(0)\n ? producedRate\n : fallbackRate.gt(0)\n ? fallbackRate\n : undefined;\n\n return {\n amount: coveredAmount,\n usd: usdRate ? coveredAmount.mul(usdRate) : undefined,\n };\n };\n\n const buildDestinationBalanceDisplayToken = (\n coverage: ReturnType,\n token?: SwapTokenOption\n ): SwapTokenOption | null => {\n if (!coverage || !token || coverage.amount.lte(0)) return null;\n\n const amount = coverage.amount\n .toDecimalPlaces(Math.max(0, token.decimals ?? 18), Decimal.ROUND_DOWN)\n .toFixed();\n const usd = coverage.usd?.toDecimalPlaces(6, Decimal.ROUND_DOWN).toFixed();\n const balanceUsd = coverage.usd\n ? `$${coverage.usd.toDecimalPlaces(2, Decimal.ROUND_DOWN).toFixed()}`\n : token.balanceInFiat || \"$0.00\";\n\n return {\n ...token,\n balance: `${amount} ${token.symbol}`,\n balanceInFiat: balanceUsd,\n userAmount: amount,\n userAmountMode: \"token\",\n userAmountUsd: usd,\n };\n };\n\n const cacheSymbolUsdRate = (symbol: string | undefined, rate: Decimal) => {\n const symbolKey = getSymbolUsdRateCacheKey(symbol);\n if (!symbolKey || rate.lte(0)) return;\n\n intentSymbolUsdRateCacheRef.current[symbolKey] = {\n amount: \"1\",\n rate: rate.toDecimalPlaces(18).toFixed(),\n updatedAt: Date.now(),\n value: rate.toFixed(),\n };\n };\n\n const getPredictiveDestinationKey = (token?: SwapTokenOption) => {\n const tokenKey = getTokenUsdRateCacheKey(token);\n return tokenKey ? `destination:${tokenKey}` : \"\";\n };\n\n const getPredictiveSourceKey = (token: SwapTokenOption) =>\n [\n token.chainId ?? \"unknown\",\n (token.contractAddress || zeroAddress).toLowerCase(),\n token.symbol.toUpperCase(),\n ].join(\":\");\n\n const getPredictiveQuoteCacheKey = (\n mode = activeMode,\n type = swapType,\n destination = toToken,\n sources = fromTokens\n ) => {\n const destinationKey = getPredictiveDestinationKey(destination);\n if (!destinationKey) return \"\";\n if (mode !== \"swap\" || type !== \"exactIn\") {\n return `exactOut:${destinationKey}`;\n }\n\n const sourceKey = getExpandedSourceTokens(sources)\n .map(getPredictiveSourceKey)\n .sort()\n .join(\"+\");\n return sourceKey ? `exactIn:${sourceKey}->${destinationKey}` : \"\";\n };\n\n const getPredictiveDisplayAmount = (\n amount: Decimal,\n token?: Pick\n ) => {\n const decimals = Math.min(\n PREDICTIVE_QUOTE_DISPLAY_DECIMALS,\n Math.max(0, token?.decimals ?? 18)\n );\n return amount.toDecimalPlaces(decimals, Decimal.ROUND_DOWN).toFixed();\n };\n\n const resolveUsdRateForToken = async (token?: SwapTokenOption) => {\n if (!token?.symbol) return new Decimal(0);\n\n const localRate = getTokenUsdRate(token);\n if (localRate.gt(0)) return localRate;\n\n const resolvedRate = await resolveUsdRateForSymbol(token.symbol);\n if (resolvedRate.gt(0)) {\n cacheSymbolUsdRate(token.symbol, resolvedRate);\n }\n return resolvedRate;\n };\n\n const getDestinationReceiveLimitUsd = (token?: SwapTokenOption) => {\n if (!token?.chainId) return undefined;\n const limit = DESTINATION_RECEIVE_LIMIT_USD_BY_CHAIN_ID[token.chainId];\n return limit ? new Decimal(limit) : undefined;\n };\n\n const getImmediateDestinationReceiveUsdRate = (token?: SwapTokenOption) => {\n const priceUsd = parseFiatNumber(token?.priceUSD);\n if (priceUsd && priceUsd.gt(0)) return priceUsd;\n\n const cachedRate = getCachedDestinationUsdRate(token);\n if (cachedRate && cachedRate.gt(0)) return cachedRate;\n\n if (!token) return undefined;\n const localRate = getTokenUsdRate(token);\n return localRate.gt(0) ? localRate : undefined;\n };\n\n const getExactInSourceUsdForReceiveLimit = (\n sourceTokens: SwapTokenOption[],\n inputAmount: string\n ) => {\n if (sourceTokens.length === 0) return undefined;\n let hasPositiveSourceAmount = false;\n let totalUsd = new Decimal(0);\n\n for (const token of sourceTokens) {\n const fallbackAmount =\n sourceTokens.length === 1 ? inputAmount : undefined;\n const sourceAmount = parseFiatNumber(token.userAmount || fallbackAmount);\n if (!sourceAmount || sourceAmount.lte(0)) continue;\n\n hasPositiveSourceAmount = true;\n const sourceUsd = getTokenUsdValue(token, fallbackAmount);\n if (sourceUsd.lte(0)) return undefined;\n totalUsd = totalUsd.plus(sourceUsd);\n }\n\n return hasPositiveSourceAmount ? totalUsd : undefined;\n };\n\n const buildReceiveAmountIssue = ({\n destinationRate,\n destinationToken = toToken,\n inputAmount = amount,\n mode = activeMode,\n sourceTokens = fromTokens,\n type = swapType,\n }: {\n destinationRate?: Decimal;\n destinationToken?: SwapTokenOption;\n inputAmount?: string;\n mode?: NexusWidgetMode;\n sourceTokens?: SwapTokenOption[];\n type?: SwapType;\n } = {}): ReceiveAmountIssue | null => {\n const limit = getDestinationReceiveLimitUsd(destinationToken);\n if (!limit || !destinationToken) return null;\n\n const parsedAmount = parseFiatNumber(inputAmount);\n if (!parsedAmount || parsedAmount.lte(0)) return null;\n if (mode === \"swap\" && type === \"exactIn\" && sourceTokens.length === 0) {\n return null;\n }\n\n const chainName = getShortChainName(\n destinationToken.chainId,\n destinationToken.chainName\n );\n const resolvedDestinationRate =\n destinationRate ??\n getImmediateDestinationReceiveUsdRate(destinationToken);\n\n if (!resolvedDestinationRate || resolvedDestinationRate.lte(0)) {\n return {\n ctaLabel: \"Price unavailable\",\n message: `Unable to price ${destinationToken.symbol} on ${chainName}. Select another receive token.`,\n type: \"unpricedReceiveToken\",\n };\n }\n\n let receiveUsd: Decimal | undefined;\n if (mode === \"swap\" && type === \"exactIn\") {\n receiveUsd = getExactInSourceUsdForReceiveLimit(\n sourceTokens,\n inputAmount\n );\n if (!receiveUsd || receiveUsd.lte(0)) {\n return {\n ctaLabel: \"Price unavailable\",\n message: `Unable to price selected assets for ${chainName}'s receive limit.`,\n type: \"unpricedReceiveToken\",\n };\n }\n } else if (mode === \"deposit\" && depositAmountMode === \"usd\") {\n receiveUsd = parsedAmount;\n } else {\n receiveUsd = parsedAmount.mul(resolvedDestinationRate);\n }\n\n if (receiveUsd.gt(limit)) {\n return {\n ctaLabel: \"Receive limit exceeded\",\n message: `Maximum receive amount on ${chainName} is ${formatUsdDisplay(limit)}.`,\n type: \"receiveLimitExceeded\",\n };\n }\n\n return null;\n };\n\n const buildConfiguredAmountIssue = (\n inputAmount = amount\n ): ReceiveAmountIssue | null => {\n if (!amountInputConfig || !inputAmount) return null;\n const parsedAmount = parseFiatNumber(inputAmount);\n if (!parsedAmount || parsedAmount.lte(0)) return null;\n\n const minAmount = parseFiatNumber(amountInputConfig.min);\n if (minAmount && parsedAmount.lt(minAmount)) {\n return {\n ctaLabel: \"Below minimum\",\n message: `Minimum amount is ${minAmount.toFixed()}.`,\n type: \"configuredAmountLimit\",\n };\n }\n\n const maxAmount = parseFiatNumber(amountInputConfig.max);\n if (maxAmount && parsedAmount.gt(maxAmount)) {\n return {\n ctaLabel: \"Above maximum\",\n message: `Maximum amount is ${maxAmount.toFixed()}.`,\n type: \"configuredAmountLimit\",\n };\n }\n\n return null;\n };\n\n const clearPreviewForBlockingAmountIssue = () => {\n clearPendingSwapIntent(true);\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setTxError(null);\n setSwapQuoteIssue(null);\n };\n\n const applyReceiveAmountIssue = (issue: ReceiveAmountIssue | null) => {\n const key = issue ? `${issue.type}:${issue.message}` : \"\";\n receiveAmountIssueRef.current = issue;\n if (receiveAmountIssueKeyRef.current !== key) {\n receiveAmountIssueKeyRef.current = key;\n setReceiveAmountIssue(issue);\n }\n if (!issue) return;\n\n clearPendingSwapIntent(true);\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setTxError(null);\n };\n\n const getPredictiveExactInSourceTokens = () => {\n const expanded = getExpandedSourceTokens(fromTokens);\n if (expanded.length === 0) return [];\n\n return expanded\n .map((token) => {\n const userAmount =\n token.userAmount ||\n (expanded.length === 1 && hasPositiveDecimalInput(amount)\n ? amount\n : \"\");\n return { ...token, userAmount };\n })\n .filter((token) => hasPositiveDecimalInput(token.userAmount));\n };\n\n const sortUnifiedSourceTokens = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const fiatDiff = getTokenBalanceUsd(b).cmp(getTokenBalanceUsd(a));\n if (fiatDiff !== 0) return fiatDiff;\n return getTokenBalanceAmount(b).cmp(getTokenBalanceAmount(a));\n });\n\n const allocateUnifiedExactInToken = (\n token: SwapTokenOption,\n fallbackAmount?: string\n ) => {\n if (!token.isUnified || !token.sourceTokens?.length) return [token];\n\n const rawAmount =\n parseFiatNumber(token.userAmount || fallbackAmount) ?? new Decimal(0);\n if (rawAmount.lte(0)) return [];\n\n const sortedSources = sortUnifiedSourceTokens(token.sourceTokens).filter(\n (source) =>\n source.chainId &&\n source.contractAddress &&\n getTokenBalanceAmount(source).gt(0) &&\n hasMinimumSourceUsdBalance(source)\n );\n const allocated: SwapTokenOption[] = [];\n\n if (token.userAmountMode === \"usd\") {\n let remainingUsd = rawAmount;\n\n for (const source of sortedSources) {\n if (remainingUsd.lte(0)) break;\n\n const availableUsd = getTokenBalanceUsd(source);\n if (availableUsd.lte(0)) continue;\n\n const targetUsd = Decimal.min(remainingUsd, availableUsd);\n const tokenAmount = getTokenAmountForUsd(\n source,\n targetUsd\n ).toDecimalPlaces(\n Math.max(0, source.decimals || 18),\n Decimal.ROUND_DOWN\n );\n if (tokenAmount.lte(0)) continue;\n\n const actualUsd = getUsdForTokenAmount(source, tokenAmount);\n allocated.push({\n ...source,\n userAmount: tokenAmount.toFixed(),\n userAmountMode: \"token\",\n userAmountUsd: actualUsd\n .toDecimalPlaces(6, Decimal.ROUND_DOWN)\n .toFixed(),\n });\n remainingUsd = remainingUsd.minus(targetUsd);\n }\n\n return allocated;\n }\n\n let remainingTokenAmount = rawAmount;\n\n for (const source of sortedSources) {\n if (remainingTokenAmount.lte(0)) break;\n\n const availableTokenAmount = getTokenBalanceAmount(source);\n if (availableTokenAmount.lte(0)) continue;\n\n const tokenAmount = Decimal.min(\n remainingTokenAmount,\n availableTokenAmount\n ).toDecimalPlaces(Math.max(0, source.decimals || 18), Decimal.ROUND_DOWN);\n if (tokenAmount.lte(0)) continue;\n\n const actualUsd = getUsdForTokenAmount(source, tokenAmount);\n allocated.push({\n ...source,\n userAmount: tokenAmount.toFixed(),\n userAmountMode: \"token\",\n userAmountUsd: actualUsd\n .toDecimalPlaces(6, Decimal.ROUND_DOWN)\n .toFixed(),\n });\n remainingTokenAmount = remainingTokenAmount.minus(tokenAmount);\n }\n\n return allocated;\n };\n\n const getExactInSourceTokens = (\n tokens: SwapTokenOption[],\n fallbackAmount?: string\n ) =>\n tokens\n .flatMap((token) =>\n token.isUnified\n ? allocateUnifiedExactInToken(token, fallbackAmount)\n : [token]\n )\n .filter(hasMinimumSourceUsdBalance);\n\n const hasPositiveDecimalInput = (value: unknown) =>\n Boolean(parseFiatNumber(value)?.gt(0));\n\n const hasPositiveSourceSpend = (token: SwapTokenOption) =>\n hasPositiveDecimalInput(token.userAmount) ||\n hasPositiveDecimalInput(token.userAmountUsd);\n\n const getReadyExactInSourceTokens = (tokens: SwapTokenOption[]) =>\n getExactInSourceTokens(tokens).filter(\n (token) =>\n Boolean(token.chainId && token.contractAddress) &&\n hasPositiveDecimalInput(token.userAmount)\n );\n\n const hasReadyExactInSwapInput = (\n tokens: SwapTokenOption[],\n destination?: SwapTokenOption\n ) =>\n Boolean(\n destination?.chainId &&\n destination.contractAddress &&\n getReadyExactInSourceTokens(tokens).length > 0\n );\n\n const getExpandedSourceTokens = (tokens: SwapTokenOption[]) => {\n const expanded = tokens.flatMap((token) =>\n token.isUnified && token.sourceTokens?.length\n ? token.sourceTokens\n : [token]\n );\n const seen = new Set();\n return expanded.filter((token) => {\n if (!token.chainId || !token.contractAddress) return false;\n if (\n !isSwapSupportedBySdkChainList(\n token.chainId,\n swapSupportedChainsAndTokens\n )\n ) {\n return false;\n }\n const key = `${token.chainId}-${token.contractAddress.toLowerCase()}`;\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n });\n };\n\n const getNativeGasBalanceForChain = (chainId: number) => {\n const nativeSymbol =\n CHAIN_METADATA[chainId]?.nativeCurrency?.symbol?.toUpperCase();\n let balance = new Decimal(0);\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== chainId) continue;\n const breakdownSymbol = (\n breakdown.symbol ??\n asset.symbol ??\n \"\"\n ).toUpperCase();\n const assetSymbol = (asset.symbol ?? \"\").toUpperCase();\n const isNativeBalance =\n isNativeTokenAddress(breakdown.contractAddress) ||\n Boolean(\n nativeSymbol &&\n (breakdownSymbol === nativeSymbol || assetSymbol === nativeSymbol)\n );\n\n if (!isNativeBalance) continue;\n balance = balance.plus(\n parseFiatNumber(breakdown.balance) ?? new Decimal(0)\n );\n }\n }\n\n return balance;\n };\n\n const hasGasForSource = (token: SwapTokenOption) => {\n if (!token.chainId || !token.contractAddress) return false;\n const tokenBalance = parseFiatNumber(token.balance) ?? new Decimal(0);\n if (tokenBalance.lte(0)) return false;\n if (isNativeTokenAddress(token.contractAddress)) return true;\n return getNativeGasBalanceForChain(token.chainId).gt(0);\n };\n\n const getGasCapableBalanceSourceTokens = () => {\n const tokens: SwapTokenOption[] = [];\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const chainId = breakdown.chain?.id;\n const contractAddress = breakdown.contractAddress;\n const balance = parseFiatNumber(breakdown.balance) ?? new Decimal(0);\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n if (\n !chainId ||\n !contractAddress ||\n balance.lte(0) ||\n !fiatBalance ||\n fiatBalance.lt(minimumSourceUsd)\n )\n continue;\n\n const chainMeta = CHAIN_METADATA[chainId];\n const symbol = breakdown.symbol ?? asset.symbol;\n tokens.push({\n chainId,\n chainLogo: chainMeta?.logo ?? breakdown.chain?.logo,\n chainName: getShortChainName(\n chainId,\n chainMeta?.name ?? breakdown.chain?.name\n ),\n contractAddress,\n decimals: breakdown.decimals ?? asset.decimals ?? 18,\n logo: asset.logo ?? \"\",\n name: symbol,\n symbol,\n balance: `${breakdown.balance} ${symbol}`,\n balanceInFiat:\n fiatBalance !== undefined\n ? `$${fiatBalance.toDecimalPlaces(2).toFixed()}`\n : \"$0.00\",\n });\n }\n }\n\n return getExpandedSourceTokens(tokens).filter(hasGasForSource);\n };\n\n const getMinimumBalanceSourceTokens = () =>\n filterMinimumSourceUsdTokens(\n getExpandedSourceTokens(\n swapBalance\n ? deriveTokenOptions(swapBalance, swapSupportedChainsAndTokens)\n : []\n )\n );\n const getHeldDestinationTokenOption = () => {\n if (!toToken?.chainId || !toToken.contractAddress) return undefined;\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const chainId = breakdown.chain?.id;\n if (chainId !== toToken.chainId) continue;\n\n const breakdownAddress = breakdown.contractAddress;\n const addressMatches =\n breakdownAddress &&\n (breakdownAddress.toLowerCase() ===\n toToken.contractAddress.toLowerCase() ||\n (isNativeTokenAddress(breakdownAddress) &&\n isNativeTokenAddress(toToken.contractAddress)));\n const symbolMatches =\n (breakdown.symbol ?? asset.symbol ?? \"\").toUpperCase() ===\n toToken.symbol.toUpperCase();\n\n if (!addressMatches && !symbolMatches) continue;\n\n const balanceAmount = parseFiatNumber(breakdown.balance);\n if (!balanceAmount || balanceAmount.lte(0)) continue;\n\n const chainMeta = CHAIN_METADATA[chainId];\n const symbol = breakdown.symbol ?? asset.symbol ?? toToken.symbol;\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n\n return {\n chainId,\n chainLogo:\n chainMeta?.logo ?? breakdown.chain?.logo ?? toToken.chainLogo,\n chainName: getShortChainName(\n chainId,\n chainMeta?.name ?? breakdown.chain?.name ?? toToken.chainName\n ),\n contractAddress: breakdown.contractAddress ?? toToken.contractAddress,\n decimals:\n breakdown.decimals ?? asset.decimals ?? toToken.decimals ?? 18,\n logo: asset.logo ?? toToken.logo,\n name: symbol,\n symbol,\n balance: `${breakdown.balance} ${symbol}`,\n balanceInFiat:\n fiatBalance !== undefined\n ? `$${fiatBalance.toDecimalPlaces(2).toFixed()}`\n : \"$0.00\",\n } satisfies SwapTokenOption;\n }\n }\n\n return undefined;\n };\n\n const getDepositDestinationForSourceSelection = () => {\n const destination =\n activeMode === \"deposit\" ? selectedOpportunity : toToken;\n const chainId = destination?.chainId;\n const tokenAddress =\n activeMode === \"deposit\"\n ? selectedOpportunity?.tokenAddress\n : toToken?.contractAddress;\n const tokenSymbol =\n activeMode === \"deposit\"\n ? selectedOpportunity?.tokenSymbol\n : toToken?.symbol;\n\n if (!chainId || !tokenAddress || !tokenSymbol) return undefined;\n\n return {\n chainId,\n tokenAddress: tokenAddress as `0x${string}`,\n tokenSymbol,\n };\n };\n const getDestinationSourceIdForDeposit = () => {\n const destination = getDepositDestinationForSourceSelection();\n return destination\n ? getDepositSourceId(destination.tokenAddress, destination.chainId)\n : undefined;\n };\n const getDepositSourceTargetUsd = () => {\n if (activeMode !== \"deposit\") return undefined;\n const requestedUsd = depositUsdDecimal;\n if (!requestedUsd || requestedUsd.lte(0)) return undefined;\n\n const coverage = getExactOutDestinationBalanceCoverage({\n requestedAmount: depositTokenAmountForQuote,\n requestedUsd,\n token: toToken,\n });\n return Decimal.max(\n requestedUsd.minus(coverage?.usd ?? new Decimal(0)),\n new Decimal(0)\n );\n };\n\n const getDepositSourceIdsFromTokens = (tokens: SwapTokenOption[]) =>\n getExpandedSourceTokens(tokens)\n .filter((token) => token.chainId && token.contractAddress)\n .map((token) =>\n getDepositSourceId(token.contractAddress, token.chainId!)\n );\n\n const getDepositTokenOptionsBySourceId = () => {\n const map = new Map();\n const sourceTokens = [\n ...(swapBalance\n ? deriveTokenOptions(swapBalance, swapSupportedChainsAndTokens)\n : []),\n ...fromTokens,\n ];\n\n for (const token of getExpandedSourceTokens(sourceTokens)) {\n if (!token.chainId || !token.contractAddress) continue;\n const id = getDepositSourceId(token.contractAddress, token.chainId);\n if (!map.has(id)) {\n map.set(id, {\n ...token,\n userAmount: \"\",\n });\n }\n }\n\n return map;\n };\n\n const getDepositSourceTokensForIds = (sourceIds: string[]) => {\n const tokenBySourceId = getDepositTokenOptionsBySourceId();\n return sourceIds\n .map((sourceId) => tokenBySourceId.get(sourceId))\n .filter((token): token is SwapTokenOption => Boolean(token))\n .map((token) => ({ ...token, userAmount: \"\" }));\n };\n\n const getResolvedDepositSourceSelection = (options?: {\n filter?: DepositSourceFilter;\n selectedTokens?: SwapTokenOption[];\n isManualSelection?: boolean;\n targetAmountUsd?: Decimal;\n }) => {\n const destination = getDepositDestinationForSourceSelection();\n if (!destination) {\n return { sourcePoolIds: [], selectedSourceIds: [], fromSources: [] };\n }\n\n const manualSelection =\n options?.isManualSelection ?? sourceSelectionTouched;\n const selectedTokensForResolution = options?.selectedTokens ?? fromTokens;\n const selectedSourceIds = getDepositSourceIdsFromTokens(\n selectedTokensForResolution\n );\n const destinationSourceId = getDestinationSourceIdForDeposit();\n const targetAmountUsd =\n options?.targetAmountUsd ??\n (activeMode === \"deposit\"\n ? getDepositSourceTargetUsd()\n : activeMode === \"send\"\n ? new Decimal(sendAmountUsd || 0)\n : undefined);\n\n return resolveDepositSourceSelection({\n swapBalance,\n destination,\n filter: manualSelection\n ? \"custom\"\n : (options?.filter ?? depositSourceFilter),\n selectedSourceIds,\n isManualSelection: manualSelection,\n minimumBalanceUsd: minimumSourceUsd.toNumber(),\n targetAmountUsd: targetAmountUsd?.toNumber(),\n excludedSourceIds: destinationSourceId ? [destinationSourceId] : [],\n });\n };\n\n const getExactOutSourceTokens = (\n mode: \"all\" | \"selected\" = exactOutQuoteSourceModeRef.current,\n targetAmountUsd?: Decimal\n ) => {\n if (activeMode === \"deposit\") {\n const selection = getResolvedDepositSourceSelection({ targetAmountUsd });\n return getDepositSourceTokensForIds(\n mode === \"all\" ? selection.sourcePoolIds : selection.selectedSourceIds\n );\n }\n\n if (activeMode === \"send\" && mode === \"selected\" && fromTokens.length > 0) {\n return filterMinimumSourceUsdTokens(\n getExpandedSourceTokens(fromTokens)\n ).filter(hasGasForSource);\n }\n\n return getGasCapableBalanceSourceTokens();\n };\n\n const buildExplicitSourcesPayload = (tokens: SwapTokenOption[]) => {\n const eligibleTokens = filterMinimumSourceUsdTokens(tokens).filter(\n (token) => token.chainId && token.contractAddress\n );\n return {\n sources: eligibleTokens.map((token) => ({\n chainId: token.chainId!,\n tokenAddress: token.contractAddress as `0x${string}`,\n })),\n };\n };\n\n const getSdkSourceKey = (source: {\n chainId: number;\n tokenAddress: `0x${string}`;\n }) => {\n const normalizedAddress = isNativeTokenAddress(source.tokenAddress)\n ? zeroAddress\n : source.tokenAddress.toLowerCase();\n return `${source.chainId}:${normalizedAddress}`;\n };\n\n const dedupeSdkSources = (\n sources: Array<\n | {\n chainId: number;\n tokenAddress: `0x${string}`;\n }\n | undefined\n >\n ) => {\n const seen = new Set();\n return sources.filter(\n (\n source\n ): source is {\n chainId: number;\n tokenAddress: `0x${string}`;\n } => {\n if (!source) return false;\n const key = getSdkSourceKey(source);\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n }\n );\n };\n\n const getHeldDestinationTokenSource = () => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n if (\n !heldDestinationToken?.chainId ||\n !heldDestinationToken.contractAddress\n ) {\n return undefined;\n }\n\n return {\n chainId: heldDestinationToken.chainId,\n tokenAddress: heldDestinationToken.contractAddress as `0x${string}`,\n };\n };\n\n const shouldSendExactOutSourceAllowlist = () => {\n if (activeMode === \"deposit\") {\n return sourceSelectionTouched || depositSourceFilter !== \"all\";\n }\n\n if (activeMode === \"send\") {\n return exactOutQuoteSourceModeRef.current === \"selected\";\n }\n\n return true;\n };\n\n const buildExactOutSourcesPayload = (tokens: SwapTokenOption[]) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n return buildExplicitSourcesPayload(tokens);\n }\n\n const sourceTokens = shouldSendExactOutSourceAllowlist()\n ? tokens\n : getExactOutSourceTokens(\"all\");\n const explicitSources = buildExplicitSourcesPayload(sourceTokens).sources;\n const sources = dedupeSdkSources([\n ...explicitSources,\n getHeldDestinationTokenSource(),\n ]);\n\n return sources.length > 0 ? { sources } : {};\n };\n\n const buildPredictiveExactOutSources = async (requiredSourceUsd: Decimal) => {\n if (requiredSourceUsd.lte(0)) return [];\n\n const destinationKey = getTokenSelectionKey(toToken);\n const candidates = sortExactOutSourcesBySdkPriority(\n getExactOutSourceTokens(\n exactOutQuoteSourceModeRef.current,\n requiredSourceUsd\n )\n .filter((token) => getTokenSelectionKey(token) !== destinationKey)\n .filter(hasMinimumSourceUsdBalance)\n );\n const sources: SwapTokenOption[] = [];\n let remainingUsd = requiredSourceUsd;\n\n for (const token of candidates) {\n if (remainingUsd.lte(0)) break;\n\n const availableUsd = getTokenBalanceUsd(token);\n if (availableUsd.lt(minimumSourceUsd)) continue;\n\n const rate = await resolveUsdRateForToken(token);\n if (rate.lte(0)) continue;\n\n const targetUsd = Decimal.min(remainingUsd, availableUsd);\n const tokenAmount = targetUsd\n .div(rate)\n .toDecimalPlaces(Math.max(0, token.decimals || 18), Decimal.ROUND_DOWN);\n if (tokenAmount.lte(0)) continue;\n\n sources.push({\n ...token,\n userAmount: tokenAmount.toFixed(),\n userAmountMode: \"token\",\n userAmountUsd: targetUsd\n .toDecimalPlaces(6, Decimal.ROUND_DOWN)\n .toFixed(),\n });\n remainingUsd = remainingUsd.minus(targetUsd);\n }\n\n return remainingUsd.gt(0.01) ? [] : sources;\n };\n\n const getErrorText = (error: unknown) => {\n const err = error as any;\n const parts = [\n err?.message,\n typeof error === \"string\" ? error : undefined,\n err?.code,\n ];\n\n try {\n if (err?.data) parts.push(JSON.stringify(err.data));\n } catch {\n // Ignore non-serializable SDK error metadata.\n }\n\n return parts.filter(Boolean).join(\" \");\n };\n\n const isInsufficientSourcesError = (error: unknown) => {\n const err = error as any;\n const message = getErrorText(error).toLowerCase();\n\n return (\n err?.code === ERROR_CODES.INSUFFICIENT_BALANCE ||\n message.includes(\"insufficient balance\") ||\n message.includes(\"sources are not enough\") ||\n (message.includes(\"source\") && message.includes(\"not enough\"))\n );\n };\n\n const isViemInvalidDecimalError = (error: unknown) =>\n getErrorText(error).toLowerCase().includes(\"not a valid decimal number\");\n\n const parseLabeledErrorDecimal = (text: string, label: string) => {\n const match = text.match(\n new RegExp(`${label}\\\\s*:\\\\s*\\\\$?\\\\s*([0-9][0-9,]*(?:\\\\.[0-9]+)?)`, \"i\")\n );\n return match ? parseFiatNumber(match[1]) : undefined;\n };\n\n const getExactOutRequestedUsd = () => {\n const amountNumber = parseFiatNumber(amount);\n if (!amountNumber || amountNumber.lte(0) || !toToken?.symbol) {\n return undefined;\n }\n\n if (activeMode === \"deposit\" && depositAmountMode === \"usd\") {\n return amountNumber;\n }\n\n const fiatValue = getFiatValue(amountNumber.toNumber(), toToken.symbol);\n return Number.isFinite(fiatValue) && fiatValue > 0\n ? new Decimal(fiatValue)\n : undefined;\n };\n\n const getExactOutAvailableSourceUsd = (\n sourceTokensOverride?: SwapTokenOption[]\n ): Decimal => {\n const sumTokensWithDestinationCredit = (\n tokens: SwapTokenOption[]\n ): Decimal => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n const heldDestinationKey = getTokenSelectionKey(heldDestinationToken);\n let hasHeldDestinationToken = false;\n\n const sourceTotal = tokens.reduce((sum, token) => {\n const value = parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n const isHeldDestinationToken =\n heldDestinationKey &&\n getTokenSelectionKey(token) === heldDestinationKey;\n if (isHeldDestinationToken) {\n hasHeldDestinationToken = true;\n }\n\n return value.gt(0) &&\n (isHeldDestinationToken || value.gte(minimumSourceUsd))\n ? sum.plus(value)\n : sum;\n }, new Decimal(0));\n\n if (!heldDestinationToken || hasHeldDestinationToken) {\n return sourceTotal;\n }\n\n const heldDestinationUsd =\n parseFiatNumber(heldDestinationToken.balanceInFiat) ?? new Decimal(0);\n return heldDestinationUsd.gt(0)\n ? sourceTotal.plus(heldDestinationUsd)\n : sourceTotal;\n };\n\n if (\n sourceTokensOverride ||\n exactOutQuoteSourceModeRef.current === \"selected\"\n ) {\n const selectedSourceTokens = sourceTokensOverride ?? fromTokens;\n return sumTokensWithDestinationCredit(selectedSourceTokens);\n }\n\n return getExactOutTotalSourceBalanceUsd();\n };\n const getExactOutTotalSourceBalanceUsd = (): Decimal => {\n const allSourceTotal: Decimal = getExactOutAvailableSourceUsd(\n getMinimumBalanceSourceTokens()\n );\n\n return allSourceTotal.gt(0) ? allSourceTotal : getSwapBalanceTotalUsd();\n };\n\n const getExactOutIntentSourceUsd = () => {\n const intentSourceUsd = (intentData?.sources ?? []).reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0)\n );\n return intentSourceUsd.gt(0) ? intentSourceUsd : undefined;\n };\n\n const getExactOutHeldDestinationUsd = () => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n const value = parseFiatNumber(heldDestinationToken?.balanceInFiat);\n return value && value.gt(0) ? value : new Decimal(0);\n };\n\n const exactOutIntentIncludesHeldDestination = () => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n const heldDestinationKey = getTokenSelectionKey(heldDestinationToken);\n if (!heldDestinationKey) return false;\n\n return (intentData?.sources ?? []).some((source) => {\n const chainId = source.chain?.id;\n const tokenAddress = source.token?.contractAddress;\n if (!chainId || !tokenAddress) return false;\n\n return (\n getTokenSelectionKey({\n chainId,\n contractAddress: tokenAddress,\n } as SwapTokenOption) === heldDestinationKey\n );\n });\n };\n\n const getExactOutRequiredSourceUsd = () => {\n const intentSourceUsd = getExactOutIntentSourceUsd();\n if (intentSourceUsd) return intentSourceUsd;\n return undefined;\n };\n\n const getExactOutRequiredFundingUsd = () => {\n const sourceRequiredUsd = getExactOutRequiredSourceUsd();\n if (sourceRequiredUsd) {\n const destinationCreditUsd = exactOutIntentIncludesHeldDestination()\n ? new Decimal(0)\n : getExactOutHeldDestinationUsd();\n return sourceRequiredUsd.plus(destinationCreditUsd);\n }\n\n return getExactOutRequestedUsd();\n };\n\n const getExactInSourceDeficitUsd = () => {\n if (swapType !== \"exactIn\" || fromTokens.length === 0) return undefined;\n\n return fromTokens.reduce((sum, token) => {\n const requestedAmount = parseFiatNumber(token.userAmount);\n if (!requestedAmount || requestedAmount.lte(0)) return sum;\n\n if (token.userAmountMode === \"usd\") {\n const availableUsd = parseFiatNumber(token.balanceInFiat);\n if (!availableUsd || requestedAmount.lte(availableUsd)) return sum;\n return sum.plus(requestedAmount.minus(availableUsd));\n }\n\n const availableTokenAmount = parseFiatNumber(token.balance);\n if (!availableTokenAmount || requestedAmount.lte(availableTokenAmount)) {\n return sum;\n }\n\n const missingTokenAmount = requestedAmount.minus(availableTokenAmount);\n const fiatBalance = parseFiatNumber(token.balanceInFiat);\n if (fiatBalance && availableTokenAmount.gt(0)) {\n return sum.plus(\n missingTokenAmount.mul(fiatBalance.div(availableTokenAmount))\n );\n }\n\n return sum;\n }, new Decimal(0));\n };\n\n const buildInsufficientSourcesIssue = (error: unknown): SwapQuoteIssue => {\n const errorText = getErrorText(error);\n const details =\n (error as any)?.data?.details ?? (error as any)?.details ?? {};\n const requiredFromError =\n parseFiatNumber(\n details.requiredUsd ??\n details.requiredUSD ??\n details.requiredAmountUsd ??\n details.requiredAmount ??\n details.required\n ) ?? parseLabeledErrorDecimal(errorText, \"required\");\n const availableFromError =\n parseFiatNumber(\n details.availableUsd ??\n details.availableUSD ??\n details.availableAmountUsd ??\n details.availableAmount ??\n details.available\n ) ?? parseLabeledErrorDecimal(errorText, \"available\");\n const requestedUsd = getExactOutRequiredFundingUsd();\n const availableUsd = getExactOutAvailableSourceUsd();\n const exactInSourceDeficitUsd = getExactInSourceDeficitUsd();\n\n let missingUsd =\n exactInSourceDeficitUsd && exactInSourceDeficitUsd.gt(0)\n ? exactInSourceDeficitUsd\n : requiredFromError && availableFromError\n ? requiredFromError.minus(availableFromError)\n : undefined;\n\n if (\n requestedUsd &&\n (!missingUsd || missingUsd.lte(0) || missingUsd.gt(requestedUsd.mul(5)))\n ) {\n missingUsd = requestedUsd.minus(availableUsd);\n }\n\n if (missingUsd && missingUsd.gt(0)) {\n const formattedMissing =\n missingUsd.gt(0) && missingUsd.lt(0.01)\n ? \"<$0.01\"\n : formatUsdDisplay(missingUsd);\n\n return {\n type: \"insufficientSources\",\n missingUsd: missingUsd.toDecimalPlaces(2).toFixed(),\n message: `Need ${formattedMissing} more across your assets`,\n };\n }\n\n return {\n type: \"insufficientSources\",\n message: \"Add more source balance across your assets\",\n };\n };\n\n const buildExactOutSourceBalanceIssue = (\n sourceTokensOverride?: SwapTokenOption[]\n ): SwapQuoteIssue | null => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return null;\n const requestedUsd = getExactOutRequiredFundingUsd();\n const availableUsd = getExactOutAvailableSourceUsd(sourceTokensOverride);\n if (!requestedUsd || requestedUsd.lte(0) || !availableUsd) return null;\n\n const missingUsd = requestedUsd.minus(availableUsd);\n if (missingUsd.lte(0.01)) return null;\n\n const formattedMissing =\n missingUsd.gt(0) && missingUsd.lt(0.01)\n ? \"<$0.01\"\n : formatUsdDisplay(missingUsd);\n\n return {\n type: \"insufficientSources\",\n missingUsd: missingUsd.toDecimalPlaces(2).toFixed(),\n message: `Need ${formattedMissing} more across your assets`,\n };\n };\n\n const isNativeTokenAddress = (address?: string) =>\n !address ||\n address.toLowerCase() === \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\" ||\n address.toLowerCase() === \"0x0000000000000000000000000000000000000000\";\n\n const formatReadableTokenAmount = (rawAmount: bigint, decimals: number) =>\n new Decimal(rawAmount.toString())\n .div(new Decimal(10).pow(decimals))\n .toFixed();\n\n const formatReadableTokenBalanceAmount = (\n rawAmount: bigint,\n decimals: number\n ) =>\n new Decimal(rawAmount.toString())\n .div(new Decimal(10).pow(decimals))\n .toDecimalPlaces(6)\n .toFixed();\n\n const trimDecimalString = (value: string) =>\n value.replace(/(\\.\\d*?)0+$/, \"$1\").replace(/\\.$/, \"\");\n\n const buildSourceTokenSnapshotMap = useCallback(\n (balances: UserAsset[] | null | undefined) => {\n const snapshots = new Map();\n\n for (const asset of balances ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const chainId = breakdown.chain?.id;\n const contractAddress = breakdown.contractAddress;\n const symbol = breakdown.symbol ?? asset.symbol;\n\n if (\n !chainId ||\n !contractAddress ||\n !symbol ||\n !isSwapSupportedBySdkChainList(\n chainId,\n swapSupportedChainsAndTokens\n )\n ) {\n continue;\n }\n\n const chainMeta = CHAIN_METADATA[chainId];\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n const snapshot: SwapTokenOption = {\n balance: `${breakdown.balance ?? \"0\"} ${symbol}`,\n balanceInFiat: fiatBalance\n ? formatUsdDisplay(fiatBalance)\n : \"$0.00\",\n chainId,\n chainLogo: chainMeta?.logo ?? breakdown.chain?.logo,\n chainName: getShortChainName(\n chainId,\n chainMeta?.name ?? breakdown.chain?.name\n ),\n contractAddress,\n decimals: breakdown.decimals ?? asset.decimals ?? 18,\n logo: asset.logo ?? \"\",\n name: symbol,\n symbol,\n };\n snapshots.set(getTokenSelectionKey(snapshot), snapshot);\n }\n }\n\n return snapshots;\n },\n [swapSupportedChainsAndTokens]\n );\n\n const patchSourceTokensWithBalances = useCallback(\n (tokens: SwapTokenOption[], balances: UserAsset[]) => {\n const snapshots = buildSourceTokenSnapshotMap(balances);\n\n const updateToken = (token: SwapTokenOption): SwapTokenOption => {\n const preservedAmounts = {\n userAmount: token.userAmount,\n userAmountMode: token.userAmountMode,\n userAmountUsd: token.userAmountUsd,\n };\n\n if (token.isUnified) {\n const sourceTokens = (token.sourceTokens ?? []).map(updateToken);\n const totalBalance = sourceTokens.reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber(source.balance) ?? new Decimal(0)),\n new Decimal(0)\n );\n const totalFiat = sourceTokens.reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber(source.balanceInFiat) ?? new Decimal(0)),\n new Decimal(0)\n );\n\n return {\n ...token,\n ...preservedAmounts,\n balance: totalBalance.toDecimalPlaces(8).toFixed(),\n balanceInFiat: formatUsdDisplay(totalFiat),\n sourceTokens,\n };\n }\n\n const snapshot = snapshots.get(getTokenSelectionKey(token));\n if (!snapshot) {\n return {\n ...token,\n ...preservedAmounts,\n balance: `0 ${token.symbol}`,\n balanceInFiat: \"$0.00\",\n chainLogo:\n token.chainLogo ??\n (token.chainId ? CHAIN_METADATA[token.chainId]?.logo : undefined),\n chainName: getShortChainName(token.chainId, token.chainName),\n };\n }\n\n return {\n ...token,\n ...snapshot,\n ...preservedAmounts,\n };\n };\n\n return tokens.map(updateToken);\n },\n [buildSourceTokenSnapshotMap]\n );\n\n const refreshSelectedSourceBalances = useCallback(async () => {\n const refreshedBalance = await fetchSwapBalance();\n const balances = refreshedBalance ?? swapBalance;\n if (!balances) return;\n\n setFromTokens((current) =>\n current.length === 0\n ? current\n : patchSourceTokensWithBalances(current, balances)\n );\n setSourceSelectionRevision((current) => current + 1);\n }, [fetchSwapBalance, patchSourceTokensWithBalances, swapBalance]);\n\n const receiveMaxSafetyMultiplier = new Decimal(\"0.9\");\n const currentSwapEntry =\n currentSwapId !== null\n ? swapHistory.find((entry) => entry.id === currentSwapId)\n : undefined;\n\n const scheduleTerminalBalanceRefresh = () => {\n if (terminalBalanceRefreshTimerRef.current) {\n clearTimeout(terminalBalanceRefreshTimerRef.current);\n }\n\n terminalBalanceRefreshTimerRef.current = setTimeout(() => {\n terminalBalanceRefreshTimerRef.current = null;\n void fetchSwapBalance();\n }, BALANCE_REFRESH_AFTER_TERMINAL_MS);\n };\n\n const patchSwapHistoryEntry = (\n id: string | null | undefined,\n patch: Partial\n ) => {\n if (!id) return;\n setSwapHistory((prev) =>\n sortSwapHistoryEntries(\n prev.map((entry) => (entry.id === id ? { ...entry, ...patch } : entry))\n )\n );\n };\n\n const patchCurrentSwapHistoryEntry = (patch: Partial) => {\n patchSwapHistoryEntry(currentSwapIdRef.current, patch);\n };\n\n const patchCurrentIntentExplorerUrl = (url?: string | null) => {\n if (!isHttpUrl(url)) return;\n if (intentUrlRef.current === url) return;\n\n intentUrlRef.current = url;\n const intentId = extractIntentIdFromUrl(url);\n patchCurrentSwapHistoryEntry({\n intentExplorerUrl: url,\n ...(intentId ? { intentId } : {}),\n });\n };\n\n const resetExplorerUrls = () => {\n const next = { sourceExplorerUrl: null, destinationExplorerUrl: null };\n explorerUrlsRef.current = next;\n setExplorerUrls(next);\n };\n\n const mergeExplorerUrls = (\n patch: Partial<{\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n }>\n ) => {\n const next = { ...explorerUrlsRef.current, ...patch };\n explorerUrlsRef.current = next;\n setExplorerUrls(next);\n patchCurrentSwapHistoryEntry({\n sourceExplorerUrl: next.sourceExplorerUrl,\n finalExplorerUrl: next.destinationExplorerUrl ?? next.sourceExplorerUrl,\n });\n };\n\n const resetProgressEvents = () => {\n progressEventsRef.current = [];\n setProgressEvents((current) => (current.length === 0 ? current : []));\n setFailedProgressStep((current) => (current === null ? current : null));\n };\n\n const appendProgressEvent = (\n name: string,\n step: SwapStepType | BridgeStepType | undefined,\n defaultCompleted: boolean,\n event?: unknown\n ) => {\n if (!step) return;\n const completed =\n typeof (step as any).completed === \"boolean\"\n ? Boolean((step as any).completed)\n : defaultCompleted;\n\n setProgressEvents((prev) => {\n const next = [\n ...prev,\n {\n id: `${Date.now()}-${prev.length}-${(step as any).typeID ?? (step as any).type ?? name}`,\n name,\n completed,\n event,\n step,\n },\n ];\n progressEventsRef.current = next;\n return next;\n });\n };\n\n const appendProgressListEvent = (\n name: string,\n stepList: Array\n ) => {\n if (stepList.length === 0) return;\n\n setProgressEvents((prev) => {\n const next = [\n ...prev,\n {\n id: `${Date.now()}-${prev.length}-${name}`,\n name,\n completed: false,\n step: stepList[0],\n steps: stepList,\n },\n ];\n progressEventsRef.current = next;\n return next;\n });\n };\n\n const startSwapHistoryEntry = () => {\n const id = `${Date.now()}-${swapRunIdRef.current}`;\n const now = Date.now();\n const resolvedToToken =\n toToken && destinationBalance\n ? { ...toToken, balance: destinationBalance }\n : toToken;\n const entry: SwapHistoryEntry = {\n id,\n mode: activeMode,\n status: \"pending\",\n createdAt: now,\n startedAt: now,\n intentData,\n fromTokens,\n toToken: resolvedToToken,\n requestedToAmount:\n activeMode === \"deposit\" || activeMode === \"send\"\n ? previewDestinationAmount\n : undefined,\n requestedToValue:\n activeMode === \"deposit\" || activeMode === \"send\"\n ? previewToAmountUsd\n : undefined,\n recipientAddress: transferRecipientAddress,\n opportunity: selectedOpportunity,\n feeUsd: intentFeeUsd,\n sourceExplorerUrl: null,\n finalExplorerUrl: null,\n intentExplorerUrl: isHttpUrl(intentUrlRef.current)\n ? intentUrlRef.current\n : null,\n intentId: extractIntentIdFromUrl(intentUrlRef.current),\n autoRefundAvailable: false,\n };\n\n currentSwapStartedAtRef.current = 0;\n currentSwapIdRef.current = id;\n setCurrentSwapId(id);\n setSwapHistory((prev) => sortSwapHistoryEntries([entry, ...prev]));\n return id;\n };\n\n const finishCurrentSwapHistoryEntry = (\n status: \"fulfilled\" | \"failed\" | \"timeout\",\n patch: Partial = {}\n ) => {\n const now = Date.now();\n const startedAt = currentSwapStartedAtRef.current || now;\n patchSwapHistoryEntry(currentSwapIdRef.current, {\n status,\n endedAt: now,\n durationSeconds: Math.max(1, Math.round((now - startedAt) / 1000)),\n sourceExplorerUrl: explorerUrlsRef.current.sourceExplorerUrl,\n finalExplorerUrl:\n explorerUrlsRef.current.destinationExplorerUrl ??\n explorerUrlsRef.current.sourceExplorerUrl,\n ...patch,\n });\n scheduleTerminalBalanceRefresh();\n };\n\n const markSwapExecutionStarted = () => {\n if (currentSwapStartedAtRef.current > 0) return;\n const now = Date.now();\n currentSwapStartedAtRef.current = now;\n patchCurrentSwapHistoryEntry({ startedAt: now });\n };\n\n const enterSkippedSwapProgress = () => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n\n const shouldInitializeProgress = swapStepRef.current !== \"progress\";\n if (!currentSwapIdRef.current) {\n onStart?.();\n startSwapHistoryEntry();\n }\n\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setPreviewQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setSwapQuoteIssue(null);\n\n if (shouldInitializeProgress) {\n resetProgressEvents();\n swapStepsListRef.current = [];\n resetSteps();\n swapStepRef.current = \"progress\";\n setSwapStep(\"progress\");\n }\n };\n\n const cachePredictiveBaselineFromIntent = (intent: SwapIntentData) => {\n const destinationAmount = parseFiatNumber(intent.destination?.amount);\n const destinationValue = parseFiatNumber(intent.destination?.value);\n const sourceUsd = (intent.sources ?? []).reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0)\n );\n\n if (!destinationAmount || destinationAmount.lte(0)) return;\n\n const destinationUsdRate =\n destinationValue && destinationValue.gt(0)\n ? destinationValue.div(destinationAmount)\n : getUsdRateForSymbol(intent.destination?.token?.symbol);\n if (destinationUsdRate.lte(0)) return;\n\n cacheSymbolUsdRate(intent.destination?.token?.symbol, destinationUsdRate);\n\n const key = getPredictiveQuoteCacheKey();\n if (!key) return;\n\n const baseline: PredictiveQuoteBaseline = {\n destinationUsdRate: destinationUsdRate.toDecimalPlaces(18).toFixed(),\n updatedAt: Date.now(),\n };\n\n if (activeMode === \"swap\" && swapType === \"exactIn\" && sourceUsd.gt(0)) {\n baseline.exactInDestinationAmountPerSourceUsd = destinationAmount\n .div(sourceUsd)\n .toDecimalPlaces(18)\n .toFixed();\n }\n\n const resolvedDestinationValue =\n destinationValue && destinationValue.gt(0)\n ? destinationValue\n : destinationAmount.mul(destinationUsdRate);\n if (\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n resolvedDestinationValue.gt(0) &&\n sourceUsd.gt(0)\n ) {\n baseline.exactOutSourceUsdPerDestinationUsd = sourceUsd\n .div(resolvedDestinationValue)\n .toDecimalPlaces(18)\n .toFixed();\n }\n\n predictiveQuoteCacheRef.current[key] = baseline;\n };\n\n const applySwapIntent = useCallback(\n (intent: SwapIntentData) => {\n const sortedIntent = {\n ...intent,\n sources: sortIntentSourcesByUsdDesc(intent.sources ?? []),\n };\n const enrichedIntent = enrichSwapIntentTokenMetadata(sortedIntent);\n const sortedIntentSourceTokens = sortSwapTokensByUsdDesc(\n (enrichedIntent.sources ?? []).map(buildIntentSourceToken)\n );\n\n lastSwapIntentRefreshAtRef.current = Date.now();\n lastIntentSourceTokensRef.current = sortedIntentSourceTokens;\n if (\n !sourceSelectionTouched &&\n (activeMode === \"send\" ||\n (activeMode === \"deposit\" && swapType === \"exactOut\"))\n ) {\n lastAutoIntentSourceTokensRef.current = sortedIntentSourceTokens;\n }\n cacheDestinationUsdRateFromIntent(enrichedIntent);\n cachePredictiveBaselineFromIntent(enrichedIntent);\n setIntentData(enrichedIntent);\n setIntentToAmount(enrichedIntent.destination?.amount || undefined);\n setSwapQuoteIssue(null);\n\n if (\n !sourceSelectionTouched &&\n swapStepRef.current !== \"choose-swap-asset\" &&\n (activeMode === \"send\" ||\n (activeMode === \"deposit\" && swapType === \"exactOut\"))\n ) {\n syncingIntentSourcesRef.current = true;\n setFromTokens(sortedIntentSourceTokens);\n }\n\n try {\n const bridgeFees = enrichedIntent.feesAndBuffer?.bridge;\n const bridgeFeeData =\n bridgeFees && typeof bridgeFees === \"object\" ? bridgeFees : undefined;\n const collectionFee = parseFiatNumber(bridgeFeeData?.collection);\n const fulfilmentFee = parseFiatNumber(bridgeFeeData?.fulfilment);\n const executionGasFee =\n parseFiatNumber(bridgeFeeData?.caGas) ??\n (collectionFee !== undefined || fulfilmentFee !== undefined\n ? (collectionFee ?? new Decimal(0)).plus(\n fulfilmentFee ?? new Decimal(0)\n )\n : undefined);\n const bridgeGasSuppliedFee = parseFiatNumber(\n bridgeFeeData?.gasSupplied\n );\n const destinationGasSuppliedFee =\n getIntentDestinationGasUsdValue(enrichedIntent);\n const gasSuppliedFee =\n bridgeGasSuppliedFee ?? destinationGasSuppliedFee;\n const bridgeComponentsTotal = bridgeFeeData\n ? [\n executionGasFee,\n parseFiatNumber(bridgeFeeData.protocol),\n parseFiatNumber(bridgeFeeData.solver),\n gasSuppliedFee,\n ].reduce(\n (sum, value) => sum.plus(value ?? new Decimal(0)),\n new Decimal(0)\n )\n : undefined;\n const rawBridgeTotal =\n typeof bridgeFees === \"string\"\n ? parseFiatNumber(bridgeFees)\n : parseFiatNumber(bridgeFeeData?.total);\n const bridgeTotal =\n rawBridgeTotal &&\n !bridgeGasSuppliedFee &&\n destinationGasSuppliedFee &&\n destinationGasSuppliedFee.gt(0)\n ? rawBridgeTotal.plus(destinationGasSuppliedFee)\n : (rawBridgeTotal ??\n (bridgeComponentsTotal && bridgeComponentsTotal.gt(0)\n ? bridgeComponentsTotal\n : destinationGasSuppliedFee));\n\n if (bridgeTotal !== undefined) {\n setIntentFeeUsd(\n bridgeTotal.gt(0) ? bridgeTotal.toDecimalPlaces(6).toFixed() : \"0\"\n );\n } else {\n setIntentFeeUsd(undefined);\n }\n } catch (err) {\n console.warn(\"Could not resolve bridge fee total\", err);\n setIntentFeeUsd(undefined);\n }\n },\n [\n activeMode,\n fromTokens,\n getFiatValue,\n sourceSelectionTouched,\n swapType,\n swapBalance,\n swapSupportedChainsAndTokens,\n supportedChainsAndTokens,\n toToken,\n ]\n );\n\n const handleSwapIntentCallback = useCallback(\n (data: any, runId: number, quoteInputKey: string) => {\n const { intent, allow, deny, refresh } = data;\n const bridgeProvider = normalizeBridgeProvider(\n data?.bridgeProvider ??\n intent?.bridgeProvider ??\n intent?.normalizedIntent?.bridgeProvider ??\n intent?.swap?.bridgeProvider\n );\n const intentWithBridgeProvider = normalizeRenderableSwapIntentData(\n intent,\n bridgeProvider\n );\n logSdkIntentEvent(\"onIntent\", data, {\n bridgeProvider,\n currentRunId: swapRunIdRef.current,\n normalizedIntent: intentWithBridgeProvider,\n isCurrentRun: swapRunIdRef.current === runId,\n quoteInputKey,\n runId,\n });\n if (swapRunIdRef.current !== runId) {\n logSdkIntentEvent(\"ignored stale onIntent\", data, {\n currentRunId: swapRunIdRef.current,\n quoteInputKey,\n runId,\n });\n deny();\n return;\n }\n if (quoteInputKey && activeQuoteInputKeyRef.current !== quoteInputKey) {\n logSdkIntentEvent(\"ignored stale quote input onIntent\", data, {\n currentQuoteInputKey: activeQuoteInputKeyRef.current,\n quoteInputKey,\n runId,\n });\n deny();\n return;\n }\n if (!intentWithBridgeProvider) {\n console.warn(\"[NexusWidget SDK][intent] Unsupported intent shape\", {\n intent,\n raw: data,\n });\n finishIntentFetchTiming(runId, \"failed\");\n deny();\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setTxError(\"Quote unavailable\");\n return;\n }\n const resolvedQuoteInputKey = quoteInputKey;\n const normalizedRefresh =\n typeof refresh === \"function\"\n ? async (...args: unknown[]) => {\n const refreshed = await refresh(...args);\n const refreshedBridgeProvider = normalizeBridgeProvider(\n refreshed?.bridgeProvider ??\n refreshed?.normalizedIntent?.bridgeProvider ??\n refreshed?.swap?.bridgeProvider ??\n bridgeProvider\n );\n return (\n normalizeRenderableSwapIntentData(\n refreshed,\n refreshedBridgeProvider\n ) ?? refreshed\n );\n }\n : refresh;\n finishIntentFetchTiming(runId);\n providerSwapIntent.current = {\n intent: intentWithBridgeProvider as any,\n allow,\n deny,\n refresh: normalizedRefresh,\n };\n swapIntentRef.current = {\n intent: intentWithBridgeProvider,\n allow,\n deny,\n refresh: normalizedRefresh,\n runId,\n quoteInputKey: resolvedQuoteInputKey,\n };\n flushSync(() => {\n applySwapIntent(intentWithBridgeProvider);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n });\n },\n [applySwapIntent, finishIntentFetchTiming, providerSwapIntent]\n );\n\n // Deposit-specific\n const [selectedOpportunity, setSelectedOpportunity] = useState<\n NexusWidgetDepositOpportunityConfig | undefined\n >(() => (activeMode === \"deposit\" ? configuredDeposit : undefined));\n const selectedOpportunityIdentity =\n getDepositConfigIdentity(selectedOpportunity);\n const [depositAmountMode, setDepositAmountMode] = useState<\"token\" | \"usd\">(\n \"token\"\n );\n const [depositSourceFilter, setDepositSourceFilter] =\n useState(\"all\");\n\n useEffect(() => {\n const immediateIssue = buildReceiveAmountIssue();\n applyReceiveAmountIssue(immediateIssue);\n\n if (!toToken || !getDestinationReceiveLimitUsd(toToken)) return;\n if (!parseFiatNumber(amount)?.gt(0)) return;\n if (getImmediateDestinationReceiveUsdRate(toToken)?.gt(0)) return;\n\n let cancelled = false;\n void resolveUsdRateForToken(toToken)\n .then((resolvedRate) => {\n if (cancelled) return;\n const issue = buildReceiveAmountIssue({\n destinationRate: resolvedRate.gt(0) ? resolvedRate : undefined,\n });\n applyReceiveAmountIssue(issue);\n })\n .catch(() => {\n if (!cancelled) {\n applyReceiveAmountIssue(buildReceiveAmountIssue());\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [\n activeMode,\n amount,\n depositAmountMode,\n fromTokens,\n swapType,\n toToken?.chainId,\n toToken?.chainName,\n toToken?.contractAddress,\n toToken?.priceUSD,\n toToken?.symbol,\n ]);\n\n const trackDeposit = useCallback(\n (event: string, props?: Record) => {\n const analytics = nexusSDK?.analytics;\n if (!analytics) return;\n analytics.track(event, {\n widgetSessionId: widgetSessionIdRef.current,\n widgetAttemptId: widgetAttemptIdRef.current,\n opportunityProtocol: selectedOpportunity?.protocol ?? null,\n destinationChainId: selectedOpportunity?.chainId ?? null,\n destinationToken: selectedOpportunity?.tokenSymbol ?? null,\n ...props,\n });\n },\n [nexusSDK, selectedOpportunity]\n );\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (!nexusSDK?.analytics) return;\n if (widgetOpenedFiredRef.current) return;\n widgetOpenedFiredRef.current = true;\n widgetOpenedTsRef.current = Date.now();\n rotateAttempt();\n trackDeposit(\"deposit_widget_opened\", {\n embed: Boolean(embed),\n depositConfigured: Boolean(configuredDeposit),\n prefillAmountPresent: Boolean(config.prefill?.amount),\n });\n }, [\n activeMode,\n nexusSDK,\n embed,\n configuredDeposit,\n config.prefill,\n rotateAttempt,\n trackDeposit,\n ]);\n\n useEffect(() => {\n analyticsRef.current = nexusSDK?.analytics ?? null;\n }, [nexusSDK]);\n\n useEffect(() => {\n selectedOpportunityRef.current = selectedOpportunity;\n }, [selectedOpportunity]);\n\n useEffect(() => {\n return () => {\n if (!widgetOpenedFiredRef.current) return;\n const analytics = analyticsRef.current;\n if (!analytics) return;\n const opp = selectedOpportunityRef.current;\n analytics.track(\"deposit_widget_closed\", {\n widgetSessionId: widgetSessionIdRef.current,\n widgetAttemptId: widgetAttemptIdRef.current,\n opportunityProtocol: opp?.protocol ?? null,\n lastStep: swapStepRef.current,\n reachedTerminal: reachedTerminalRef.current,\n hadSimulationSuccess: hadSimulationSuccessRef.current,\n hadPreviewViewed: hadPreviewViewedRef.current,\n timeInWidgetMs: Date.now() - widgetOpenedTsRef.current,\n });\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const toTokenFromOpportunity = (\n opp: NexusWidgetDepositOpportunityMetadata\n ): SwapTokenOption => {\n const citreaToken = findCitreaReceiveToken({\n address: opp.tokenAddress,\n chainId: opp.chainId,\n symbol: opp.tokenSymbol,\n });\n const chainTokens = supportedChainsAndTokens?.find(\n (chain) => chain.id === opp.chainId\n )?.tokens;\n const matchedToken = chainTokens?.find(\n (token) =>\n token.contractAddress.toLowerCase() ===\n opp.tokenAddress.toLowerCase() || token.symbol === opp.tokenSymbol\n );\n const tokenSymbol =\n citreaToken?.symbol ?? matchedToken?.symbol ?? opp.tokenSymbol;\n const tokenMeta =\n TOKEN_METADATA[tokenSymbol as keyof typeof TOKEN_METADATA];\n\n return {\n chainId: opp.chainId,\n contractAddress: citreaToken?.contractAddress ?? opp.tokenAddress,\n symbol: tokenSymbol,\n name: matchedToken?.name || citreaToken?.name || tokenSymbol,\n balance: \"0\",\n balanceInFiat: \"$0.00\",\n decimals:\n matchedToken?.decimals ??\n citreaToken?.decimals ??\n opp.tokenDecimals ??\n tokenMeta?.decimals ??\n 18,\n logo:\n opp.tokenLogo ||\n matchedToken?.logo ||\n citreaToken?.logo ||\n tokenMeta?.logo,\n chainName: getShortChainName(\n opp.chainId,\n CHAIN_METADATA[opp.chainId]?.name ?? citreaToken?.chainName\n ),\n chainLogo: CHAIN_METADATA[opp.chainId]?.logo ?? citreaToken?.chainLogo,\n };\n };\n\n const getDestinationBalanceFromSwapBalances = (token?: SwapTokenOption) => {\n if (!token?.chainId || !token.contractAddress) return null;\n\n const targetAddress = token.contractAddress.toLowerCase();\n const targetSymbol = token.symbol.toUpperCase();\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== token.chainId) continue;\n\n const breakdownAddress = breakdown.contractAddress?.toLowerCase();\n const addressMatches =\n (breakdownAddress && breakdownAddress === targetAddress) ||\n (isNativeTokenAddress(breakdownAddress) &&\n isNativeTokenAddress(targetAddress));\n const symbolMatches =\n (breakdown.symbol ?? asset.symbol ?? \"\").toUpperCase() ===\n targetSymbol;\n\n if (!addressMatches && !symbolMatches) continue;\n\n const balance = parseFiatNumber(breakdown.balance);\n if (!balance) return null;\n\n return `${balance.toDecimalPlaces(6).toFixed()} ${token.symbol}`;\n }\n }\n\n return null;\n };\n\n const resolvePrefillToken = useCallback(\n (pair?: RuntimeDestinationPair) => {\n if (!pair?.token || !pair.chain) return undefined;\n\n const normalizeAddress = (address?: string) => {\n if (!address) return \"\";\n return isNativeTokenAddress(address)\n ? zeroAddress\n : address.toLowerCase();\n };\n const targetAddress = normalizeAddress(pair.token);\n\n const balanceToken = deriveTokenOptions(\n swapBalance ?? [],\n swapSupportedChainsAndTokens\n ).find(\n (token) =>\n token.chainId === pair.chain &&\n normalizeAddress(token.contractAddress) === targetAddress\n );\n if (balanceToken) return balanceToken;\n\n const chain = supportedChainsAndTokens?.find(\n (item) => item.id === pair.chain\n );\n const matchedToken = chain?.tokens?.find(\n (token) => normalizeAddress(token.contractAddress) === targetAddress\n );\n const citreaToken = findCitreaReceiveToken({\n address: pair.token,\n chainId: pair.chain,\n });\n const tokenAddressSymbol = Object.entries(\n TOKEN_CONTRACT_ADDRESSES as Record>\n ).find(\n ([, addresses]) =>\n normalizeAddress(addresses[pair.chain]) === targetAddress\n )?.[0];\n const chainMeta = CHAIN_METADATA[pair.chain];\n const isNativePrefill = isNativeTokenAddress(pair.token);\n const tokenSymbol =\n matchedToken?.symbol ??\n citreaToken?.symbol ??\n pair.symbol ??\n tokenAddressSymbol ??\n (isNativePrefill ? chainMeta?.nativeCurrency?.symbol : undefined) ??\n \"Token\";\n const tokenMeta =\n TOKEN_METADATA[tokenSymbol as keyof typeof TOKEN_METADATA];\n\n if (\n !chain &&\n !matchedToken &&\n !citreaToken &&\n !tokenAddressSymbol &&\n !pair.symbol &&\n !isNativePrefill\n ) {\n return undefined;\n }\n\n return {\n chainId: pair.chain,\n contractAddress: citreaToken?.contractAddress ?? pair.token,\n symbol: tokenSymbol,\n name: matchedToken?.name || citreaToken?.name || tokenSymbol,\n balance: `0 ${tokenSymbol}`,\n balanceInFiat: \"$0.00\",\n decimals:\n matchedToken?.decimals ??\n citreaToken?.decimals ??\n pair.decimals ??\n tokenMeta?.decimals ??\n (isNativePrefill ? chainMeta?.nativeCurrency?.decimals : undefined) ??\n 18,\n logo:\n matchedToken?.logo ||\n citreaToken?.logo ||\n pair.logo ||\n tokenMeta?.logo,\n chainName: getShortChainName(\n pair.chain,\n chain?.name ?? chainMeta?.name ?? citreaToken?.chainName\n ),\n chainLogo: chainMeta?.logo ?? chain?.logo ?? citreaToken?.chainLogo,\n } satisfies SwapTokenOption;\n },\n [supportedChainsAndTokens, swapBalance]\n );\n\n useEffect(() => {\n if (activeMode !== \"swap\") return;\n\n const sourcePrefill = config.prefill?.source;\n const destinationPrefill = config.prefill?.destination;\n if (!sourcePrefill && !destinationPrefill) return;\n\n const prefillKey = [\n sourcePrefill\n ? `source:${sourcePrefill.chain}:${sourcePrefill.token.toLowerCase()}`\n : \"\",\n destinationPrefill\n ? `destination:${destinationPrefill.chain}:${destinationPrefill.token.toLowerCase()}`\n : \"\",\n config.prefill?.amount ? `amount:${config.prefill.amount}` : \"\",\n ].join(\"|\");\n\n if (appliedTokenPrefillRef.current === prefillKey) return;\n\n const sourceToken = resolvePrefillToken(sourcePrefill);\n const destinationToken = resolvePrefillToken(destinationPrefill);\n\n if (sourcePrefill && !sourceToken) return;\n if (destinationPrefill && !destinationToken) return;\n\n if (sourceToken) {\n setFromTokens((current) => {\n const nextSourceToken = {\n ...sourceToken,\n userAmount: config.prefill?.amount ?? \"\",\n };\n const currentSourceToken = current[0];\n if (\n current.length === 1 &&\n isSameTokenSelection(currentSourceToken, nextSourceToken) &&\n currentSourceToken.userAmount === nextSourceToken.userAmount\n ) {\n return current;\n }\n return [nextSourceToken];\n });\n setSourceSelectionTouched(true);\n }\n if (destinationToken) {\n setToToken((current) =>\n isSameTokenSelection(current, destinationToken)\n ? current\n : destinationToken\n );\n }\n setSwapType(\"exactIn\");\n appliedTokenPrefillRef.current = prefillKey;\n }, [\n activeMode,\n config.prefill?.amount,\n config.prefill?.destination?.chain,\n config.prefill?.destination?.decimals,\n config.prefill?.destination?.logo,\n config.prefill?.destination?.symbol,\n config.prefill?.destination?.token,\n config.prefill?.source?.chain,\n config.prefill?.source?.decimals,\n config.prefill?.source?.logo,\n config.prefill?.source?.symbol,\n config.prefill?.source?.token,\n resolvePrefillToken,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"send\") return;\n\n const sendPrefill =\n config.prefill?.token && config.prefill?.chain\n ? {\n token: config.prefill.token,\n chain: config.prefill.chain,\n }\n : config.prefill?.destination;\n if (!sendPrefill) return;\n\n const prefillKey = `send:${sendPrefill.chain}:${sendPrefill.token.toLowerCase()}`;\n if (appliedTokenPrefillRef.current === prefillKey) return;\n\n const token = resolvePrefillToken(sendPrefill);\n if (!token) return;\n\n setToToken(token);\n setSwapType(\"exactOut\");\n appliedTokenPrefillRef.current = prefillKey;\n }, [\n activeMode,\n config.prefill?.chain,\n config.prefill?.destination?.chain,\n config.prefill?.destination?.decimals,\n config.prefill?.destination?.logo,\n config.prefill?.destination?.symbol,\n config.prefill?.destination?.token,\n config.prefill?.token,\n resolvePrefillToken,\n ]);\n\n useEffect(() => {\n if (config.prefill?.amount) setAmount(config.prefill.amount);\n if (config.prefill?.recipient)\n setRecipientAddress(config.prefill.recipient);\n }, [config.prefill?.amount, config.prefill?.recipient]);\n\n useEffect(() => {\n setDestinationBalance(null);\n\n const balanceToken =\n toToken ??\n (activeMode === \"deposit\" && selectedOpportunity\n ? toTokenFromOpportunity(selectedOpportunity)\n : undefined);\n\n if (!balanceToken?.chainId || !ownerAddress) return;\n\n const swapBalanceValue =\n getDestinationBalanceFromSwapBalances(balanceToken);\n if (swapBalanceValue) {\n setDestinationBalance(swapBalanceValue);\n }\n }, [\n activeMode,\n ownerAddress,\n selectedOpportunity?.chainId,\n selectedOpportunity?.tokenAddress,\n selectedOpportunity?.tokenLogo,\n selectedOpportunity?.tokenSymbol,\n swapBalance,\n toToken?.chainId,\n toToken?.chainName,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || !configuredDeposit) return;\n setSelectedOpportunity((current) =>\n isSameDepositConfig(current, configuredDeposit)\n ? current\n : configuredDeposit\n );\n setSwapType(\"exactOut\");\n setToToken((current) => {\n const next = {\n ...toTokenFromOpportunity(configuredDeposit),\n balance: current?.balance ?? \"0\",\n balanceInFiat: current?.balanceInFiat ?? \"$0.00\",\n };\n if (\n current &&\n current.chainId === next.chainId &&\n current.contractAddress.toLowerCase() ===\n next.contractAddress.toLowerCase() &&\n current.symbol === next.symbol &&\n current.decimals === next.decimals &&\n current.logo === next.logo &&\n current.chainLogo === next.chainLogo &&\n current.chainName === next.chainName &&\n current.balance === next.balance &&\n current.balanceInFiat === next.balanceInFiat\n ) {\n return current;\n }\n return next;\n });\n }, [\n activeMode,\n configuredDepositIdentity,\n configuredDeposit?.chainId,\n configuredDeposit?.tokenAddress,\n configuredDeposit?.tokenLogo,\n configuredDeposit?.tokenSymbol,\n supportedChainsAndTokens,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || !selectedOpportunity) return;\n setToToken((current) => {\n const next = {\n ...toTokenFromOpportunity(selectedOpportunity),\n balance: current?.balance ?? \"0\",\n balanceInFiat: current?.balanceInFiat ?? \"$0.00\",\n };\n if (\n current &&\n current.chainId === next.chainId &&\n current.contractAddress.toLowerCase() ===\n next.contractAddress.toLowerCase() &&\n current.symbol === next.symbol &&\n current.decimals === next.decimals &&\n current.logo === next.logo &&\n current.chainLogo === next.chainLogo &&\n current.chainName === next.chainName &&\n current.balance === next.balance &&\n current.balanceInFiat === next.balanceInFiat\n ) {\n return current;\n }\n return next;\n });\n }, [activeMode, selectedOpportunity, supportedChainsAndTokens]);\n\n useEffect(() => {\n if (activeMode !== \"send\") return;\n setSwapType(\"exactOut\");\n }, [activeMode]);\n\n useEffect(() => {\n if (activeMode === \"swap\" && swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n }, [activeMode, swapType]);\n\n useEffect(() => {\n if (!toToken?.symbol) return;\n if (getFiatValue(1, toToken.symbol) > 0) return;\n\n let cancelled = false;\n void resolveTokenUsdRate(toToken.symbol).catch((error) => {\n if (!cancelled) {\n console.warn(\"Unable to resolve Nexus Widget token USD rate\", {\n symbol: toToken.symbol,\n error,\n });\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [activeMode, getFiatValue, resolveTokenUsdRate, toToken?.symbol]);\n\n // Balance helpers\n const activeBalanceArray = swapBalance;\n const selectedToken = config.prefill?.token ?? \"USDC\";\n const currentAsset =\n activeBalanceArray?.find((a) => a.symbol === selectedToken) ||\n activeBalanceArray?.[0];\n const maxBalance = currentAsset?.balance\n ? String(currentAsset.balance)\n : undefined;\n const usdValue = getFiatValue(\n Number(amount) || 0,\n currentAsset?.symbol || \"USDC\"\n );\n const getDepositTokenUsdRate = () => {\n if (selectedOpportunity?.tokenSymbol) {\n const fiat = getFiatValue(1, selectedOpportunity.tokenSymbol);\n if (Number.isFinite(fiat) && fiat > 0) {\n return new Decimal(fiat);\n }\n }\n\n if (toToken) {\n const tokenRate = getTokenUsdRate(toToken);\n if (tokenRate.gt(0)) return tokenRate;\n }\n\n return getCachedDestinationUsdRate(toToken) ?? new Decimal(0);\n };\n const getDepositTokenAmountForQuote = () => {\n const parsedAmount = parseFiatNumber(amount) ?? new Decimal(0);\n if (parsedAmount.lte(0)) return undefined;\n if (depositAmountMode === \"token\") return parsedAmount;\n\n const rate = getDepositTokenUsdRate();\n if (rate.lte(0)) return undefined;\n return parsedAmount.div(rate);\n };\n const depositTokenAmountForQuote = getDepositTokenAmountForQuote();\n const depositQuoteAmountKey = depositTokenAmountForQuote?.toFixed() ?? \"\";\n const depositUsdDecimal =\n depositAmountMode === \"usd\"\n ? (parseFiatNumber(amount) ?? new Decimal(0))\n : depositTokenAmountForQuote\n ? depositTokenAmountForQuote.mul(getDepositTokenUsdRate())\n : new Decimal(0);\n const depositUsdDisplay = depositUsdDecimal.toDecimalPlaces(2).toFixed();\n const depositTokenDisplay =\n depositTokenAmountForQuote\n ?.toDecimalPlaces(\n getCappedTokenDisplayDecimals(toToken?.decimals),\n Decimal.ROUND_DOWN\n )\n .toFixed() ?? \"0\";\n const isExactOutMode = activeMode === \"deposit\" || activeMode === \"send\";\n const getActiveTotalBalanceUsd = () =>\n isExactOutMode\n ? getExactOutTotalSourceBalanceUsd()\n : getSwapBalanceTotalUsd();\n const getTotalBalancePercentUsdAmount = (pct: number) => {\n const ratio =\n isExactOutMode && pct === 100\n ? receiveMaxSafetyMultiplier\n : new Decimal(pct).div(100);\n return getActiveTotalBalanceUsd().mul(ratio);\n };\n const getExactOutDestinationUsdRate = (token: SwapTokenOption) => {\n if (activeMode === \"deposit\") {\n return getDepositTokenUsdRate();\n }\n return getTokenUsdRate(token);\n };\n const getExactOutPercentAmountFromBalance = (\n token: SwapTokenOption,\n pct: number,\n preferUsd: boolean\n ) => {\n const usdAmount = getTotalBalancePercentUsdAmount(pct);\n if (usdAmount.lte(0)) return undefined;\n\n if (preferUsd) {\n return {\n amount: usdAmount.toDecimalPlaces(2, Decimal.ROUND_DOWN).toFixed(),\n mode: \"usd\" as const,\n };\n }\n\n const rate = getExactOutDestinationUsdRate(token);\n if (rate.lte(0)) return undefined;\n\n return {\n amount: usdAmount\n .div(rate)\n .toDecimalPlaces(\n getCappedTokenDisplayDecimals(token.decimals),\n Decimal.ROUND_DOWN\n )\n .toFixed(),\n mode: \"token\" as const,\n };\n };\n const depositSourceTargetUsdKey =\n activeMode === \"deposit\"\n ? (getDepositSourceTargetUsd()?.toFixed() ?? \"\")\n : \"\";\n const normalizedQuoteAmountKey = parseFiatNumber(amount)?.toFixed() ?? \"\";\n const quoteRecipientKey =\n activeMode === \"swap\"\n ? effectiveRecipientAddress\n : activeMode === \"send\"\n ? recipientAddress\n : \"\";\n const activeQuoteInputKey = [\n activeMode,\n swapType,\n normalizedQuoteAmountKey,\n toTokenQuoteKey,\n quoteRecipientKey.toLowerCase(),\n activeMode === \"swap\" ? fromTokensQuoteKey : \"\",\n activeMode === \"deposit\"\n ? [\n depositAmountMode,\n depositQuoteAmountKey,\n selectedOpportunityIdentity,\n depositSourceTargetUsdKey,\n depositSourceFilter,\n sourceSelectionTouched ? \"manual\" : \"auto\",\n sourceSelectionRevision,\n exactOutQuoteSourceMode,\n ].join(\":\")\n : \"\",\n activeMode === \"send\"\n ? [\n sourceSelectionTouched ? \"manual\" : \"auto\",\n sourceSelectionRevision,\n exactOutQuoteSourceMode,\n ].join(\":\")\n : \"\",\n ].join(\"|\");\n\n const exactOutAutoSourceInputKey = [\n activeMode,\n normalizedQuoteAmountKey,\n toTokenQuoteKey,\n quoteRecipientKey.toLowerCase(),\n activeMode === \"deposit\"\n ? [\n depositAmountMode,\n depositQuoteAmountKey,\n selectedOpportunityIdentity,\n depositSourceTargetUsdKey,\n ].join(\":\")\n : \"\",\n ].join(\"|\");\n\n activeQuoteInputKeyRef.current = activeQuoteInputKey;\n\n useEffect(() => {\n lastAutoIntentSourceTokensRef.current = [];\n }, [exactOutAutoSourceInputKey]);\n\n useEffect(() => {\n setTxError(null);\n }, [activeQuoteInputKey]);\n const hasCurrentQuoteIntent = Boolean(\n intentData &&\n swapIntentRef.current &&\n swapIntentRef.current.runId === swapRunIdRef.current &&\n swapIntentRef.current.quoteInputKey === activeQuoteInputKey\n );\n\n useEffect(() => {\n if (swapStep !== \"idle\") return;\n if (!hasCurrentQuoteIntent) return;\n if (sourceSelectionTouched) return;\n if (\n activeMode !== \"send\" &&\n !(activeMode === \"deposit\" && swapType === \"exactOut\")\n ) {\n return;\n }\n\n const nextTokens = lastIntentSourceTokensRef.current;\n if (nextTokens.length === 0) return;\n\n setFromTokens((current) => {\n if (\n getSourceTokenListSyncKey(current) ===\n getSourceTokenListSyncKey(nextTokens)\n ) {\n return current;\n }\n\n syncingIntentSourcesRef.current = true;\n return nextTokens;\n });\n }, [\n activeMode,\n hasCurrentQuoteIntent,\n sourceSelectionTouched,\n swapStep,\n swapType,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (!nexusSDK?.analytics) return;\n const parsed = parseFiatNumber(amount);\n if (!parsed || parsed.lte(0)) return;\n if (amount === amountEnteredLastValueRef.current) return;\n const timeout = setTimeout(() => {\n amountEnteredLastValueRef.current = amount;\n trackDeposit(\"deposit_amount_entered\", {\n amountToken: depositTokenDisplay,\n amountUsd: Number(depositUsdDisplay) || 0,\n inputMethod: lastInputMethodRef.current,\n });\n lastInputMethodRef.current = \"typed\";\n }, 500);\n return () => clearTimeout(timeout);\n }, [\n amount,\n activeMode,\n nexusSDK,\n depositTokenDisplay,\n depositUsdDisplay,\n trackDeposit,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (intentData) hadSimulationSuccessRef.current = true;\n }, [intentData, activeMode]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (sourceSelectionTouched) return;\n previousAutoSourceCountRef.current = (intentData?.sources ?? []).length;\n }, [intentData, activeMode, sourceSelectionTouched]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n const prev = prevSourceTouchedRef.current;\n const curr = sourceSelectionTouched;\n if (prev === curr) return;\n prevSourceTouchedRef.current = curr;\n if (!prev && curr) {\n trackDeposit(\"deposit_source_selection_changed\", {\n sourceCount: fromTokens.length,\n sourceChainIds: fromTokens.map((t) => t.chainId).filter(Boolean),\n sourceTokenSymbols: fromTokens.map((t) => t.symbol).filter(Boolean),\n previousSourceCount: previousAutoSourceCountRef.current,\n });\n } else if (prev && !curr) {\n trackDeposit(\"deposit_source_selection_reverted_to_auto\", {\n previousSourceCount: fromTokens.length,\n });\n }\n }, [sourceSelectionTouched, activeMode, fromTokens, trackDeposit]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (swapStep !== \"preview-intent\") return;\n if (intentLoading) return;\n if (!intentData) return;\n if (hadPreviewViewedRef.current) return;\n hadPreviewViewedRef.current = true;\n previewViewedTsRef.current = Date.now();\n trackDeposit(\"deposit_preview_viewed\", {\n totalFeeUsd: Number(intentFeeUsd) || 0,\n toAmountUsd: Number(depositUsdDisplay) || 0,\n sourceCount: (intentData?.sources ?? []).length,\n });\n }, [\n swapStep,\n intentLoading,\n intentData,\n activeMode,\n intentFeeUsd,\n depositUsdDisplay,\n trackDeposit,\n ]);\n const requiredDestinationTokenAmount =\n activeMode === \"deposit\"\n ? depositTokenAmountForQuote\n : activeMode === \"send\"\n ? parseFiatNumber(amount)\n : undefined;\n const canRefreshExactOutQuote = () =>\n activeMode === \"deposit\"\n ? Boolean(\n hasPositiveDecimalInput(amount) &&\n toToken &&\n selectedOpportunity &&\n depositTokenAmountForQuote &&\n depositTokenAmountForQuote.gt(0)\n )\n : activeMode === \"send\"\n ? Boolean(hasPositiveDecimalInput(amount) && toToken)\n : false;\n const invalidateExactOutQuoteForRefresh = (options?: {\n sourceTokens?: SwapTokenOption[];\n }) => {\n immediateQuoteAfterSourceEditRef.current = true;\n const receiveIssue = buildReceiveAmountIssue({\n sourceTokens: options?.sourceTokens,\n });\n applyReceiveAmountIssue(receiveIssue);\n const configuredIssue = buildConfiguredAmountIssue();\n if (configuredIssue) {\n clearPreviewForBlockingAmountIssue();\n }\n const sourceBalanceIssue = buildExactOutSourceBalanceIssue(\n options?.sourceTokens\n );\n if (sourceBalanceIssue) {\n clearPreviewForBlockingAmountIssue();\n setSwapQuoteIssue(sourceBalanceIssue);\n }\n const shouldLoadQuote = Boolean(\n !receiveIssue &&\n !configuredIssue &&\n !sourceBalanceIssue &&\n nexusSDK &&\n canRefreshExactOutQuote()\n );\n if (!receiveIssue && !configuredIssue && !sourceBalanceIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n setTxError(null);\n setSwapQuoteIssue(null);\n }\n return shouldLoadQuote;\n };\n\n useEffect(() => {\n if (\n activeMode !== \"swap\" ||\n swapStep !== \"idle\" ||\n swapType !== \"exactIn\"\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n\n const sources = getPredictiveExactInSourceTokens();\n const key = getPredictiveQuoteCacheKey();\n if (!toToken || sources.length === 0 || !key) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n\n const runId = ++predictiveQuoteRunRef.current;\n let cancelled = false;\n\n void (async () => {\n const baseline = predictiveQuoteCacheRef.current[key];\n const cachedDestinationRate = parseFiatNumber(\n baseline?.destinationUsdRate\n );\n const destinationRate =\n cachedDestinationRate && cachedDestinationRate.gt(0)\n ? cachedDestinationRate\n : await resolveUsdRateForToken(toToken);\n\n if (cancelled || runId !== predictiveQuoteRunRef.current) return;\n if (destinationRate.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n\n let sourceUsd = new Decimal(0);\n for (const source of sources) {\n const sourceAmount =\n parseFiatNumber(source.userAmount) ?? new Decimal(0);\n if (sourceAmount.lte(0)) continue;\n\n if (source.userAmountMode === \"usd\") {\n sourceUsd = sourceUsd.plus(sourceAmount);\n continue;\n }\n\n const sourceRate = await resolveUsdRateForToken(source);\n if (cancelled || runId !== predictiveQuoteRunRef.current) return;\n if (sourceRate.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n sourceUsd = sourceUsd.plus(sourceAmount.mul(sourceRate));\n }\n\n if (sourceUsd.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current\n );\n return;\n }\n\n const cachedAmountPerSourceUsd = parseFiatNumber(\n baseline?.exactInDestinationAmountPerSourceUsd\n );\n const predictedDestinationAmount =\n cachedAmountPerSourceUsd && cachedAmountPerSourceUsd.gt(0)\n ? sourceUsd.mul(cachedAmountPerSourceUsd)\n : sourceUsd\n .mul(BASIS_POINTS - PREDICTIVE_EXACT_IN_DISCOUNT_BPS)\n .div(BASIS_POINTS)\n .div(destinationRate);\n const predictedDestinationUsd =\n cachedAmountPerSourceUsd && cachedAmountPerSourceUsd.gt(0)\n ? predictedDestinationAmount.mul(destinationRate)\n : sourceUsd\n .mul(BASIS_POINTS - PREDICTIVE_EXACT_IN_DISCOUNT_BPS)\n .div(BASIS_POINTS);\n\n if (\n cancelled ||\n runId !== predictiveQuoteRunRef.current ||\n predictedDestinationAmount.lte(0)\n ) {\n return;\n }\n\n setPredictiveQuote({\n key,\n mode: \"exactIn\",\n toAmount: getPredictiveDisplayAmount(\n predictedDestinationAmount,\n toToken\n ),\n toUsd: predictedDestinationUsd.toDecimalPlaces(6).toFixed(),\n });\n })();\n\n return () => {\n cancelled = true;\n };\n }, [\n activeMode,\n amount,\n fromTokens,\n swapStep,\n swapType,\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n useEffect(() => {\n if (\n (activeMode !== \"deposit\" && activeMode !== \"send\") ||\n swapStep !== \"idle\" ||\n swapType !== \"exactOut\" ||\n !nexusSDK\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current\n );\n return;\n }\n\n const parsedAmount = parseFiatNumber(amount);\n const key = getPredictiveQuoteCacheKey();\n if (\n !toToken ||\n !parsedAmount ||\n parsedAmount.lte(0) ||\n !key ||\n (activeMode === \"deposit\" && !selectedOpportunity)\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current\n );\n return;\n }\n\n const runId = ++predictiveQuoteRunRef.current;\n let cancelled = false;\n\n void (async () => {\n const baseline = predictiveQuoteCacheRef.current[key];\n const cachedDestinationRate = parseFiatNumber(\n baseline?.destinationUsdRate\n );\n const destinationRate =\n cachedDestinationRate && cachedDestinationRate.gt(0)\n ? cachedDestinationRate\n : await resolveUsdRateForToken(toToken);\n\n if (cancelled || runId !== predictiveQuoteRunRef.current) return;\n if (destinationRate.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current\n );\n return;\n }\n\n const destinationAmount =\n activeMode === \"deposit\" && depositAmountMode === \"usd\"\n ? parsedAmount.div(destinationRate)\n : parsedAmount;\n const destinationUsd =\n activeMode === \"deposit\" && depositAmountMode === \"usd\"\n ? parsedAmount\n : destinationAmount.mul(destinationRate);\n const destinationCoverage = getExactOutDestinationBalanceCoverage({\n requestedAmount: destinationAmount,\n requestedUsd: destinationUsd,\n token: toToken,\n });\n const destinationUsdNeedingSources = Decimal.max(\n destinationUsd.minus(destinationCoverage?.usd ?? new Decimal(0)),\n new Decimal(0)\n );\n const cachedSourceUsdRatio = parseFiatNumber(\n baseline?.exactOutSourceUsdPerDestinationUsd\n );\n const requiredSourceUsd = getPredictiveExactOutSourceTargetUsd(\n destinationUsdNeedingSources,\n cachedSourceUsdRatio\n );\n const sources = requiredSourceUsd.gt(0)\n ? await buildPredictiveExactOutSources(requiredSourceUsd)\n : [];\n\n if (\n cancelled ||\n runId !== predictiveQuoteRunRef.current ||\n (requiredSourceUsd.gt(0) && sources.length === 0)\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current\n );\n return;\n }\n\n setPredictiveQuote({\n key,\n mode: \"exactOut\",\n sources,\n toAmount: getPredictiveDisplayAmount(destinationAmount, toToken),\n toUsd: destinationUsd.toDecimalPlaces(6).toFixed(),\n });\n })();\n\n return () => {\n cancelled = true;\n };\n }, [\n activeMode,\n amount,\n depositAmountMode,\n destinationBalance,\n fromTokensQuoteKey,\n nexusSDK,\n selectedOpportunityIdentity,\n sourceSelectionRevision,\n swapBalance,\n swapStep,\n swapType,\n toToken?.balance,\n toToken?.balanceInFiat,\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n const resolvedDepositSourceTokens = useMemo(() => {\n if (activeMode !== \"deposit\" || !swapBalance) return [];\n const selection = getResolvedDepositSourceSelection();\n return getDepositSourceTokensForIds(selection.selectedSourceIds);\n }, [\n activeMode,\n depositSourceFilter,\n depositQuoteAmountKey,\n depositSourceTargetUsdKey,\n depositUsdDecimal.toFixed(),\n fromTokensQuoteKey,\n selectedOpportunity?.chainId,\n selectedOpportunity?.tokenAddress,\n selectedOpportunity?.tokenSymbol,\n sourceSelectionRevision,\n sourceSelectionTouched,\n swapBalance,\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.symbol,\n ]);\n const lockedDestinationSourceTokens = useMemo(() => {\n if (\n (activeMode !== \"deposit\" && activeMode !== \"send\") ||\n !toToken?.chainId ||\n !requiredDestinationTokenAmount ||\n requiredDestinationTokenAmount.lte(0)\n ) {\n return [];\n }\n\n const heldDestinationToken = getHeldDestinationTokenOption();\n return heldDestinationToken ? [heldDestinationToken] : [];\n }, [\n activeMode,\n requiredDestinationTokenAmount?.toFixed(),\n swapBalance,\n toToken?.chainId,\n toToken?.chainLogo,\n toToken?.chainName,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.logo,\n toToken?.symbol,\n ]);\n\n const setSourcePickerDraftSelection = useCallback(\n (tokens: SwapTokenOption[]) => {\n const nextTokens = tokens.map((token) => ({\n ...token,\n userAmount: \"\",\n }));\n sourcePickerDraftTokensRef.current = nextTokens;\n setSourcePickerDraftTokens(nextTokens);\n },\n []\n );\n\n const getAutoExactOutSourceTokensForPicker = useCallback(() => {\n const autoIntentTokens = lastAutoIntentSourceTokensRef.current;\n if (autoIntentTokens.length > 0) {\n return autoIntentTokens;\n }\n\n return sourceSelectionTouched ? [] : fromTokens;\n }, [fromTokens, sourceSelectionTouched]);\n\n const resetSourcePickerDraft = useCallback(() => {\n sourcePickerDraftTokensRef.current = null;\n setSourcePickerDraftTokens(null);\n }, []);\n\n const beginSourcePickerEdit = useCallback(() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n resetSourcePickerDraft();\n return;\n }\n\n sourcePickerDraftDepositFilterRef.current = depositSourceFilter;\n sourcePickerDraftTouchedRef.current = sourceSelectionTouched;\n sourcePickerDraftModeRef.current = exactOutQuoteSourceModeRef.current;\n setSourcePickerDraftSelection(fromTokens);\n }, [\n activeMode,\n depositSourceFilter,\n fromTokens,\n resetSourcePickerDraft,\n setSourcePickerDraftSelection,\n sourceSelectionTouched,\n ]);\n\n const handleSourcePickerCancel = useCallback(() => {\n resetSourcePickerDraft();\n closeDrawerToIdle();\n }, [closeDrawerToIdle, resetSourcePickerDraft]);\n\n const handleSourcePickerDraftSelectionChange = useCallback(\n (tokens: SwapTokenOption[]) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n\n setSourcePickerDraftSelection(tokens);\n sourcePickerDraftTouchedRef.current = true;\n sourcePickerDraftModeRef.current = \"selected\";\n if (activeMode === \"deposit\") {\n sourcePickerDraftDepositFilterRef.current = \"custom\";\n }\n },\n [activeMode, setSourcePickerDraftSelection]\n );\n\n const handleSourcePickerFilterTabSelect = useCallback(\n (tab: Exclude) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n\n const nextFilter: DepositSourceFilter =\n tab === \"stables\" ? \"stablecoins\" : tab;\n\n if (tab === \"all\") {\n sourcePickerDraftDepositFilterRef.current = nextFilter;\n sourcePickerDraftTouchedRef.current = false;\n sourcePickerDraftModeRef.current = \"all\";\n setSourcePickerDraftSelection(getAutoExactOutSourceTokensForPicker());\n return;\n }\n\n if (activeMode !== \"deposit\") return;\n\n const selection = getResolvedDepositSourceSelection({\n filter: nextFilter,\n isManualSelection: false,\n });\n const sourcePoolTokens = getDepositSourceTokensForIds(\n selection.sourcePoolIds\n );\n\n sourcePickerDraftDepositFilterRef.current = nextFilter;\n sourcePickerDraftTouchedRef.current = false;\n sourcePickerDraftModeRef.current = \"all\";\n setSourcePickerDraftSelection(sourcePoolTokens);\n },\n [\n activeMode,\n getAutoExactOutSourceTokensForPicker,\n getDepositSourceTokensForIds,\n getResolvedDepositSourceSelection,\n setSourcePickerDraftSelection,\n ]\n );\n\n const commitSourcePickerDraft = useCallback(\n (tokens?: SwapTokenOption[]) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n resetSourcePickerDraft();\n closeDrawerToIdle();\n return;\n }\n\n const nextTokens =\n tokens ?? sourcePickerDraftTokensRef.current ?? fromTokens;\n const normalizedTokens = nextTokens.map((token) => ({\n ...token,\n userAmount: \"\",\n }));\n\n setSourceSelectionTouched(sourcePickerDraftTouchedRef.current);\n setExactOutQuoteSourceModeValue(sourcePickerDraftModeRef.current);\n if (activeMode === \"deposit\") {\n setDepositSourceFilter(sourcePickerDraftDepositFilterRef.current);\n }\n invalidateExactOutQuoteForRefresh({\n sourceTokens: normalizedTokens,\n });\n setSourceSelectionRevision((current) => current + 1);\n setFromTokens(normalizedTokens);\n resetSourcePickerDraft();\n closeDrawerToIdle();\n },\n [\n activeMode,\n closeDrawerToIdle,\n fromTokens,\n invalidateExactOutQuoteForRefresh,\n resetSourcePickerDraft,\n setExactOutQuoteSourceModeValue,\n ]\n );\n\n const sourcePickerSelectedTokens =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n swapStep === \"choose-swap-asset\"\n ? (sourcePickerDraftTokens ?? fromTokens)\n : fromTokens;\n\n useEffect(() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n if (lockedDestinationSourceTokens.length === 0) return;\n if (activeMode === \"deposit\" && !sourceSelectionTouched) return;\n\n setFromTokens((current) => {\n const missing = lockedDestinationSourceTokens.filter(\n (locked) =>\n !current.some(\n (token) =>\n getTokenSelectionKey(token) === getTokenSelectionKey(locked)\n )\n );\n if (missing.length === 0) return current;\n return [\n ...current,\n ...missing.map((token) => ({ ...token, userAmount: \"\" })),\n ];\n });\n }, [activeMode, lockedDestinationSourceTokens, sourceSelectionTouched]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (sourceSelectionTouched) return;\n if (\n !toToken ||\n !depositTokenAmountForQuote ||\n depositTokenAmountForQuote.lte(0)\n ) {\n return;\n }\n if (resolvedDepositSourceTokens.length === 0) {\n return;\n }\n\n setFromTokens((current) => {\n const canInitialize = current.length === 0;\n if (!canInitialize) return current;\n\n const next: SwapTokenOption[] = [];\n const seen = new Set();\n for (const token of resolvedDepositSourceTokens) {\n const key = getTokenSelectionKey(token);\n if (!key || seen.has(key)) continue;\n seen.add(key);\n next.push({ ...token, userAmount: \"\" });\n }\n\n const currentKeys = current.map(getTokenSelectionKey).sort().join(\"|\");\n const nextKeys = next.map(getTokenSelectionKey).sort().join(\"|\");\n if (currentKeys === nextKeys) return current;\n return next;\n });\n }, [\n activeMode,\n depositQuoteAmountKey,\n resolvedDepositSourceTokens,\n sourceSelectionTouched,\n toTokenQuoteKey,\n ]);\n\n // ---------------------------------------------------------------------------\n // Handlers\n // ---------------------------------------------------------------------------\n\n const applyConfiguredPrefillsAfterReset = () => {\n const prefillAmount = config.prefill?.amount ?? \"\";\n const prefillRecipient = config.prefill?.recipient ?? \"\";\n\n appliedTokenPrefillRef.current = null;\n syncingIntentSourcesRef.current = false;\n maxPercentRunRef.current += 1;\n setAmount(prefillAmount);\n setRecipientAddress(prefillRecipient);\n setTxError(null);\n setSwapQuoteIssue(null);\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n setPredictiveQuote(null);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setMaxCalculationPercent(null);\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setExactOutQuoteSourceModeValue(\"all\");\n setSourceSelectionRevision((current) => current + 1);\n setDepositAmountMode(\"token\");\n\n if (activeMode === \"swap\") {\n const sourcePrefill = config.prefill?.source;\n const destinationPrefill = config.prefill?.destination;\n const sourceToken = resolvePrefillToken(sourcePrefill);\n const destinationToken = resolvePrefillToken(destinationPrefill);\n\n setFromTokens(\n sourceToken\n ? [\n {\n ...sourceToken,\n userAmount: prefillAmount,\n },\n ]\n : []\n );\n setSourceSelectionTouched(Boolean(sourceToken));\n setToToken(destinationToken);\n setSelectedOpportunity(undefined);\n setSwapType(\"exactIn\");\n\n if (\n (!sourcePrefill || sourceToken) &&\n (!destinationPrefill || destinationToken) &&\n (sourcePrefill || destinationPrefill)\n ) {\n appliedTokenPrefillRef.current = [\n sourcePrefill\n ? `source:${sourcePrefill.chain}:${sourcePrefill.token.toLowerCase()}`\n : \"\",\n destinationPrefill\n ? `destination:${destinationPrefill.chain}:${destinationPrefill.token.toLowerCase()}`\n : \"\",\n config.prefill?.amount ? `amount:${config.prefill.amount}` : \"\",\n ].join(\"|\");\n }\n return;\n }\n\n if (activeMode === \"send\") {\n const sendPrefill =\n config.prefill?.token && config.prefill?.chain\n ? {\n token: config.prefill.token,\n chain: config.prefill.chain,\n }\n : config.prefill?.destination;\n const token = resolvePrefillToken(sendPrefill);\n\n setFromTokens([]);\n setSelectedOpportunity(undefined);\n setToToken(token);\n setSwapType(\"exactOut\");\n\n if (sendPrefill && token) {\n appliedTokenPrefillRef.current = `send:${sendPrefill.chain}:${sendPrefill.token.toLowerCase()}`;\n }\n return;\n }\n\n const destinationPrefill = config.prefill?.destination;\n const configuredPrefillDeposit = destinationPrefill\n ? configuredDepositOptions.find(\n (deposit) =>\n deposit.chainId === destinationPrefill.chain &&\n deposit.tokenAddress.toLowerCase() ===\n destinationPrefill.token.toLowerCase()\n )\n : undefined;\n const nextDeposit = configuredPrefillDeposit ?? configuredDeposit;\n\n setFromTokens([]);\n setSelectedOpportunity(nextDeposit);\n setToToken(nextDeposit ? toTokenFromOpportunity(nextDeposit) : undefined);\n setSwapType(\"exactOut\");\n };\n\n const handleReset = () => {\n clearPendingSwapIntent();\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n amountEnteredLastValueRef.current = config.prefill?.amount ?? \"\";\n applyConfiguredPrefillsAfterReset();\n rotateAttempt();\n };\n\n const resetSourceSelectionAfterFailure = () => {\n lastAutoIntentSourceTokensRef.current = [];\n lastIntentSourceTokensRef.current = [];\n resetSourcePickerDraft();\n resetExactOutSourcesToAuto();\n };\n\n const handleFailureBack = () => {\n clearPendingSwapIntent();\n resetSourceSelectionAfterFailure();\n setTxError(null);\n void refreshSelectedSourceBalances();\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n rotateAttempt();\n };\n\n const clearTerminalInputState = () => {\n setAmount(\"\");\n setRecipientAddress(\"\");\n setTxError(null);\n setSwapQuoteIssue(null);\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n setFromTokens((current) => (current.length === 0 ? current : []));\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setDepositAmountMode(\"token\");\n };\n\n const resetInputsAfterSuccessfulExecution = () => {\n clearTerminalInputState();\n if (activeMode === \"deposit\") {\n setSelectedOpportunity(configuredDeposit);\n setToToken(\n configuredDeposit\n ? toTokenFromOpportunity(configuredDeposit)\n : undefined\n );\n } else {\n setToToken(undefined);\n }\n };\n\n const handleTerminalReceiptDone = () => {\n clearPendingSwapIntent();\n clearTerminalInputState();\n void refreshSelectedSourceBalances();\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n amountEnteredLastValueRef.current = \"\";\n rotateAttempt();\n };\n\n const handleModalOpenChange = useCallback(\n (open: boolean) => {\n if (!open && swapStepRef.current === \"progress\") return;\n if (!isControlledOpen) {\n setInternalOpen(open);\n }\n onOpenChange?.(open);\n if (!open) {\n clearPendingSwapIntent();\n onClose?.();\n }\n },\n [clearPendingSwapIntent, isControlledOpen, onClose, onOpenChange]\n );\n\n const handleClose = () => {\n if (!embed) {\n handleModalOpenChange(false);\n return;\n }\n clearPendingSwapIntent();\n onClose?.();\n };\n\n const handleConnectWallet = async () => {\n if (walletActionPending || nexusLoading) return;\n\n const clickHandler = onConnectClick || onConnectWallet;\n if (clickHandler) {\n setWalletActionPending(true);\n setTxError(null);\n try {\n await clickHandler();\n } catch (error: any) {\n setTxError(error?.message || \"Unable to connect wallet.\");\n } finally {\n setWalletActionPending(false);\n }\n return;\n }\n\n if (isWalletConnectPending) return;\n\n setWalletActionPending(true);\n setTxError(null);\n try {\n let activeConnector = connector;\n\n if (walletStatus !== \"connected\") {\n const nextConnector = connectors[0];\n if (!nextConnector) {\n throw new Error(\"No wallet connector available.\");\n }\n await connectAsync({ connector: nextConnector });\n return;\n }\n\n const effectiveProvider = await getEffectiveWalletProvider(\n activeConnector\n );\n\n if (\n !effectiveProvider ||\n typeof effectiveProvider.request !== \"function\"\n ) {\n throw new Error(\"Wallet provider is not ready yet.\");\n }\n\n await handleInit(effectiveProvider as EthereumProvider, ownerAddress);\n } catch (error: any) {\n setTxError(error?.message || \"Unable to connect wallet.\");\n } finally {\n setWalletActionPending(false);\n }\n };\n\n const handleOpenRecipientEditor = () => {\n if (activeMode === \"swap\" && !recipientAddress && defaultRecipientAddress) {\n setRecipientAddress(defaultRecipientAddress);\n }\n setTxError(null);\n openDrawerStep(\"enter-recipient\");\n };\n\n const handleResetRecipientToDefault = () => {\n setRecipientAddress(defaultRecipientAddress);\n setTxError(null);\n };\n\n const handleSaveRecipient = () => {\n const next = recipientAddress.trim();\n if (!next) {\n setTxError(\"Recipient address is required\");\n return;\n }\n if (!next.endsWith(\".eth\") && !isAddress(next)) {\n setTxError(\"Incorrect address\");\n return;\n }\n if (\n activeMode === \"send\" &&\n ownerAddress &&\n isAddress(next) &&\n next.toLowerCase() === ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\n setRecipientAddress(next);\n setTxError(null);\n closeDrawerToIdle();\n };\n\n /** Start swap flow — v2 SDK per-operation onIntent hooks populate preview. */\n const handleEnterPreview = async (options: { background?: boolean } = {}) => {\n const { background = false } = options;\n const isExactOutFlow = activeMode === \"deposit\" || activeMode === \"send\";\n const quoteInputKey = activeQuoteInputKeyRef.current;\n const isCurrentQuoteInput = () =>\n activeQuoteInputKeyRef.current === quoteInputKey;\n\n if (!toToken) {\n return;\n }\n\n if (isExactOutFlow) {\n if (!hasPositiveDecimalInput(amount)) {\n return;\n }\n } else if (!hasReadyExactInSwapInput(fromTokens, toToken)) {\n if (!background) {\n setTxError(null);\n setSwapQuoteIssue(null);\n }\n return;\n }\n\n const receiveIssue = buildReceiveAmountIssue();\n if (receiveIssue) {\n applyReceiveAmountIssue(receiveIssue);\n if (!background && swapStepRef.current !== \"idle\") {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n }\n return;\n }\n\n const configuredIssue = buildConfiguredAmountIssue();\n if (configuredIssue) {\n clearPreviewForBlockingAmountIssue();\n if (!background && swapStepRef.current !== \"idle\") {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n }\n return;\n }\n\n const sourceBalanceIssue = isExactOutFlow\n ? buildExactOutSourceBalanceIssue()\n : null;\n if (sourceBalanceIssue) {\n clearPreviewForBlockingAmountIssue();\n setSwapQuoteIssue(sourceBalanceIssue);\n if (!background && swapStepRef.current !== \"idle\") {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n }\n return;\n }\n\n if (!background && activeMode === \"deposit\") {\n trackDeposit(\"deposit_confirm_clicked\", {\n amountToken: depositTokenDisplay,\n amountUsd: Number(depositUsdDisplay) || 0,\n selectionMode: sourceSelectionTouched ? \"manual\" : \"auto\",\n sourceCount: (intentData?.sources ?? []).length,\n });\n }\n\n setTxError(null);\n setSwapQuoteIssue(null);\n\n if (\n !background &&\n swapIntentRef.current?.runId === swapRunIdRef.current &&\n swapIntentRef.current?.quoteInputKey === quoteInputKey &&\n intentData &&\n (activeMode !== \"send\" || Boolean(recipientAddress)) &&\n ((activeMode !== \"deposit\" && activeMode !== \"send\") ||\n (intentData.sources ?? []).length > 0 ||\n Boolean(intentData.destination))\n ) {\n swapStepRef.current = \"preview-intent\";\n setSwapStep(\"preview-intent\");\n return;\n }\n\n let resolvedRecipientAddress =\n activeMode === \"swap\" ? effectiveRecipientAddress : recipientAddress;\n\n if (!background && activeMode === \"send\" && !resolvedRecipientAddress) {\n setTxError(\"Recipient address is required\");\n return;\n }\n\n if ((!background && activeMode === \"send\") || hasCustomSwapRecipient) {\n if (!resolvedRecipientAddress) {\n setTxError(\"Recipient address is required\");\n return;\n }\n\n if (\n activeMode === \"send\" &&\n ownerAddress &&\n isAddress(resolvedRecipientAddress) &&\n resolvedRecipientAddress.toLowerCase() === ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\n\n if (resolvedRecipientAddress.endsWith(\".eth\")) {\n try {\n const mainnetClient =\n publicClient?.chain?.id === 1\n ? publicClient\n : createPublicClient({\n chain: mainnet,\n transport: http(),\n });\n const ensAddr = await mainnetClient.getEnsAddress({\n name: normalize(resolvedRecipientAddress),\n });\n if (!ensAddr) {\n setTxError(\"Could not resolve ENS name to an address.\");\n return;\n }\n resolvedRecipientAddress = ensAddr;\n } catch (e: any) {\n setTxError(e.message || \"Failed to resolve ENS name.\");\n return;\n }\n } else {\n if (!isAddress(resolvedRecipientAddress)) {\n setTxError(\"Invalid recipient address.\");\n return;\n }\n }\n\n if (\n activeMode === \"send\" &&\n ownerAddress &&\n isAddress(resolvedRecipientAddress) &&\n resolvedRecipientAddress.toLowerCase() === ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\n }\n\n if (!isCurrentQuoteInput()) {\n return;\n }\n\n if (!background) {\n swapStepRef.current = \"preview-intent\";\n setSwapStep(\"preview-intent\");\n }\n setIntentLoading(true);\n setQuoteRefreshing(background);\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n swapIntentRef.current?.deny();\n swapIntentRef.current = null;\n if (!background) {\n resetProgressEvents();\n swapStepsListRef.current = [];\n resetSteps();\n }\n\n if (!nexusSDK) {\n setTxError(\"SDK not initialized\");\n if (!background) {\n setSwapStep(\"idle\");\n }\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n swapRunIdRef.current += 1;\n const runId = swapRunIdRef.current;\n\n const isActionPlanStep = (step: SwapStepType | BridgeStepType) => {\n const type = getProgressStepType(step);\n return (\n type === \"APPROVAL\" ||\n type === \"TRANSACTION_SENT\" ||\n type === \"TRANSACTION_CONFIRMED\"\n );\n };\n\n const hasSwapPlanSteps = (stepList: Array) =>\n stepList.some((step) => !isActionPlanStep(step));\n\n const handleProgressStepSideEffects = (\n event: any,\n step: SwapStepType | BridgeStepType,\n completed: boolean\n ) => {\n const type = getProgressStepType(step);\n const rawStepType = String(\n event?.stepType ?? (step as any)?.type ?? (step as any)?.typeID ?? \"\"\n ).toLowerCase();\n const rawState = String(event?.state ?? \"\").toLowerCase();\n const explorerUrl = getPlanStepExplorerUrl(event, step);\n const sourceMovementStep = isSourceMovementProgressStep(type, rawStepType);\n const hasSourceMovementTransaction = Boolean(\n explorerUrl || getPlanStepTransactionHash(event, step)\n );\n const intentExplorerUrl = getEventIntentExplorerUrl(\n appConfig.nexusNetwork,\n event,\n step\n );\n\n patchCurrentIntentExplorerUrl(intentExplorerUrl);\n\n if (\n type === \"TRANSACTION_SENT\" ||\n type === \"TRANSACTION_CONFIRMED\" ||\n type === \"SOURCE_SWAP\" ||\n type === \"BRIDGE_DEPOSIT\" ||\n type === \"BRIDGE_INTENT_SUBMISSION\" ||\n type === \"BRIDGE_FILL\" ||\n type === \"DESTINATION_SWAP\" ||\n type === \"SWAP_COMPLETE\" ||\n type === \"SWAP_SKIPPED\"\n ) {\n markSwapExecutionStarted();\n }\n\n if (\n sourceMovementStep &&\n hasSourceMovementTransaction &&\n PLAN_STEP_FUNDS_MAY_HAVE_MOVED_STATES.has(rawState)\n ) {\n fundsMovedRef.current = true;\n }\n\n if (explorerUrl) {\n if (\n rawStepType === \"destination_swap\" ||\n rawStepType === \"execute_transaction\" ||\n type.includes(\"DESTINATION_SWAP\") ||\n type === \"TRANSACTION_SENT\" ||\n type === \"TRANSACTION_CONFIRMED\"\n ) {\n mergeExplorerUrls({ destinationExplorerUrl: explorerUrl });\n } else if (\n sourceMovementStep\n ) {\n mergeExplorerUrls({ sourceExplorerUrl: explorerUrl });\n }\n\n if (\n !intentUrlRef.current &&\n (rawStepType === \"bridge_intent_submission\" ||\n rawStepType === \"request_submission\" ||\n type === \"BRIDGE_INTENT_SUBMISSION\")\n ) {\n patchCurrentIntentExplorerUrl(explorerUrl);\n }\n }\n\n if (completed) {\n onStepComplete(step as SwapStepType);\n }\n };\n\n const handlePlanEvent = (event: any) => {\n if (event.type === \"plan_preview\" || event.type === \"plan_confirmed\") {\n const stepList = Array.isArray(event.plan?.steps)\n ? event.plan.steps.map((step: any) =>\n normalizePlanStep(step, step?.type, undefined, false)\n )\n : [];\n logSwapPlanSteps(event.type, stepList, event.plan?.steps);\n if (stepList.length === 0) return;\n\n if (hasSwapPlanSteps(stepList)) {\n swapStepsListRef.current = stepList as SwapStepType[];\n appendProgressListEvent(\n PROGRESS_EVENT_NAMES.SWAP_PLAN_LIST,\n stepList\n );\n } else {\n appendProgressListEvent(\n PROGRESS_EVENT_NAMES.BRIDGE_PLAN_LIST,\n stepList\n );\n }\n onStepsList(stepList as SwapStepType[]);\n return;\n }\n\n if (event.type !== \"plan_progress\") {\n logSdkSwapEvent(\"unhandled typed event\", event);\n return;\n }\n\n const state = String(event.state ?? \"\").toLowerCase();\n const completed = PLAN_FINAL_STATES.has(state);\n const step = normalizePlanStep(\n event.step,\n event.stepType,\n event.state,\n completed\n );\n const eventName = isActionPlanStep(step)\n ? PROGRESS_EVENT_NAMES.BRIDGE_PLAN_PROGRESS\n : PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS;\n\n logSwapPlanProgress(event, step, eventName, completed);\n appendProgressEvent(eventName, step, completed, event);\n handleProgressStepSideEffects(event, step, completed);\n };\n\n const appendSkippedSwapProgress = () => {\n const step = {\n completed: true,\n type: \"SWAP_SKIPPED\",\n typeID: \"SWAP_SKIPPED\",\n } as SwapStepType;\n enterSkippedSwapProgress();\n appendProgressEvent(PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS, step, true);\n onStepComplete(step);\n };\n\n const handleSwapEvent = (event: any) => {\n if (!event || typeof event !== \"object\") return;\n if (typeof event.type === \"string\") {\n handlePlanEvent(event);\n return;\n }\n logSdkSwapEvent(\"ignored event without string type\", event);\n };\n\n const onEvent = (event: any) => {\n const isCurrentRun = swapRunIdRef.current === runId;\n const isCurrentQuote = isCurrentQuoteInput();\n logSdkSwapEvent(\"onEvent\", event, {\n currentRunId: swapRunIdRef.current,\n isCurrentQuote,\n isCurrentRun,\n quoteInputKey,\n runId,\n });\n if (!isCurrentRun || !isCurrentQuote) {\n logSdkSwapEvent(\"ignored stale onEvent\", event, {\n currentRunId: swapRunIdRef.current,\n isCurrentQuote,\n quoteInputKey,\n runId,\n });\n return;\n }\n patchCurrentIntentExplorerUrl(\n getEventIntentExplorerUrl(appConfig.nexusNetwork, event)\n );\n handleSwapEvent(event);\n };\n\n const buildRecipientTransferExecuteConfig = (transferAmount: bigint) => {\n if (!resolvedRecipientAddress) {\n throw new Error(\"Recipient address is required\");\n }\n\n const isNative =\n !toToken.contractAddress ||\n toToken.contractAddress.toLowerCase() ===\n \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\" ||\n toToken.contractAddress ===\n \"0x0000000000000000000000000000000000000000\";\n\n if (isNative) {\n return {\n to: resolvedRecipientAddress as `0x${string}`,\n value: transferAmount,\n gas: BigInt(100000),\n };\n }\n\n return {\n to: toToken.contractAddress as `0x${string}`,\n data: encodeFunctionData({\n abi: erc20Abi,\n functionName: \"transfer\",\n args: [resolvedRecipientAddress as `0x${string}`, transferAmount],\n }),\n gas: BigInt(100000),\n };\n };\n\n const executeRecipientTransfer = async (transferAmount: bigint) => {\n const result = await nexusSDK.execute(\n {\n toChainId: toToken.chainId!,\n ...buildRecipientTransferExecuteConfig(transferAmount),\n },\n { onEvent }\n );\n const finalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result\n );\n if (finalExplorerUrl) {\n setTransferExplorerUrl(finalExplorerUrl);\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n return finalExplorerUrl;\n };\n\n try {\n if (!isExactOutFlow) {\n const fromPayload: {\n chainId: number;\n tokenAddress: `0x${string}`;\n amountRaw: bigint;\n }[] = [];\n\n const exactInSourceTokens = getReadyExactInSourceTokens(fromTokens);\n\n for (const token of exactInSourceTokens) {\n // Determine the amount to use for this specific token\n let rawAmountStr = token.userAmount;\n if (!rawAmountStr && exactInSourceTokens.length === 1) {\n rawAmountStr = amount; // fallback for single-token case\n }\n\n let cleanAmount = parseFiatNumber(rawAmountStr) ?? new Decimal(0);\n if (cleanAmount.lte(0)) continue;\n\n if (token.userAmountMode === \"usd\") {\n const tokenBalance =\n parseFiatNumber(token.balance) ?? new Decimal(0);\n const fiatBalance =\n parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n const price = tokenBalance.gt(0)\n ? fiatBalance.div(tokenBalance)\n : new Decimal(0);\n if (price.gt(0)) {\n cleanAmount = cleanAmount.div(price);\n } else {\n cleanAmount = new Decimal(0);\n }\n }\n\n if (cleanAmount.lte(0)) continue;\n\n const safeTokenAmountStr = toViemDecimalString(\n cleanAmount,\n token.decimals || 18\n );\n\n fromPayload.push({\n chainId: token.chainId!,\n tokenAddress: token.contractAddress as `0x${string}`,\n amountRaw: parseUnits(safeTokenAmountStr, token.decimals || 18),\n });\n }\n\n if (fromPayload.length === 0) {\n throw new Error(\"No source amount available for swap.\");\n }\n\n resetExplorerUrls();\n const exactInSwapPayload = {\n sources: fromPayload,\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n };\n let intentExplorerUrl: string | null = null;\n let intentId = currentSwapEntry?.intentId;\n let finalExplorerUrl: string | null =\n explorerUrlsRef.current.destinationExplorerUrl ||\n explorerUrlsRef.current.sourceExplorerUrl;\n\n if (hasCustomSwapRecipient && resolvedRecipientAddress) {\n const sdkWithOptionalTransfer = nexusSDK as any;\n\n if (typeof sdkWithOptionalTransfer.swapAndTransfer === \"function\") {\n const swapAndTransferExactInInput = {\n mode: \"exactIn\",\n recipient: resolvedRecipientAddress as `0x${string}`,\n ...exactInSwapPayload,\n };\n logSdkIntentInput(\"swapAndTransfer exactIn\", swapAndTransferExactInInput, {\n activeMode,\n quoteInputKey,\n runId,\n });\n const result = await sdkWithOptionalTransfer.swapAndTransfer(\n swapAndTransferExactInInput,\n { onEvent }\n );\n if (result?.success === false) {\n throw new Error(result?.error || \"Swap and transfer failed\");\n }\n\n const swapResult = getSdkSwapResult(result);\n intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result,\n swapResult\n );\n intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const resultFinalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result,\n swapResult\n );\n finalExplorerUrl = resultFinalExplorerUrl || finalExplorerUrl;\n if (resultFinalExplorerUrl) {\n setTransferExplorerUrl(resultFinalExplorerUrl);\n mergeExplorerUrls({\n destinationExplorerUrl: resultFinalExplorerUrl,\n });\n }\n } else {\n logSdkIntentInput(\"swapWithExactIn\", exactInSwapPayload, {\n activeMode,\n quoteInputKey,\n runId,\n });\n startIntentFetchTiming({\n background,\n operation: \"swapWithExactIn\",\n quoteInputKey,\n runId,\n });\n const result = await nexusSDK.swapWithExactIn(exactInSwapPayload, {\n hooks: {\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n },\n onEvent,\n });\n\n intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result\n );\n intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n\n const latestSwapIntent = (\n swapIntentRef.current as unknown as {\n intent?: SwapIntentData;\n } | null\n )?.intent;\n const transferAmount = latestSwapIntent?.destination?.amount;\n if (!transferAmount) {\n throw new Error(\n \"Unable to determine received amount to transfer.\"\n );\n }\n\n const transferAmountBigInt = parseUnits(\n toViemDecimalString(transferAmount, toToken.decimals || 18),\n toToken.decimals || 18\n );\n finalExplorerUrl =\n (await executeRecipientTransfer(transferAmountBigInt)) ||\n finalExplorerUrl;\n }\n } else {\n // Start exact-in swap — the intent hook will fire and populate preview\n logSdkIntentInput(\"swapWithExactIn\", exactInSwapPayload, {\n activeMode,\n quoteInputKey,\n runId,\n });\n startIntentFetchTiming({\n background,\n operation: \"swapWithExactIn\",\n quoteInputKey,\n runId,\n });\n const result = await nexusSDK.swapWithExactIn(exactInSwapPayload, {\n hooks: {\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n },\n onEvent,\n });\n intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result\n );\n intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const swapResult = getSdkSwapResult(result);\n const resultFinalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result,\n swapResult\n );\n finalExplorerUrl = resultFinalExplorerUrl || finalExplorerUrl;\n if (resultFinalExplorerUrl) {\n setTransferExplorerUrl(resultFinalExplorerUrl);\n mergeExplorerUrls({\n destinationExplorerUrl: resultFinalExplorerUrl,\n });\n }\n }\n\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n const resolvedFinalExplorerUrl =\n finalExplorerUrl ||\n explorerUrlsRef.current.destinationExplorerUrl ||\n explorerUrlsRef.current.sourceExplorerUrl;\n const resolvedIntentExplorerUrl =\n intentExplorerUrl || intentUrlRef.current;\n finishCurrentSwapHistoryEntry(\"fulfilled\", {\n finalExplorerUrl: resolvedFinalExplorerUrl,\n ...(resolvedIntentExplorerUrl\n ? { intentExplorerUrl: resolvedIntentExplorerUrl }\n : {}),\n ...(intentId ? { intentId } : {}),\n });\n resetInputsAfterSuccessfulExecution();\n onComplete?.();\n setSwapStep(\"success\");\n }\n } else {\n const exactOutAmountString =\n activeMode === \"deposit\"\n ? depositTokenAmountForQuote\n ?.toDecimalPlaces(toToken.decimals || 18, Decimal.ROUND_DOWN)\n .toFixed()\n : amount;\n if (!exactOutAmountString || new Decimal(exactOutAmountString).lte(0)) {\n setTxError(\n depositAmountMode === \"usd\"\n ? \"Unable to convert USD amount into the destination token amount.\"\n : \"Enter a valid amount.\"\n );\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n const amountBigInt = parseUnits(\n toViemDecimalString(exactOutAmountString, toToken.decimals || 18),\n toToken.decimals || 18\n );\n\n resetExplorerUrls();\n\n const fromSourcesPayload = buildExactOutSourcesPayload(\n getExactOutSourceTokens()\n );\n\n let executeConfig: any;\n if (activeMode === \"deposit\" && !selectedOpportunity?.executeDeposit) {\n throw new Error(\"Deposit config is missing executeDeposit.\");\n }\n\n if (activeMode === \"deposit\" && selectedOpportunity) {\n const user = (ownerAddress ?? connectedAddress) as `0x${string}`;\n\n const executeParams = selectedOpportunity.executeDeposit(\n selectedOpportunity.tokenSymbol,\n selectedOpportunity.tokenAddress,\n amountBigInt,\n selectedOpportunity.chainId,\n user\n );\n if (!isPositiveGasLimit(executeParams.gas)) {\n throw new Error(\n \"Deposit config executeDeposit must return a positive gas limit.\"\n );\n }\n executeConfig = {\n to: executeParams.to,\n value: executeParams.value,\n data: executeParams.data,\n tokenApproval: executeParams.tokenApproval,\n gas: executeParams.gas,\n };\n } else if (\n (activeMode === \"send\" || hasCustomSwapRecipient) &&\n resolvedRecipientAddress\n ) {\n executeConfig = buildRecipientTransferExecuteConfig(amountBigInt);\n }\n\n if (executeConfig?.tokenApproval) {\n executeConfig = {\n ...executeConfig,\n tokenApproval: {\n toTokenAddress:\n executeConfig.tokenApproval.toTokenAddress ||\n toToken.contractAddress,\n amount: executeConfig.tokenApproval.amount,\n spender: executeConfig.tokenApproval.spender,\n },\n };\n }\n\n if (executeConfig) {\n const sdkWithOptionalTransfer = nexusSDK as any;\n const isTransferExactOut =\n (activeMode === \"send\" || hasCustomSwapRecipient) &&\n typeof sdkWithOptionalTransfer.swapAndTransfer === \"function\";\n const exactOutOperationInput = isTransferExactOut\n ? {\n mode: \"exactOut\",\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmountRaw: amountBigInt,\n recipient: resolvedRecipientAddress as `0x${string}`,\n ...fromSourcesPayload,\n }\n : {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmountRaw: amountBigInt,\n execute: executeConfig,\n ...fromSourcesPayload,\n };\n logSdkIntentInput(\n isTransferExactOut\n ? \"swapAndTransfer exactOut\"\n : \"swapAndExecute exactOut\",\n exactOutOperationInput,\n {\n activeMode,\n quoteInputKey,\n runId,\n }\n );\n startIntentFetchTiming({\n background,\n operation: isTransferExactOut\n ? \"swapAndTransfer exactOut\"\n : \"swapAndExecute exactOut\",\n quoteInputKey,\n runId,\n });\n const result =\n isTransferExactOut\n ? await sdkWithOptionalTransfer.swapAndTransfer(\n exactOutOperationInput,\n {\n onEvent,\n onIntent: (data: any) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n }\n )\n : await nexusSDK.swapAndExecute(\n exactOutOperationInput as any,\n {\n onEvent,\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n }\n );\n\n const swapResult = result?.swapResult ?? result?.result ?? null;\n const swapSkipped = Boolean((result as any)?.swapSkipped);\n if (swapSkipped) {\n appendSkippedSwapProgress();\n }\n if (\n !swapResult &&\n !swapSkipped &&\n activeMode !== \"send\" &&\n !hasCustomSwapRecipient\n ) {\n throw new Error(\"Swap failed\");\n }\n const executeTxHash = getSdkTransactionHash(result);\n const intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result,\n swapResult\n );\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const finalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n executeTxHash,\n result,\n swapResult\n );\n if (finalExplorerUrl) {\n if (activeMode === \"send\" || hasCustomSwapRecipient) {\n setTransferExplorerUrl(finalExplorerUrl);\n }\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n patchCurrentSwapHistoryEntry({\n ...(finalExplorerUrl ? { finalExplorerUrl } : {}),\n ...(intentExplorerUrl ? { intentExplorerUrl } : {}),\n ...(intentId ? { intentId } : {}),\n });\n } else {\n const exactOutSwapInput = {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmountRaw: amountBigInt,\n ...fromSourcesPayload,\n };\n logSdkIntentInput(\"swapWithExactOut\", exactOutSwapInput, {\n activeMode,\n quoteInputKey,\n runId,\n });\n startIntentFetchTiming({\n background,\n operation: \"swapWithExactOut\",\n quoteInputKey,\n runId,\n });\n const result = await nexusSDK.swapWithExactOut(\n exactOutSwapInput,\n {\n hooks: {\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n },\n onEvent,\n }\n );\n const intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result\n );\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const swapResult = getSdkSwapResult(result);\n const finalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result,\n swapResult\n );\n if (finalExplorerUrl) {\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n patchCurrentSwapHistoryEntry({\n ...(finalExplorerUrl ? { finalExplorerUrl } : {}),\n ...(intentExplorerUrl ? { intentExplorerUrl } : {}),\n ...(intentId ? { intentId } : {}),\n });\n }\n\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n finishCurrentSwapHistoryEntry(\"fulfilled\");\n resetInputsAfterSuccessfulExecution();\n onComplete?.();\n if (activeMode === \"deposit\") {\n reachedTerminalRef.current = true;\n const now = Date.now();\n trackDeposit(\"deposit_completed\", {\n postConfirmDurationMs: previewConfirmedTsRef.current\n ? now - previewConfirmedTsRef.current\n : 0,\n totalDurationMs: now - widgetOpenedTsRef.current,\n attemptCount: attemptCountRef.current,\n amountToken: depositTokenDisplay,\n amountUsd: Number(depositUsdDisplay) || 0,\n });\n }\n setSwapStep(\"success\");\n }\n }\n } catch (err: any) {\n const caughtTimeout = isTimeoutLikeError(err);\n if (caughtTimeout) {\n console.warn(\"Timeout in handleEnterPreview:\", err);\n } else {\n console.error(\"Error in handleEnterPreview:\", err);\n }\n if (swapRunIdRef.current !== runId || !isCurrentQuoteInput()) {\n return;\n }\n finishIntentFetchTiming(runId, \"failed\");\n if (activeMode === \"deposit\" && err?.code !== \"USER_DENIED_INTENT\") {\n const hasActiveExecution =\n swapStepRef.current === \"progress\" &&\n Boolean(currentSwapIdRef.current);\n const isInsufficient = isInsufficientSourcesError(err);\n const errMessage =\n (typeof err?.message === \"string\" ? err.message : \"\") ||\n (typeof err === \"string\" ? err : \"\");\n const errName = typeof err?.name === \"string\" ? err.name : \"\";\n const isTimeout = isTimeoutLikeError(err);\n const isUserRejected =\n err?.code === 4001 ||\n err?.code === \"ACTION_REJECTED\" ||\n errName === \"UserRejectedRequestError\" ||\n /user rejected|user denied/i.test(errMessage);\n const failedAtStep:\n | \"simulation\"\n | \"nexus_operation\"\n | \"execute_leg\"\n | \"unknown\" = !hasActiveExecution ? \"simulation\" : \"nexus_operation\";\n const errorCategory: string = isUserRejected\n ? \"user_rejected\"\n : isTimeout\n ? \"timeout\"\n : isInsufficient\n ? \"no_eligible_sources\"\n : !hasActiveExecution\n ? \"quote_failed\"\n : fundsMovedRef.current\n ? \"pending_after_funds_moved\"\n : \"execution_failed\";\n reachedTerminalRef.current = true;\n if (fundsMovedRef.current) {\n trackDeposit(\"deposit_pending_after_funds_moved\", {\n errorCode: err?.code ?? \"UNKNOWN\",\n errorMessage: errMessage || \"Transaction pending.\",\n intentUrl: intentUrlRef.current,\n failedAtStep,\n });\n } else {\n trackDeposit(\"deposit_failed\", {\n errorCode: err?.code ?? \"UNKNOWN\",\n errorCategory,\n errorMessage: errMessage || \"Transaction failed.\",\n failedAtStep,\n });\n }\n }\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n const hasActiveExecution =\n swapStepRef.current === \"progress\" && Boolean(currentSwapIdRef.current);\n const isTimeout = caughtTimeout;\n const showFailedProgressThenReceipt = (\n error: string,\n patch: Partial = {}\n ) => {\n const failedProgressEvent = progressEventsRef.current.at(-1);\n const isTransferExecution =\n activeMode === \"send\" || hasCustomSwapRecipient;\n const fallbackFailedStep =\n activeMode === \"deposit\" || isTransferExecution\n ? ({ type: \"APPROVAL\", typeID: \"AP\" } as BridgeStepType)\n : ({\n type: \"DETERMINING_SWAP\",\n typeID: \"DETERMINING_SWAP\",\n } as unknown as SwapStepType);\n const failedStep = failedProgressEvent?.step ?? fallbackFailedStep;\n const autoRefundAvailable =\n isAutoRefundAvailableProgressEvent(failedProgressEvent);\n setFailedProgressStep(failedStep);\n finishCurrentSwapHistoryEntry(\"failed\", {\n error,\n autoRefundAvailable,\n failureDescription: getFailureDescriptionForProgressStep(\n failedStep,\n autoRefundAvailable\n ),\n failureMessage: getFailureMessageForProgressStep(\n failedStep,\n hasCustomSwapRecipient ? \"send\" : activeMode,\n autoRefundAvailable\n ),\n failedStepType: getProgressStepType(failedStep),\n ...patch,\n });\n window.setTimeout(() => {\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n setSwapStep(\"failed\");\n }\n }, 700);\n };\n const showTimeoutReceipt = (\n message = \"Transaction timed out\",\n patch: Partial = {}\n ) => {\n finishCurrentSwapHistoryEntry(\"timeout\", {\n error: message,\n failureDescription:\n \"This transaction is still pending. Check the intent explorer for the latest status.\",\n failureMessage: TIMEOUT_LABEL,\n ...patch,\n });\n window.setTimeout(() => {\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n setSwapStep(\"failed\");\n }\n }, 700);\n };\n if (err?.code === \"USER_DENIED_INTENT\") {\n if (hasActiveExecution) {\n showFailedProgressThenReceipt(\"Transaction cancelled by user\");\n } else if (!background && swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n return;\n }\n if (isInsufficientSourcesError(err) && !hasActiveExecution) {\n const issue = buildInsufficientSourcesIssue(err);\n if (!background || swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n setTxError(null);\n setSwapQuoteIssue(issue);\n onError?.(issue.message);\n return;\n }\n if (isExactOutFlow && isViemInvalidDecimalError(err) && !hasActiveExecution) {\n const issue = buildExactOutSourceBalanceIssue();\n if (issue) {\n if (!background || swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n setTxError(null);\n setSwapQuoteIssue(issue);\n onError?.(issue.message);\n return;\n }\n }\n const errorMessage =\n err?.message ||\n (typeof err === \"string\"\n ? err\n : \"Transaction failed. Please try again or check console.\");\n if (isTimeout && hasActiveExecution) {\n showTimeoutReceipt(errorMessage);\n setTxError(null);\n return;\n }\n if (hasActiveExecution && fundsMovedRef.current) {\n showTimeoutReceipt(errorMessage, {\n failureDescription:\n \"Funds have already moved and the transaction is still pending. Check the intent explorer for the latest status.\",\n });\n setTxError(null);\n return;\n }\n if (hasActiveExecution) {\n showFailedProgressThenReceipt(errorMessage);\n } else if (!background || swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n setTxError(errorMessage);\n onError?.(errorMessage);\n }\n };\n\n const hasInsufficientSourcesQuoteIssue =\n swapQuoteIssue?.type === \"insufficientSources\";\n const hasReceiveAmountQuoteIssue = Boolean(receiveAmountIssue);\n\n useEffect(() => {\n if (activeMode !== \"swap\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = false;\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n }\n\n if (hasReceiveAmountQuoteIssue) {\n clearPendingSwapIntent(true);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n if (hasInsufficientSourcesQuoteIssue) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n const hasEnoughForQuote = hasReadyExactInSwapInput(fromTokens, toToken);\n\n if (!hasEnoughForQuote) {\n clearPendingSwapIntent();\n setSwapQuoteIssue(null);\n setTxError(null);\n return;\n }\n\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n return;\n }\n\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n let quoteStarted = false;\n const quoteDelay = getQuoteRequestDelay();\n const timer = window.setTimeout(() => {\n quoteStarted = true;\n void handleEnterPreview({ background: true });\n }, quoteDelay);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (!quoteStarted && swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n activeQuoteInputKey,\n amount,\n defaultRecipientAddress,\n fromTokensQuoteKey,\n hasCurrentQuoteIntent,\n hasInsufficientSourcesQuoteIssue,\n hasReceiveAmountQuoteIssue,\n getQuoteRequestDelay,\n nexusSDK,\n recipientAddress,\n swapStep,\n toTokenQuoteKey,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = false;\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n }\n\n if (hasReceiveAmountQuoteIssue) {\n clearPendingSwapIntent(true);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n if (hasInsufficientSourcesQuoteIssue) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n const parsedAmount = parseFiatNumber(amount);\n const hasEnoughForQuote = Boolean(\n parsedAmount?.gt(0) &&\n toToken &&\n selectedOpportunity &&\n depositTokenAmountForQuote\n );\n\n if (!hasEnoughForQuote) {\n clearPendingSwapIntent();\n clearSelectedSources();\n return;\n }\n\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n return;\n }\n\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n let quoteStarted = false;\n const quoteDelay = getQuoteRequestDelay();\n const timer = window.setTimeout(() => {\n quoteStarted = true;\n void handleEnterPreview({ background: true });\n }, quoteDelay);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (!quoteStarted && swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n amount,\n activeQuoteInputKey,\n depositAmountMode,\n depositQuoteAmountKey,\n hasCurrentQuoteIntent,\n hasInsufficientSourcesQuoteIssue,\n hasReceiveAmountQuoteIssue,\n getQuoteRequestDelay,\n nexusSDK,\n sourceSelectionRevision,\n selectedOpportunityIdentity,\n swapStep,\n toTokenQuoteKey,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"send\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = false;\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n }\n\n if (hasReceiveAmountQuoteIssue) {\n clearPendingSwapIntent(true);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n if (hasInsufficientSourcesQuoteIssue) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n const parsedAmount = parseFiatNumber(amount);\n const hasEnoughForQuote = Boolean(parsedAmount?.gt(0) && toToken);\n\n if (!hasEnoughForQuote) {\n clearPendingSwapIntent();\n clearSelectedSources();\n return;\n }\n\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n return;\n }\n\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n let quoteStarted = false;\n const quoteDelay = getQuoteRequestDelay();\n const timer = window.setTimeout(() => {\n quoteStarted = true;\n void handleEnterPreview({ background: true });\n }, quoteDelay);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (!quoteStarted && swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n amount,\n activeQuoteInputKey,\n hasCurrentQuoteIntent,\n hasInsufficientSourcesQuoteIssue,\n hasReceiveAmountQuoteIssue,\n getQuoteRequestDelay,\n nexusSDK,\n sourceSelectionRevision,\n swapStep,\n toTokenQuoteKey,\n ]);\n\n const refreshActiveSwapIntent = useCallback(async () => {\n if (receiveAmountIssueRef.current) return;\n\n const activeIntent = swapIntentRef.current;\n if (\n !activeIntent ||\n intentLoading ||\n quoteRefreshing ||\n receiveMaxCalculating ||\n previewQuoteRefreshing\n ) {\n return;\n }\n\n const runId = activeIntent.runId;\n const quoteInputKey = activeIntent.quoteInputKey;\n if (!quoteInputKey || activeQuoteInputKeyRef.current !== quoteInputKey) {\n return;\n }\n const isPreviewRefresh = swapStepRef.current === \"preview-intent\";\n if (isPreviewRefresh) {\n setPreviewQuoteRefreshing(true);\n } else {\n setQuoteRefreshing(true);\n }\n try {\n const updatedRaw = await activeIntent.refresh();\n const updatedBridgeProvider = normalizeBridgeProvider(\n (updatedRaw as any)?.bridgeProvider ??\n (updatedRaw as any)?.normalizedIntent?.bridgeProvider ??\n (updatedRaw as any)?.swap?.bridgeProvider ??\n activeIntent.intent?.bridgeProvider\n );\n const updated = normalizeRenderableSwapIntentData(\n updatedRaw,\n updatedBridgeProvider\n );\n if (\n !updated ||\n swapRunIdRef.current !== runId ||\n activeQuoteInputKeyRef.current !== quoteInputKey\n ) {\n return;\n }\n\n if (swapIntentRef.current) {\n swapIntentRef.current.intent = updated;\n }\n applySwapIntent(updated);\n } catch (err) {\n console.error(\"Unable to refresh swap intent\", err);\n } finally {\n if (\n swapRunIdRef.current === runId &&\n activeQuoteInputKeyRef.current === quoteInputKey\n ) {\n if (isPreviewRefresh) {\n setPreviewQuoteRefreshing(false);\n } else {\n setQuoteRefreshing(false);\n }\n }\n }\n }, [\n applySwapIntent,\n intentLoading,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n ]);\n\n useEffect(() => {\n const hasRefreshableIntent =\n (activeMode === \"swap\" ||\n activeMode === \"deposit\" ||\n activeMode === \"send\") &&\n Boolean(\n intentData &&\n swapIntentRef.current &&\n swapIntentRef.current.quoteInputKey === activeQuoteInputKey\n ) &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n\n if (!hasRefreshableIntent || receiveAmountIssue) {\n setQuoteRefreshProgress(0);\n setQuoteRefreshSecondsRemaining(0);\n return;\n }\n\n let cancelled = false;\n let timeout: number | undefined;\n\n const scheduleRefresh = () => {\n const quoteAge = Date.now() - lastSwapIntentRefreshAtRef.current;\n const delay = Math.max(0, QUOTE_REFRESH_INTERVAL_MS - quoteAge);\n timeout = window.setTimeout(() => {\n if (receiveAmountIssueRef.current) {\n clearPendingSwapIntent(true);\n setQuoteRefreshProgress(0);\n setQuoteRefreshSecondsRemaining(0);\n return;\n }\n\n if (\n intentLoading ||\n quoteRefreshing ||\n receiveMaxCalculating ||\n previewQuoteRefreshing\n ) {\n if (!cancelled) {\n timeout = window.setTimeout(scheduleRefresh, 1000);\n }\n return;\n }\n\n void refreshActiveSwapIntent().finally(() => {\n if (!cancelled) {\n scheduleRefresh();\n }\n });\n }, delay);\n };\n\n scheduleRefresh();\n\n return () => {\n cancelled = true;\n if (timeout !== undefined) {\n window.clearTimeout(timeout);\n }\n };\n }, [\n activeMode,\n activeQuoteInputKey,\n intentData,\n intentLoading,\n receiveAmountIssue,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n refreshActiveSwapIntent,\n swapStep,\n ]);\n\n useEffect(() => {\n const hasRefreshableIntent =\n (activeMode === \"swap\" ||\n activeMode === \"deposit\" ||\n activeMode === \"send\") &&\n Boolean(\n intentData &&\n swapIntentRef.current &&\n swapIntentRef.current.quoteInputKey === activeQuoteInputKey\n ) &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n\n if (!hasRefreshableIntent || receiveAmountIssue) {\n setQuoteRefreshProgress(0);\n setQuoteRefreshSecondsRemaining(0);\n return;\n }\n\n const updateProgress = () => {\n const quoteAge = Date.now() - lastSwapIntentRefreshAtRef.current;\n const remaining = Math.max(0, QUOTE_REFRESH_INTERVAL_MS - quoteAge);\n setQuoteRefreshProgress(remaining / QUOTE_REFRESH_INTERVAL_MS);\n setQuoteRefreshSecondsRemaining(Math.ceil(remaining / 1000));\n };\n\n updateProgress();\n const interval = window.setInterval(updateProgress, 250);\n\n return () => window.clearInterval(interval);\n }, [\n activeMode,\n activeQuoteInputKey,\n intentData,\n receiveAmountIssue,\n swapStep,\n ]);\n\n /** User accepted swap from the preview — call allow() from the intent hook */\n const handleSwapAccept = () => {\n const activeIntent = swapIntentRef.current;\n if (activeIntent) {\n if (\n activeIntent.quoteInputKey &&\n activeQuoteInputKeyRef.current !== activeIntent.quoteInputKey\n ) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n setSwapStep(\"idle\");\n return;\n }\n if (activeMode === \"deposit\") {\n previewConfirmedTsRef.current = Date.now();\n attemptCountRef.current += 1;\n const timeInPreviewMs = previewViewedTsRef.current\n ? previewConfirmedTsRef.current - previewViewedTsRef.current\n : 0;\n trackDeposit(\"deposit_preview_confirmed\", {\n timeInPreviewMs,\n totalFeeUsd: Number(intentFeeUsd) || 0,\n sourceCount: (intentData?.sources ?? []).length,\n });\n }\n onStart?.();\n startSwapHistoryEntry();\n setSwapStep(\"progress\");\n setQuoteRefreshing(false);\n resetProgressEvents();\n if (swapStepsListRef.current.length > 0) {\n seed(swapStepsListRef.current);\n } else {\n resetSteps();\n }\n activeIntent.allow();\n }\n };\n\n // ---------------------------------------------------------------------------\n // Header title\n // ---------------------------------------------------------------------------\n const isDepositMethodScreen =\n activeMode === \"deposit\" &&\n swapStep === \"idle\" &&\n depositFundingStep === \"method\";\n const isDepositWalletScreen =\n activeMode === \"deposit\" &&\n swapStep === \"idle\" &&\n depositFundingStep === \"wallet\";\n const isDepositOnrampScreen =\n activeMode === \"deposit\" &&\n swapStep === \"idle\" &&\n depositFundingStep === \"onramp\";\n const normalizedDepositOnrampSessionState =\n depositOnrampSessionState?.trim().toUpperCase() ?? \"\";\n const hasDepositOnrampSession =\n isDepositOnrampScreen && Boolean(depositOnrampSessionState);\n const getDepositOnrampSessionTitle = () => {\n if (\n normalizedDepositOnrampSessionState === \"SETTLED\" ||\n [\n \"COMPLETED\",\n \"DEPOSIT_COMPLETE\",\n \"DEPOSIT_SUCCESS\",\n \"DEPOSITED\",\n ].includes(normalizedDepositOnrampSessionState)\n ) {\n return \"Success\";\n }\n if (normalizedDepositOnrampSessionState === \"FAILED\") {\n return \"Payment failed\";\n }\n if (\n [\n \"DEPOSIT_ATTENTION\",\n \"DEPOSIT_FAILED\",\n \"DEPOSIT_REQUIRES_ATTENTION\",\n ].includes(normalizedDepositOnrampSessionState)\n ) {\n return \"Deposit needs your attention\";\n }\n if (normalizedDepositOnrampSessionState === \"CANCELLED\") {\n return \"Payment cancelled\";\n }\n if (normalizedDepositOnrampSessionState === \"REFUNDED\") {\n return \"Payment refunded\";\n }\n if (normalizedDepositOnrampSessionState === \"EXPIRED\") {\n return \"Payment expired\";\n }\n if (\n [\n \"COMPLETING_DEPOSIT\",\n \"DEPOSIT_PROCESSING\",\n \"DEPOSITING\",\n ].includes(normalizedDepositOnrampSessionState)\n ) {\n return \"Completing your deposit\";\n }\n if (\n normalizedDepositOnrampSessionState === \"PROCESSING\" ||\n normalizedDepositOnrampSessionState === \"SETTLING\"\n ) {\n return \"Processing your payment\";\n }\n return \"Continue on Other Window\";\n };\n\n const getTitle = () => {\n const configuredWidgetHeading = normalizeConfiguredString(\n appearanceConfig?.widgetHeading\n );\n if (swapStep === \"history\") return \"Transaction History\";\n // Drawer panels overlay the main page,\n // so the header should still show the main page title.\n\n if (swapStep === \"preview-intent\") {\n return activeMode === \"deposit\"\n ? \"Confirm Deposit\"\n : activeMode === \"send\"\n ? \"Confirm Send\"\n : \"Confirm Swap\";\n }\n\n if (activeMode === \"swap\") {\n if (swapStep === \"progress\") return \"Swapping…\";\n if (swapStep === \"success\") return \"Swap Complete\";\n if (swapStep === \"failed\" && currentSwapEntry?.status === \"timeout\") {\n return TIMEOUT_LABEL;\n }\n if (swapStep === \"failed\") return \"Swap Failed\";\n return configuredWidgetHeading ?? \"Swap and Bridge\";\n }\n if (activeMode === \"deposit\") {\n if (isDepositOnrampScreen) {\n return hasDepositOnrampSession\n ? getDepositOnrampSessionTitle()\n : \"Payment Amount\";\n }\n if (swapStep === \"progress\") return \"Depositing…\";\n if (swapStep === \"success\") return \"Deposit Complete\";\n if (swapStep === \"failed\" && currentSwapEntry?.status === \"timeout\") {\n return TIMEOUT_LABEL;\n }\n if (swapStep === \"failed\") return \"Deposit Failed\";\n return configuredWidgetHeading ?? \"Deposit\";\n }\n if (activeMode === \"send\") {\n if (swapStep === \"progress\") return \"Sending…\";\n if (swapStep === \"success\") return \"Send Complete\";\n if (swapStep === \"failed\" && currentSwapEntry?.status === \"timeout\") {\n return TIMEOUT_LABEL;\n }\n if (swapStep === \"failed\") return \"Send Failed\";\n return configuredWidgetHeading ?? \"Send\";\n }\n return configuredWidgetHeading ?? \"Nexus Widget\";\n };\n\n // Titles that should be center-aligned (main screens / confirm screens)\n // Left-aligned: choose-swap-asset, choose-receive-asset (sub-screens with subtitles)\n const isTitleCentered = () => {\n if (swapStep === \"history\") return false;\n return true; // idle, drawer panels, preview-intent, progress, etc.\n };\n\n const canGoBack =\n swapStep === \"preview-intent\" ||\n swapStep === \"history\" ||\n isDepositWalletScreen ||\n (isDepositOnrampScreen && !hasDepositOnrampSession);\n const showHistoryButton =\n !(isDepositMethodScreen || isDepositOnrampScreen);\n const handleBack = () => {\n if (\n activeMode === \"deposit\" &&\n swapStep === \"idle\" &&\n depositFundingStep !== \"method\"\n ) {\n setDepositFundingStep(\"method\");\n return;\n }\n if (swapStep === \"history\") {\n setSwapStep(\"idle\");\n return;\n }\n if (swapStep === \"choose-swap-asset\") {\n handleSourcePickerCancel();\n return;\n }\n if (swapStep === \"choose-receive-asset\") {\n closeDrawerToIdle();\n return;\n }\n if (swapStep === \"enter-recipient\") {\n closeDrawerToIdle();\n return;\n }\n if (swapStep === \"preview-intent\") {\n const canRequoteAfterPreviewBack =\n activeMode === \"swap\"\n ? hasReadyExactInSwapInput(fromTokens, toToken)\n : canRefreshExactOutQuote();\n\n if (\n canRequoteAfterPreviewBack &&\n (activeMode === \"deposit\" || activeMode === \"send\")\n ) {\n setExactOutQuoteSourceModeValue(\"all\");\n }\n if (activeMode === \"deposit\" || activeMode === \"send\") {\n invalidateExactOutQuoteForRefresh();\n } else {\n clearPendingSwapIntent(true, {\n keepQuoteRefreshing: canRequoteAfterPreviewBack,\n });\n }\n if (canRequoteAfterPreviewBack && activeMode === \"swap\") {\n setQuoteRefreshing(true);\n setTxError(null);\n setSwapQuoteIssue(null);\n }\n setSwapStep(\"idle\");\n return;\n }\n if (swapStep === \"progress\") {\n return;\n } // can't go back during tx\n setSwapStep(\"idle\");\n };\n\n const handleSwapAmountChange = (val: string, panel: \"send\" | \"receive\") => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n setSwapQuoteIssue(null);\n setTxError(null);\n const nextAmount = parseFiatNumber(val);\n const receiveIssue = buildReceiveAmountIssue({ inputAmount: val });\n applyReceiveAmountIssue(receiveIssue);\n const hasSelectedSourceToken = fromTokens.some(\n (token) => token.chainId && token.contractAddress\n );\n const shouldLoadQuote = Boolean(\n !receiveIssue &&\n nexusSDK &&\n nextAmount?.gt(0) &&\n toToken &&\n hasSelectedSourceToken\n );\n if (!receiveIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n }\n setAmount(val);\n if (panel === \"receive\") {\n setFromTokens((prev) =>\n prev.map((token) => ({ ...token, userAmount: \"\" }))\n );\n }\n // Nexus Widget swaps are exact-in only. Exact-out is reserved for Deposit and Send.\n if (swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n };\n\n const handleSwapTokensUpdate = (tokens: SwapTokenOption[]) => {\n setSwapQuoteIssue(null);\n setTxError(null);\n applyReceiveAmountIssue(buildReceiveAmountIssue({ sourceTokens: tokens }));\n setFromTokens(tokens);\n };\n\n const handleDepositAmountChange = (val: string) => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n resetExactOutSourcesToAuto();\n maxPercentRunRef.current += 1;\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setSwapQuoteIssue(null);\n const nextAmount = parseFiatNumber(val);\n const receiveIssue = buildReceiveAmountIssue({ inputAmount: val });\n applyReceiveAmountIssue(receiveIssue);\n const shouldLoadQuote = Boolean(\n !receiveIssue &&\n nexusSDK &&\n nextAmount?.gt(0) &&\n toToken &&\n selectedOpportunity\n );\n if (!receiveIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n } else {\n clearSelectedSources();\n }\n setAmount(val);\n };\n\n const handleSendAmountChange = (val: string) => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n resetExactOutSourcesToAuto();\n maxPercentRunRef.current += 1;\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setSwapQuoteIssue(null);\n setSwapType(\"exactOut\");\n const nextAmount = parseFiatNumber(val);\n const receiveIssue = buildReceiveAmountIssue({\n inputAmount: val,\n type: \"exactOut\",\n });\n applyReceiveAmountIssue(receiveIssue);\n const shouldLoadQuote = Boolean(\n !receiveIssue && nexusSDK && nextAmount?.gt(0) && toToken\n );\n if (!receiveIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n } else {\n clearSelectedSources();\n }\n setAmount(val);\n };\n\n const handleDepositAmountModeToggle = () => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n resetExactOutSourcesToAuto();\n const rate = getDepositTokenUsdRate();\n const parsedAmount = parseFiatNumber(amount) ?? new Decimal(0);\n if (parsedAmount.gt(0) && rate.gt(0)) {\n const converted =\n depositAmountMode === \"token\"\n ? parsedAmount.mul(rate).toDecimalPlaces(2)\n : parsedAmount\n .div(rate)\n .toDecimalPlaces(\n getCappedTokenDisplayDecimals(toToken?.decimals),\n Decimal.ROUND_DOWN\n );\n setAmount(converted.toFixed());\n }\n clearPendingSwapIntent();\n setDepositAmountMode((current) => (current === \"token\" ? \"usd\" : \"token\"));\n };\n\n const handleDepositPercentSelect = async (pct: number) => {\n if (isConfiguredAmountFixed) return;\n if (!toToken) return;\n\n syncingIntentSourcesRef.current = false;\n setTxError(null);\n setSwapQuoteIssue(null);\n const runId = ++maxPercentRunRef.current;\n lastInputMethodRef.current =\n pct === 20\n ? \"percent_20\"\n : pct === 25\n ? \"percent_25\"\n : pct === 50\n ? \"percent_50\"\n : pct === 75\n ? \"percent_75\"\n : \"percent_max\";\n\n if (pct !== 100) {\n const nextAmount = getExactOutPercentAmountFromBalance(\n toToken,\n pct,\n depositAmountMode === \"usd\"\n );\n\n if (nextAmount) {\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setDepositAmountMode(nextAmount.mode);\n handleDepositAmountChange(nextAmount.amount);\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setTxError(\"Unable to calculate this percentage for the deposit asset.\");\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n const maxAmount = getExactOutPercentAmountFromBalance(\n toToken,\n 100,\n depositAmountMode === \"usd\"\n );\n if (runId !== maxPercentRunRef.current) return;\n if (!maxAmount) {\n setTxError(\"No depositable amount is available for this deposit.\");\n return;\n }\n\n setDepositAmountMode(maxAmount.mode);\n handleDepositAmountChange(maxAmount.amount);\n };\n\n const handleSendPercentSelect = async (pct: number) => {\n if (isConfiguredAmountFixed) return;\n if (!toToken) return;\n\n syncingIntentSourcesRef.current = false;\n setTxError(null);\n setSwapQuoteIssue(null);\n const runId = ++maxPercentRunRef.current;\n lastInputMethodRef.current =\n pct === 20\n ? \"percent_20\"\n : pct === 25\n ? \"percent_25\"\n : pct === 50\n ? \"percent_50\"\n : pct === 75\n ? \"percent_75\"\n : \"percent_max\";\n\n if (pct !== 100) {\n const nextAmount = getExactOutPercentAmountFromBalance(\n toToken,\n pct,\n false\n );\n\n if (nextAmount) {\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleSendAmountChange(nextAmount.amount);\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setTxError(\"Unable to calculate this percentage for the send asset.\");\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n const maxAmount = getExactOutPercentAmountFromBalance(toToken, 100, false);\n if (runId !== maxPercentRunRef.current) return;\n if (!maxAmount) {\n setTxError(\"No transferable amount is available for this asset.\");\n return;\n }\n\n handleSendAmountChange(maxAmount.amount);\n };\n\n // ---------------------------------------------------------------------------\n // Render\n // ---------------------------------------------------------------------------\n const insufficientSourceIssue =\n swapQuoteIssue?.type === \"insufficientSources\" ? swapQuoteIssue : null;\n const configuredAmountIssue = buildConfiguredAmountIssue();\n const blockingQuoteIssue =\n insufficientSourceIssue ?? receiveAmountIssue ?? configuredAmountIssue;\n const isExactOutPaymentFlow =\n activeMode === \"deposit\" || activeMode === \"send\";\n const hasCurrentRunnableIntent = hasCurrentQuoteIntent;\n const hasIntentSources = Boolean((intentData?.sources ?? []).length > 0);\n const hasCurrentExactOutPaymentIntent =\n hasCurrentRunnableIntent &&\n (hasIntentSources ||\n (isExactOutPaymentFlow && Boolean(intentData?.destination)));\n const isExactOutRouteLoading =\n isExactOutPaymentFlow &&\n swapStep === \"idle\" &&\n swapType === \"exactOut\" &&\n Boolean(\n toToken && (receiveMaxCalculating || (amount && Number(amount) > 0))\n ) &&\n !blockingQuoteIssue &&\n !hasCurrentExactOutPaymentIntent &&\n (quoteRefreshing || intentLoading || receiveMaxCalculating);\n const isQuoteUnavailableForAutoSourceFlow =\n isExactOutPaymentFlow &&\n Boolean(hasPositiveDecimalInput(amount) && toToken) &&\n !quoteRefreshing &&\n !receiveMaxCalculating &&\n !intentLoading &&\n !blockingQuoteIssue &&\n !hasCurrentExactOutPaymentIntent;\n const hasPositiveRootAmount = hasPositiveDecimalInput(amount);\n const hasReadySwapQuoteInput = hasReadyExactInSwapInput(fromTokens, toToken);\n const needsWalletConnection = !ownerAddress || !nexusSDK;\n const isExactOutPaymentQuotePending =\n isExactOutPaymentFlow && (quoteRefreshing || intentLoading);\n const walletConnectBusy =\n walletActionPending ||\n nexusLoading ||\n isWalletConnectPending ||\n walletStatus === \"connecting\";\n const hasConnectWalletHandler = Boolean(\n onConnectClick || onConnectWallet || connectors.length > 0\n );\n const walletCtaLabel = hasConnectWalletHandler\n ? walletConnectBusy\n ? \"Connecting...\"\n : \"Connect Wallet\"\n : \"Connect your wallet to proceed\";\n const isSwapCtaDisabled = needsWalletConnection\n ? !hasConnectWalletHandler || walletConnectBusy\n : !hasReadySwapQuoteInput ||\n receiveMaxCalculating ||\n quoteRefreshing ||\n Boolean(blockingQuoteIssue);\n const isDepositCtaDisabled = needsWalletConnection\n ? !hasConnectWalletHandler || walletConnectBusy\n : !hasPositiveRootAmount ||\n !toToken ||\n receiveMaxCalculating ||\n isExactOutPaymentQuotePending ||\n (!hasCurrentExactOutPaymentIntent &&\n isQuoteUnavailableForAutoSourceFlow) ||\n Boolean(blockingQuoteIssue);\n const sendNeedsRecipient = activeMode === \"send\" && !recipientAddress;\n const isSendCtaDisabled = needsWalletConnection\n ? !hasConnectWalletHandler || walletConnectBusy\n : !hasPositiveRootAmount ||\n !toToken ||\n hasSameOwnerSendRecipient ||\n receiveMaxCalculating ||\n (!sendNeedsRecipient &&\n (isExactOutPaymentQuotePending ||\n (!hasCurrentExactOutPaymentIntent &&\n isQuoteUnavailableForAutoSourceFlow))) ||\n Boolean(blockingQuoteIssue);\n const quoteCtaLabel = (fallback: string) => {\n if (needsWalletConnection) return walletCtaLabel;\n if (insufficientSourceIssue) return \"Insufficient balance\";\n if (receiveAmountIssue) return receiveAmountIssue.ctaLabel;\n if (configuredAmountIssue) return configuredAmountIssue.ctaLabel;\n if (receiveMaxCalculating) return \"Calculating...\";\n if (\n isExactOutPaymentQuotePending ||\n (!hasCurrentExactOutPaymentIntent && (quoteRefreshing || intentLoading))\n ) {\n return \"Fetching quotes...\";\n }\n if (isQuoteUnavailableForAutoSourceFlow) return \"Quote unavailable\";\n if (!hasPositiveRootAmount) return \"Enter amount\";\n return fallback;\n };\n const sendCtaLabel = (() => {\n if (needsWalletConnection) return walletCtaLabel;\n if (insufficientSourceIssue) return \"Insufficient balance\";\n if (receiveAmountIssue) return receiveAmountIssue.ctaLabel;\n if (configuredAmountIssue) return configuredAmountIssue.ctaLabel;\n if (!hasPositiveRootAmount) return \"Enter amount\";\n if (!toToken) return \"Select token\";\n if (hasSameOwnerSendRecipient) return \"Change recipient\";\n if (sendNeedsRecipient) return \"Add recipient\";\n return quoteCtaLabel(\"Review send\");\n })();\n const previewIntentSourceUsdNumber = (intentData?.sources ?? []).reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0)\n );\n const previewSourceUsdNumber = previewIntentSourceUsdNumber.gt(0)\n ? previewIntentSourceUsdNumber\n : fromTokens.length > 0\n ? fromTokens.reduce(\n (sum, token) =>\n sum.plus(\n getTokenUsdValue(\n token,\n swapType === \"exactIn\" && fromTokens.length === 1\n ? amount\n : undefined\n )\n ),\n new Decimal(0)\n )\n : undefined;\n const previewExactOutDestinationAmount =\n activeMode === \"deposit\"\n ? depositTokenAmountForQuote\n : activeMode === \"send\"\n ? parseFiatNumber(amount)\n : undefined;\n const previewExactOutDestinationUsdNumber =\n activeMode === \"deposit\"\n ? depositUsdDecimal\n : activeMode === \"send\" && amount && toToken\n ? getTokenUsdValue(\n {\n ...toToken,\n userAmount: amount,\n userAmountMode: \"token\",\n },\n amount\n )\n : undefined;\n const previewDestinationUsdNumber =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n previewExactOutDestinationUsdNumber?.gt(0)\n ? previewExactOutDestinationUsdNumber\n : parseFiatNumber((intentData?.destination as any)?.value);\n const previewDestinationAmount =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n previewExactOutDestinationAmount?.gt(0)\n ? previewExactOutDestinationAmount\n .toDecimalPlaces(toToken?.decimals ?? 18, Decimal.ROUND_DOWN)\n .toFixed()\n : intentToAmount;\n const previewFromAmountUsd =\n previewSourceUsdNumber && previewSourceUsdNumber.gt(0)\n ? previewSourceUsdNumber.toDecimalPlaces(6).toFixed()\n : undefined;\n const previewToAmountUsd =\n previewDestinationUsdNumber && previewDestinationUsdNumber.gt(0)\n ? previewDestinationUsdNumber.toDecimalPlaces(6).toFixed()\n : undefined;\n const previewDestinationGasFeeUsd = (() => {\n const value = getIntentDestinationGasUsdValue(intentData);\n return value && value.gt(0)\n ? value.toDecimalPlaces(6, Decimal.ROUND_DOWN).toFixed()\n : undefined;\n })();\n const predictiveExactInQuote =\n predictiveQuote?.mode === \"exactIn\" &&\n predictiveQuote.key === getPredictiveQuoteCacheKey(\"swap\", \"exactIn\")\n ? predictiveQuote\n : null;\n const predictiveExactOutQuote =\n predictiveQuote?.mode === \"exactOut\" &&\n predictiveQuote.key === getPredictiveQuoteCacheKey(activeMode, \"exactOut\")\n ? predictiveQuote\n : null;\n const shouldUseCurrentExactOutIntentSources =\n isExactOutPaymentFlow && hasCurrentQuoteIntent && hasIntentSources;\n const currentExactOutIntentSourceTokens =\n shouldUseCurrentExactOutIntentSources\n ? sortSwapTokensByUsdDesc(\n (intentData?.sources ?? [])\n .map(buildIntentSourceToken)\n .filter(hasPositiveSourceSpend)\n )\n : [];\n const resolvedToToken =\n toToken ??\n (activeMode === \"deposit\" && selectedOpportunity\n ? toTokenFromOpportunity(selectedOpportunity)\n : undefined);\n const enrichedResolvedToToken = enrichTokenOptionLogo(resolvedToToken);\n const toTokenWithFetchedBalance =\n enrichedResolvedToToken && destinationBalance\n ? { ...enrichedResolvedToToken, balance: destinationBalance }\n : enrichedResolvedToToken;\n const idleReceiveQuoteAmount =\n activeMode === \"swap\" && swapType === \"exactIn\"\n ? (intentToAmount ?? predictiveExactInQuote?.toAmount)\n : undefined;\n const idleReceiveQuoteUsd =\n activeMode === \"swap\" && swapType === \"exactIn\"\n ? (previewToAmountUsd ?? predictiveExactInQuote?.toUsd)\n : previewToAmountUsd;\n const exactOutDestinationCoverage = getExactOutDestinationBalanceCoverage({\n requestedAmount: previewExactOutDestinationAmount,\n requestedUsd: previewExactOutDestinationUsdNumber,\n producedAmount: hasIntentSources\n ? parseFiatNumber(intentData?.destination?.amount)\n : undefined,\n producedUsd: hasIntentSources\n ? parseFiatNumber(intentData?.destination?.value)\n : undefined,\n token: toTokenWithFetchedBalance,\n });\n const destinationBalanceDisplayToken = buildDestinationBalanceDisplayToken(\n exactOutDestinationCoverage,\n toTokenWithFetchedBalance\n );\n const shouldShowPredictiveExactOutDisplay =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (quoteRefreshing || intentLoading) &&\n !hasIntentSources &&\n Boolean(\n predictiveExactOutQuote &&\n ((predictiveExactOutQuote.sources?.length ?? 0) > 0 ||\n destinationBalanceDisplayToken)\n );\n const baseDisplayFromTokens = shouldUseCurrentExactOutIntentSources\n ? currentExactOutIntentSourceTokens\n : shouldShowPredictiveExactOutDisplay\n ? (predictiveExactOutQuote?.sources ?? fromTokens)\n : fromTokens;\n const displayFromTokens = (() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n return baseDisplayFromTokens;\n }\n if (!destinationBalanceDisplayToken) {\n return mergeDisplaySourceTokens(\n sortDisplaySourcesByBalanceUsdDesc(baseDisplayFromTokens)\n );\n }\n\n const destinationKey = getTokenSelectionKey(destinationBalanceDisplayToken);\n let replacedEmptyDestinationToken = false;\n const tokens = baseDisplayFromTokens.map((token) => {\n const isDestinationToken = getTokenSelectionKey(token) === destinationKey;\n if (\n isDestinationToken &&\n !hasPositiveDecimalInput(token.userAmount) &&\n !hasPositiveDecimalInput(token.userAmountUsd)\n ) {\n replacedEmptyDestinationToken = true;\n return destinationBalanceDisplayToken;\n }\n return token;\n });\n\n const displayTokens = replacedEmptyDestinationToken\n ? tokens\n : [...tokens, destinationBalanceDisplayToken];\n return mergeDisplaySourceTokens(\n sortDisplaySourcesByBalanceUsdDesc(displayTokens)\n );\n })();\n const displayExactOutRouteLoading =\n isExactOutRouteLoading && !shouldShowPredictiveExactOutDisplay;\n const isSourcePickerRefreshDisabled =\n quoteRefreshing || intentLoading || previewQuoteRefreshing;\n const isIntentSourceUiActionable =\n activeMode === \"deposit\"\n ? !isDepositCtaDisabled\n : activeMode === \"send\"\n ? !isSendCtaDisabled\n : !isSwapCtaDisabled;\n\n useEffect(() => {\n if (!intentFetchTiming?.endTimeMs) {\n return;\n }\n if (latestIntentFetchRunIdRef.current !== intentFetchTiming.runId) {\n return;\n }\n if (intentUiRenderLoggedRunIdRef.current === intentFetchTiming.runId) {\n return;\n }\n const hasRenderedSources =\n displayFromTokens.length > 0 ||\n hasIntentSources ||\n Boolean(intentData?.destination);\n if (!hasRenderedSources) return;\n if (\n isSwapBalancePending ||\n isSourcePickerRefreshDisabled ||\n quoteRefreshing ||\n intentLoading ||\n previewQuoteRefreshing ||\n receiveMaxCalculating ||\n displayExactOutRouteLoading ||\n !isIntentSourceUiActionable\n ) {\n return;\n }\n\n const renderTimeMs = Date.now();\n const renderDurationMs = renderTimeMs - intentFetchTiming.startTimeMs;\n const fetchDurationMs =\n intentFetchTiming.fetchDurationMs ??\n intentFetchTiming.endTimeMs - intentFetchTiming.startTimeMs;\n const differenceMs = renderDurationMs - fetchDurationMs;\n\n console.log(\"[NexusWidget] intent sources UI render complete\", {\n runId: intentFetchTiming.runId,\n walletAddress: intentFetchTiming.walletAddress,\n operation: intentFetchTiming.operation,\n quoteInputKey: intentFetchTiming.quoteInputKey,\n sourceCount: displayFromTokens.length,\n dateNow: renderTimeMs,\n date: new Date(renderTimeMs),\n totalRenderTimeSeconds: renderDurationMs / 1000,\n elapsedMs: renderDurationMs,\n });\n console.log(\"[NexusWidget] intent render/fetch difference\", {\n runId: intentFetchTiming.runId,\n walletAddress: intentFetchTiming.walletAddress,\n operation: intentFetchTiming.operation,\n quoteInputKey: intentFetchTiming.quoteInputKey,\n differenceMs,\n dateNow: renderTimeMs,\n date: new Date(renderTimeMs),\n });\n intentUiRenderLoggedRunIdRef.current = intentFetchTiming.runId;\n }, [\n displayExactOutRouteLoading,\n displayFromTokens.length,\n hasIntentSources,\n intentLoading,\n intentData?.destination,\n intentFetchTiming,\n isDepositCtaDisabled,\n isIntentSourceUiActionable,\n isSendCtaDisabled,\n isSourcePickerRefreshDisabled,\n isSwapCtaDisabled,\n isSwapBalancePending,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n ]);\n\n const totalSwapBalanceUsd = getActiveTotalBalanceUsd()\n .toDecimalPlaces(2)\n .toFixed();\n const requestedOutputUsd =\n amount && toToken\n ? getTokenUsdValue(\n {\n ...toToken,\n userAmount: amount,\n userAmountMode: \"token\",\n },\n amount\n )\n : new Decimal(0);\n const sendAmountUsd = requestedOutputUsd.toNumber();\n const exactOutRequiredUsdAmount = (() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return undefined;\n\n const issueMissingUsd = parseFiatNumber(insufficientSourceIssue?.missingUsd);\n if (issueMissingUsd && issueMissingUsd.gt(0)) {\n return getExactOutAvailableSourceUsd().plus(issueMissingUsd);\n }\n\n const requiredFundingUsd = getExactOutRequiredFundingUsd();\n if (requiredFundingUsd?.gt(0)) return requiredFundingUsd;\n\n if (requestedOutputUsd.gt(0)) return requestedOutputUsd;\n\n return undefined;\n })();\n const exactOutRequiredUsdDisplay = exactOutRequiredUsdAmount\n ?.toDecimalPlaces(2)\n .toFixed();\n const assetSelectorRequiredUsdDisplay =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (swapStep === \"choose-swap-asset\" ||\n closingDrawerStep === \"choose-swap-asset\")\n ? (sourceSelectionRequiredUsdDisplay ?? exactOutRequiredUsdDisplay)\n : exactOutRequiredUsdDisplay;\n const captureSourceSelectionRequiredUsd = () => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n setSourceSelectionRequiredUsdDisplay(exactOutRequiredUsdDisplay);\n };\n const isIdleSwapQuoteLoading =\n activeMode === \"swap\" &&\n swapStep === \"idle\" &&\n (quoteRefreshing || intentLoading);\n const isReceiveAmountLoading =\n receiveMaxCalculating ||\n (isIdleSwapQuoteLoading &&\n swapType === \"exactIn\" &&\n !idleReceiveQuoteAmount);\n const isReceiveUsdLoading =\n receiveMaxCalculating ||\n (isIdleSwapQuoteLoading && swapType === \"exactIn\" && !idleReceiveQuoteUsd);\n const hasQuoteRefreshCountdown =\n (activeMode === \"swap\" ||\n activeMode === \"deposit\" ||\n activeMode === \"send\") &&\n hasCurrentQuoteIntent &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n const isRecipientDrawerClosing = closingDrawerStep === \"enter-recipient\";\n const isSwapAssetDrawerClosing = closingDrawerStep === \"choose-swap-asset\";\n const isReceiveAssetDrawerClosing =\n closingDrawerStep === \"choose-receive-asset\";\n const isReceiveAssetPickerLocked =\n activeMode === \"deposit\"\n ? configuredDepositOptions.length <= 1\n : (config.allowedDestinationPairs?.length ?? 0) === 1;\n const hideDestinationTokenDropdownIcon =\n activeMode === \"deposit\"\n ? configuredDepositOptions.length === 1\n : (config.allowedDestinationPairs?.length ?? 0) === 1;\n const isTokenAssetDrawerActive =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n closingDrawerStep === \"choose-swap-asset\" ||\n closingDrawerStep === \"choose-receive-asset\";\n const configuredReceiveTokenCount =\n activeMode === \"deposit\" || activeMode === \"send\"\n ? Math.max(\n configuredDestinationTokenOptions.length,\n config.allowedDestinationPairs?.length ?? 0\n )\n : 0;\n const isConfiguredReceiveAssetDrawerActive =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (swapStep === \"choose-receive-asset\" ||\n closingDrawerStep === \"choose-receive-asset\") &&\n Boolean(config.allowedDestinationPairs?.length);\n const tokenAssetDrawerMinRootContentHeight =\n isConfiguredReceiveAssetDrawerActive\n ? getConfiguredReceiveSelectorRootHeight(configuredReceiveTokenCount)\n : isTokenAssetDrawerActive\n ? TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT\n : 0;\n const isDrawerOverlayActive =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n swapStep === \"enter-recipient\" ||\n closingDrawerStep !== null;\n const isPreviewIntentScreen = swapStep === \"preview-intent\";\n const displayedRootContentHeight =\n hasMeasuredRootContent && rootContentHeight\n ? Math.max(\n rootContentHeight,\n tokenAssetDrawerMinRootContentHeight\n )\n : null;\n\n const widgetContent = (\n {\n const transitions = [];\n if (hasMeasuredRootContent && shouldAnimateRootHeight) {\n transitions.push(`height ${ROOT_HEIGHT_TRANSITION_MS}ms ease-out`);\n }\n return transitions.join(\", \");\n })(),\n willChange: \"height\",\n width: \"450px\",\n maxWidth: \"100%\",\n minWidth: \"320px\",\n WebkitFontSmoothing: \"antialiased\",\n MozOsxFontSmoothing: \"grayscale\",\n }}\n >\n \n \n \n {canGoBack && (\n \n \n \n )}\n \n {getTitle()}\n \n\n {/* Sub-screen asset counts */}\n {!isTitleCentered() &&\n activeMode === \"swap\" &&\n swapStep === \"choose-swap-asset\" &&\n swapType === \"exactIn\" && (\n \n {fromTokens.length} asset(s) selected\n \n )}\n \n\n {/* Right side icons */}\n \n {hasQuoteRefreshCountdown && (\n \n )}\n {showHistoryButton && (\n setSwapStep(\"history\")}\n style={{\n alignItems: \"center\",\n backgroundColor: theme.primitives.iconButton.backgroundColor,\n borderColor: theme.primitives.iconButton.borderColor,\n borderRadius: theme.radius.iconButton,\n borderStyle: \"solid\",\n borderWidth: \"1px\",\n boxShadow: theme.primitives.iconButton.boxShadow,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n height: \"28px\",\n justifyContent: \"center\",\n width: \"28px\",\n cursor: \"pointer\",\n padding: 0,\n }}\n >\n \n \n \n \n \n \n )}\n {showCloseButton && (\n \n \n \n \n \n )}\n \n \n\n {/* ------------------------------------------------------------------ */}\n {/* Main content area */}\n {/* ------------------------------------------------------------------ */}\n \n {/* =============================================================== */}\n {/* SHARED SUB-SCREENS (non-drawer panels) */}\n {/* =============================================================== */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep !== \"idle\" &&\n swapStep !== \"choose-swap-asset\" &&\n swapStep !== \"choose-receive-asset\" &&\n swapStep !== \"enter-recipient\" && (\n <>\n {/* Panel: preview. */}\n {swapStep === \"preview-intent\" && (\n \n {\n clearPendingSwapIntent();\n setSwapStep(\"idle\");\n }}\n onTransitionChange={setIsPreviewTransitioning}\n opportunity={selectedOpportunity}\n recipientAddress={transferRecipientAddress}\n steps={steps}\n supportedTokenAssets={supportedChainsAndTokens}\n swapBalances={swapBalance}\n swapType={swapType}\n toAmount={previewDestinationAmount}\n toAmountTokens={\n previewDestinationAmount\n ? `${previewDestinationAmount}`\n : undefined\n }\n toAmountUsd={previewToAmountUsd}\n toToken={toTokenWithFetchedBalance}\n totalFeeUsd={intentFeeUsd}\n />\n \n )}\n\n {swapStep === \"progress\" && (\n \n )}\n\n {(swapStep === \"success\" || swapStep === \"failed\") &&\n currentSwapEntry && (\n
\n \n
\n )}\n \n )}\n\n {/* =============================================================== */}\n {/* HISTORY SCREEN */}\n {/* =============================================================== */}\n {swapStep === \"history\" && (\n \n )}\n\n {/* =============================================================== */}\n {/* SWAP IDLE SCREEN */}\n {/* =============================================================== */}\n {activeMode === \"swap\" &&\n [\n \"idle\",\n \"choose-swap-asset\",\n \"choose-receive-asset\",\n \"enter-recipient\",\n ].includes(swapStep) && (\n <>\n {\n handleSwapAmountChange(val, panel);\n }}\n onOpenDestPicker={() =>\n openDrawerStep(\"choose-receive-asset\")\n }\n onOpenRecipientPicker={handleOpenRecipientEditor}\n onOpenSourcePicker={(index) => {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n setEditingAssetIndex(index ?? null);\n beginSourcePickerEdit();\n openDrawerStep(\"choose-swap-asset\");\n }}\n onUpdateTokens={handleSwapTokensUpdate}\n receiveQuoteAmount={\n swapType === \"exactIn\" ? idleReceiveQuoteAmount : undefined\n }\n receiveQuoteUsd={idleReceiveQuoteUsd}\n recipientAddress={effectiveRecipientAddress}\n sourceRouteMessage={insufficientSourceIssue?.message}\n sourceRouteStatus={\n insufficientSourceIssue\n ? \"insufficient\"\n : isExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n swapType={swapType}\n toToken={toTokenWithFetchedBalance}\n totalBalance={totalSwapBalanceUsd}\n usdValue={amount && usdValue > 0 ? usdValue.toFixed(2) : \"\"}\n />\n\n {receiveAmountIssue && (\n \n )}\n\n {configuredAmountIssue && (\n \n )}\n\n {txError && !blockingQuoteIssue && (\n \n )}\n\n {/* CTA Button */}\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n void handleEnterPreview();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: blockingQuoteIssue\n ? \"#FCEEED\"\n : isSwapCtaDisabled\n ? theme.colors.surfaceCool\n : primaryButtonBackground,\n border: blockingQuoteIssue ? \"1px solid #F7C4C1\" : \"none\",\n borderRadius: theme.radius.primaryButton,\n boxShadow:\n blockingQuoteIssue || isSwapCtaDisabled\n ? \"none\"\n : theme.shadows.primaryButton,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"7px\",\n height: \"42px\",\n justifyContent: \"center\",\n marginTop: \"8px\",\n paddingInline: \"16px\",\n cursor: isSwapCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {blockingQuoteIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n quoteRefreshing ||\n receiveMaxCalculating ? (\n\t \n ) : null}\n \n {needsWalletConnection\n ? walletCtaLabel\n : fromTokens.length === 0\n ? \"Add Assets to Bridge\"\n : quoteCtaLabel(\"Review swap\")}\n \n \n \n \n )}\n\n {/* =============================================================== */}\n {/* DEPOSIT MODE LAYOUT */}\n {/* =============================================================== */}\n {activeMode === \"deposit\" &&\n [\n \"idle\",\n \"choose-swap-asset\",\n \"choose-receive-asset\",\n \"enter-recipient\",\n ].includes(swapStep) && (\n <>\n {swapStep === \"idle\" && depositFundingStep === \"method\" && (\n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n setDepositFundingStep(\"onramp\");\n }}\n onSelectWallet={() => setDepositFundingStep(\"wallet\")}\n primaryButtonForeground={primaryButtonForeground}\n totalBalance={totalSwapBalanceUsd}\n />\n )}\n\n {swapStep === \"idle\" && depositFundingStep === \"onramp\" && (\n {\n setTxError(message);\n onError?.(message);\n }}\n onSelectDestinationToken={(token) => {\n const tokenChanged = !isSameTokenSelection(toToken, token);\n if (tokenChanged) {\n onReceiveAssetChange?.({\n chainId: token.chainId,\n chainName: token.chainName,\n contractAddress: token.contractAddress,\n symbol: token.symbol,\n });\n }\n const nextDeposit = getDepositForTokenSelection(\n configuredDepositOptions,\n token\n );\n if (nextDeposit) {\n setSelectedOpportunity(nextDeposit);\n }\n setToToken(token);\n }}\n onSessionStateChange={setDepositOnrampSessionState}\n ownerAddress={ownerAddress}\n opportunity={selectedOpportunity}\n primaryButtonForeground={primaryButtonForeground}\n toToken={toTokenWithFetchedBalance}\n />\n )}\n\n {selectedOpportunity && depositFundingStep === \"wallet\" && (\n <>\n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n captureSourceSelectionRequiredUsd();\n beginSourcePickerEdit();\n openDrawerStep(\"choose-swap-asset\");\n }}\n onOpenTokenPicker={() =>\n openDrawerStep(\"choose-receive-asset\")\n }\n onSetPercent={handleDepositPercentSelect}\n routeMessage={insufficientSourceIssue?.message}\n routeStatus={\n insufficientSourceIssue\n ? \"insufficient\"\n : displayExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n showAutoBadge={!sourceSelectionTouched}\n isTokenPickerDisabled={isReceiveAssetPickerLocked}\n hideDestinationTokenDropdownIcon={\n hideDestinationTokenDropdownIcon\n }\n tokenValue={depositTokenDisplay}\n toToken={toTokenWithFetchedBalance}\n totalBalance={totalSwapBalanceUsd}\n usdValue={depositUsdDisplay}\n />\n\n {receiveAmountIssue && (\n \n )}\n\n {configuredAmountIssue && (\n \n )}\n\n {txError && !blockingQuoteIssue && (\n \n )}\n\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n void handleEnterPreview();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: blockingQuoteIssue\n ? \"#FCEEED\"\n : isDepositCtaDisabled\n ? theme.colors.surfaceCool\n : primaryButtonBackground,\n border: blockingQuoteIssue\n ? \"1px solid #F7C4C1\"\n : \"none\",\n borderRadius: blockingQuoteIssue\n ? \"4px\"\n : theme.radius.primaryButton,\n boxShadow:\n blockingQuoteIssue || isDepositCtaDisabled\n ? \"none\"\n : theme.shadows.primaryButton,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"7px\",\n height: \"40px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n cursor: isDepositCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {blockingQuoteIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n isExactOutPaymentQuotePending ||\n receiveMaxCalculating ? (\n\t \n ) : null}\n \n {quoteCtaLabel(\"Review deposit\")}\n \n \n \n \n )}\n \n )}\n\n {/* =============================================================== */}\n {/* SEND MODE — recipient first, then amount, then asset */}\n {/* =============================================================== */}\n {activeMode === \"send\" &&\n [\n \"idle\",\n \"choose-swap-asset\",\n \"choose-receive-asset\",\n \"enter-recipient\",\n ].includes(swapStep) && (\n <>\n \n openDrawerStep(\"choose-receive-asset\")\n }\n isRecipientLocked={isConfiguredRecipientLocked}\n onOpenRecipientPicker={\n isConfiguredRecipientLocked\n ? () => undefined\n : handleOpenRecipientEditor\n }\n onOpenSourcePicker={() => {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n setEditingAssetIndex(null);\n captureSourceSelectionRequiredUsd();\n beginSourcePickerEdit();\n openDrawerStep(\"choose-swap-asset\");\n }}\n onSetPercent={handleSendPercentSelect}\n recipientAddress={recipientAddress || \"\"}\n routeMessage={insufficientSourceIssue?.message}\n routeStatus={\n insufficientSourceIssue\n ? \"insufficient\"\n : displayExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n showAutoBadge={!sourceSelectionTouched}\n toToken={toTokenWithFetchedBalance}\n totalBalance={totalSwapBalanceUsd}\n usdValue={\n amount && sendAmountUsd > 0 ? sendAmountUsd.toFixed(2) : \"\"\n }\n />\n\n {receiveAmountIssue && (\n \n )}\n\n {configuredAmountIssue && (\n \n )}\n\n {txError && !blockingQuoteIssue && (\n \n )}\n\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n if (sendNeedsRecipient) {\n handleOpenRecipientEditor();\n return;\n }\n void handleEnterPreview();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: blockingQuoteIssue\n ? \"#FCEEED\"\n : isSendCtaDisabled\n ? theme.colors.surfaceCool\n : primaryButtonBackground,\n border: blockingQuoteIssue ? \"1px solid #F7C4C1\" : \"none\",\n borderRadius: blockingQuoteIssue\n ? \"4px\"\n : theme.radius.primaryButton,\n boxShadow:\n blockingQuoteIssue || isSendCtaDisabled\n ? \"none\"\n : theme.shadows.primaryButton,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"7px\",\n height: \"40px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n cursor: isSendCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {blockingQuoteIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n (!sendNeedsRecipient &&\n (isExactOutPaymentQuotePending ||\n receiveMaxCalculating)) ? (\n\t \n ) : null}\n \n {sendCtaLabel}\n \n \n \n \n )}\n \n \n\n {/* ================================================================== */}\n {/* DRAWER PANELS — rendered as direct children of root widget */}\n {/* so they overlay the main page as bottom drawers */}\n {/* ================================================================== */}\n\n {/* Drawer: enter-recipient */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep === \"enter-recipient\" && (\n \n {\n setTxError(null);\n closeDrawerToIdle();\n }}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n backgroundColor: \"rgba(0,0,0,0.35)\",\n pointerEvents: \"auto\",\n opacity: isRecipientDrawerClosing ? 0 : 1,\n transition: `opacity ${DRAWER_CLOSE_MS}ms ease`,\n }}\n />\n \n \n \n \n \n {\n setTxError(null);\n closeDrawerToIdle();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: theme.colors.surface,\n border: `1px solid ${theme.colors.border}`,\n borderRadius: \"8px\",\n cursor: \"pointer\",\n display: \"flex\",\n flexShrink: 0,\n height: \"32px\",\n justifyContent: \"center\",\n padding: 0,\n width: \"32px\",\n }}\n type=\"button\"\n >\n \n \n \n Recipient\n \n \n \n \n \n Wallet Address\n \n {activeMode === \"swap\" && defaultRecipientAddress && (\n \n Reset to default\n \n )}\n \n {\n setRecipientAddress(next);\n if (txError) setTxError(null);\n }}\n onClear={() => setRecipientAddress(\"\")}\n placeholder=\"Wallet address\"\n value={recipientAddress}\n />\n {txError && (\n \n {txError}\n \n )}\n {activeMode === \"send\" && (\n \n Recipient must be different from the connected wallet.\n \n )}\n \n Save\n \n \n \n )}\n\n {/* Drawer: choose-swap-asset */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep === \"choose-swap-asset\" && (\n \n \n \n {\n if (activeMode === \"swap\") {\n const next = [...fromTokens];\n const targetIndex =\n editingAssetIndex !== null &&\n editingAssetIndex < next.length\n ? editingAssetIndex\n : null;\n const existingToken =\n targetIndex !== null ? next[targetIndex] : undefined;\n const tokenChanged = !isSameTokenSelection(\n existingToken,\n token\n );\n const preservedAmount = tokenChanged\n ? \"\"\n : existingToken?.userAmount ||\n (targetIndex === 0 ? amount : \"\");\n const newToken = {\n ...token,\n userAmount: preservedAmount,\n };\n\n if (targetIndex !== null) {\n next[targetIndex] = newToken;\n } else {\n next.push(newToken);\n }\n\n if (tokenChanged) {\n clearPendingSwapIntent();\n setAmount(getSourceAmountInput(next));\n }\n if (swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n setFromTokens(next);\n closeDrawerToIdle();\n } else if (\n activeMode === \"deposit\" ||\n activeMode === \"send\"\n ) {\n handleSourcePickerDraftSelectionChange([\n { ...token, userAmount: \"\" },\n ]);\n }\n }}\n onSelectionChange={\n activeMode === \"deposit\" || activeMode === \"send\"\n ? handleSourcePickerDraftSelectionChange\n : undefined\n }\n onToggle={(token) => {\n const isDepositOrSendSourcePicker =\n activeMode === \"deposit\" || activeMode === \"send\";\n const prevTokens = isDepositOrSendSourcePicker\n ? sourcePickerSelectedTokens\n : fromTokens;\n const isSameSelection = (\n a: SwapTokenOption,\n b: SwapTokenOption\n ) => {\n if (a.isUnified || b.isUnified) {\n return Boolean(\n a.isUnified &&\n b.isUnified &&\n a.unifiedSymbol === b.unifiedSymbol\n );\n }\n return (\n a.contractAddress.toLowerCase() ===\n b.contractAddress.toLowerCase() && a.chainId === b.chainId\n );\n };\n const sourceTokens = token.sourceTokens ?? [];\n const isSameUnifiedGroup = (item: SwapTokenOption) =>\n Boolean(\n item.isUnified &&\n token.isUnified &&\n item.unifiedSymbol === token.unifiedSymbol\n );\n const withDefaultAmount = (item: SwapTokenOption) => ({\n ...item,\n userAmount:\n activeMode === \"swap\" && prevTokens.length === 0\n ? amount\n : \"\",\n });\n const getNextTokens = () => {\n if (\n isDepositOrSendSourcePicker &&\n token.isUnified &&\n sourceTokens.length > 0\n ) {\n const hasUnifiedSelection =\n prevTokens.some(isSameUnifiedGroup);\n const areAllChildrenSelected = sourceTokens.every(\n (source) =>\n prevTokens.some((item) =>\n isSameSelection(item, source)\n )\n );\n const withoutGroup = prevTokens.filter(\n (item) =>\n !isSameUnifiedGroup(item) &&\n !sourceTokens.some((source) =>\n isSameSelection(item, source)\n )\n );\n\n if (hasUnifiedSelection || areAllChildrenSelected) {\n return withoutGroup;\n }\n\n return [\n ...withoutGroup,\n ...sourceTokens.map((source) =>\n withDefaultAmount(source)\n ),\n ];\n }\n\n if (isDepositOrSendSourcePicker && !token.isUnified) {\n const unifiedSelection = prevTokens.find(\n (item) =>\n item.isUnified &&\n item.sourceTokens?.some((source) =>\n isSameSelection(source, token)\n )\n );\n\n if (unifiedSelection?.sourceTokens?.length) {\n const withoutUnified = prevTokens.filter(\n (item) => !isSameSelection(item, unifiedSelection)\n );\n return [\n ...withoutUnified,\n ...unifiedSelection.sourceTokens\n .filter((source) => !isSameSelection(source, token))\n .map((source) => withDefaultAmount(source)),\n ];\n }\n }\n\n const exists = prevTokens.find((item) =>\n isSameSelection(item, token)\n );\n if (exists) {\n return prevTokens.filter(\n (item) => !isSameSelection(item, token)\n );\n }\n const tokenSourceKeys = new Set(\n (token.sourceTokens ?? []).map(\n (source) =>\n `${source.chainId}-${source.contractAddress.toLowerCase()}`\n )\n );\n const next = prevTokens.filter((existing) => {\n if (\n token.isUnified &&\n tokenSourceKeys.has(\n `${existing.chainId}-${existing.contractAddress.toLowerCase()}`\n )\n ) {\n return false;\n }\n if (\n existing.isUnified &&\n existing.sourceTokens?.some(\n (source) =>\n source.chainId === token.chainId &&\n source.contractAddress.toLowerCase() ===\n token.contractAddress.toLowerCase()\n )\n ) {\n return false;\n }\n return true;\n });\n return [...next, withDefaultAmount(token)];\n };\n const nextTokens = getNextTokens();\n\n if (isDepositOrSendSourcePicker) {\n handleSourcePickerDraftSelectionChange(nextTokens);\n } else {\n clearPendingSwapIntent();\n setFromTokens(nextTokens);\n }\n }}\n preserveSelectedBelowMinimum={false}\n requiredUsd={assetSelectorRequiredUsdDisplay}\n selectedTokens={sourcePickerSelectedTokens}\n showBelowMinimumInline={\n activeMode === \"swap\" && swapType === \"exactIn\"\n }\n swapBalance={swapBalance}\n swapSupportedChains={swapSupportedChainsAndTokens}\n title={\n activeMode === \"deposit\" || activeMode === \"send\"\n ? \"Choose Assets to Pay with\"\n : \"Choose assets to send\"\n }\n />\n \n \n )}\n\n {/* Drawer: choose-receive-asset */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep === \"choose-receive-asset\" && (\n \n \n \n {\n const tokenChanged = !isSameTokenSelection(toToken, token);\n if (tokenChanged) {\n onReceiveAssetChange?.({\n chainId: token.chainId,\n chainName: token.chainName,\n contractAddress: token.contractAddress,\n symbol: token.symbol,\n });\n }\n if (activeMode === \"send\" || activeMode === \"deposit\") {\n setExactOutQuoteSourceModeValue(\"all\");\n if (tokenChanged) {\n clearPendingSwapIntent();\n if (!isConfiguredAmountFixed) {\n setAmount(\"\");\n }\n }\n setSwapType(\"exactOut\");\n if (activeMode === \"deposit\") {\n const nextDeposit = getDepositForTokenSelection(\n configuredDepositOptions,\n token\n );\n if (nextDeposit) {\n setSelectedOpportunity(nextDeposit);\n }\n }\n setToToken(token);\n closeDrawerToIdle();\n return;\n }\n if (tokenChanged) {\n clearPendingSwapIntent();\n }\n if (swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n setToToken(token);\n closeDrawerToIdle();\n }}\n />\n \n \n )}\n \n );\n\n if (embed) return widgetContent;\n\n return (\n \n \n \n \n \n {widgetContent}\n \n \n );\n}\n\nexport default NexusWidget;\n", + "content": "// biome-ignore-all lint: NexusWidget registry component from shadcn registry.\n\"use client\";\n\nimport { ERROR_CODES, type EthereumProvider } from \"@avail-project/nexus-core\";\nimport Decimal from \"decimal.js\";\nimport { AlertCircle, ArrowLeft, ChevronDown, Loader2 } from \"lucide-react\";\nimport React, {\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { flushSync } from \"react-dom\";\nimport {\n createPublicClient,\n encodeFunctionData,\n erc20Abi,\n http,\n isAddress,\n parseUnits,\n zeroAddress,\n} from \"viem\";\nimport { mainnet } from \"viem/chains\";\nimport { normalize } from \"viem/ens\";\nimport {\n useAccount,\n useConnect,\n useConnectorClient,\n usePublicClient,\n useWalletClient,\n} from \"wagmi\";\nimport { ErrorBoundary } from \"../common/components/ErrorBoundary\";\nimport { useTransactionSteps } from \"../common/tx/useTransactionSteps\";\nimport type {\n BridgeStepType,\n SwapStepType,\n} from \"../common/types/transaction-flow\";\nimport {\n CHAIN_METADATA,\n getShortChainName,\n isSwapSupportedBySdkChainList,\n SUPPORTED_CHAINS,\n TOKEN_CONTRACT_ADDRESSES,\n TOKEN_METADATA,\n} from \"../common/utils/constant\";\nimport { type UserAsset, useNexus } from \"../nexus/NexusProvider\";\nimport { Button } from \"../ui/button\";\nimport { Dialog, DialogContent, DialogTrigger } from \"../ui/dialog\";\nimport { DepositFundingMethod } from \"./components/deposit-funding-method\";\nimport { DepositIdleForm } from \"./components/deposit-idle-form\";\nimport { DepositOnrampFlow } from \"./components/deposit-onramp-flow\";\nimport {\n type NexusWidgetProgressEvent,\n NexusWidgetProgressScreen,\n} from \"./components/nexus-widget-progress-screen\";\nimport {\n getCachedReceiveTokenMatch,\n preloadReceiveTokens,\n ReceiveAssetSelector,\n} from \"./components/receive-asset-selector\";\nimport { RecipientInput } from \"./components/recipient-input\";\nimport { SendIdleForm } from \"./components/send-idle-form\";\nimport { StatusAlert } from \"./components/status-alerts\";\nimport {\n deriveTokenOptions,\n SwapAssetSelector,\n type SwapTokenOption,\n} from \"./components/swap-asset-selector\";\nimport { SwapIdleForm } from \"./components/swap-idle-form\";\nimport {\n type BridgeProvider,\n type SwapIntentData,\n type SwapIntentDestination,\n type SwapIntentSource,\n SwapIntentPreview,\n} from \"./components/swap-intent-preview\";\nimport {\n NEXUS_WIDGET_DEFAULT_PRIMARY_COLOR,\n NEXUS_WIDGET_FAST_SPINNER_STYLE,\n nexusWidgetTheme,\n} from \"./theme\";\nimport {\n type NexusWidgetAppearance,\n type NexusWidgetConfig,\n type NexusWidgetDepositConfig,\n type NexusWidgetDestination,\n type NexusWidgetDestinationToken,\n type NexusWidgetDepositOpportunityConfig,\n type NexusWidgetDepositOpportunityMetadata,\n type NexusWidgetMode,\n type NexusWidgetPrefillToken,\n type NexusWidgetProps,\n type NexusWidgetRuntimePrefill,\n type SwapType,\n} from \"./types\";\nimport { findCitreaReceiveToken } from \"./utils/citrea-tokens\";\nimport {\n type DepositSourceFilter,\n getDepositSourceId,\n resolveDepositSourceSelection,\n} from \"./utils/deposit-source-selection\";\n\n// ---------------------------------------------------------------------------\n// Types for swap step machine\n// ---------------------------------------------------------------------------\n\ntype SwapStep =\n | \"idle\" // main screen\n | \"choose-swap-asset\" // pick source token\n | \"choose-receive-asset\" // pick receive token\n | \"enter-recipient\" // pick recipient (send mode)\n | \"preview-intent\" // intent preview card\n | \"progress\" // transaction in flight\n | \"success\" // completed seamlessly\n | \"failed\" // failed swap receipt\n | \"history\"; // transaction history\n\ntype DepositFundingStep = \"method\" | \"wallet\" | \"onramp\";\n\ntype SourceFilterTab = \"all\" | \"native\" | \"stables\";\n\ntype SwapHistoryStatus =\n | \"pending\"\n | \"fulfilled\"\n | \"failed\"\n | \"timeout\"\n | \"refund-initiated\";\n\ninterface SwapHistoryEntry {\n autoRefundAvailable?: boolean;\n createdAt: number;\n durationSeconds?: number;\n endedAt?: number;\n error?: string;\n failedStepType?: string;\n failureDescription?: string;\n failureMessage?: string;\n feeUsd?: string;\n finalExplorerUrl?: string | null;\n fromTokens: SwapTokenOption[];\n id: string;\n intentData: SwapIntentData | null;\n intentExplorerUrl?: string | null;\n intentId?: number;\n mode: NexusWidgetMode;\n opportunity?: NexusWidgetDepositOpportunityMetadata;\n recipientAddress?: string;\n requestedToAmount?: string;\n requestedToValue?: string;\n sourceExplorerUrl?: string | null;\n startedAt: number;\n status: SwapHistoryStatus;\n toToken?: SwapTokenOption;\n}\n\ntype HistorySourceRow = {\n amount: string;\n chainId?: number;\n chainLogo?: string;\n chainName: string;\n contractAddress?: string;\n key: string;\n symbol: string;\n tokenLogo?: string;\n value?: unknown;\n};\n\ntype SwapQuoteIssue = {\n type: \"insufficientSources\";\n message: string;\n missingUsd?: string;\n};\n\ntype ReceiveAmountIssue = {\n ctaLabel: string;\n message: string;\n type:\n | \"configuredAmountLimit\"\n | \"receiveLimitExceeded\"\n | \"unpricedReceiveToken\";\n};\n\ntype CachedIntentUsdRate = {\n amount: string;\n rate: string;\n updatedAt: number;\n value: string;\n};\n\ntype PredictiveQuote = {\n key: string;\n mode: \"exactIn\" | \"exactOut\";\n sources?: SwapTokenOption[];\n toAmount?: string;\n toUsd?: string;\n};\n\ntype IntentFetchTiming = {\n background: boolean;\n endTimeMs?: number;\n fetchDurationMs?: number;\n operation: string;\n quoteInputKey: string;\n runId: number;\n sourceCount?: number;\n startTimeMs: number;\n walletAddress?: string | null;\n};\n\ntype PredictiveQuoteBaseline = {\n destinationUsdRate: string;\n exactInDestinationAmountPerSourceUsd?: string;\n exactOutSourceUsdPerDestinationUsd?: string;\n updatedAt: number;\n};\n\nconst DESTINATION_RECEIVE_LIMIT_USD_BY_CHAIN_ID: Record = {\n [SUPPORTED_CHAINS.MEGAETH]: 10_000,\n [SUPPORTED_CHAINS.CITREA]: 2000,\n [SUPPORTED_CHAINS.SCROLL]: 500,\n};\n\nconst SCIENTIFIC_DECIMAL_REGEX = /^-?(?:\\d+\\.?\\d*|\\.\\d+)e[+-]?\\d+$/i;\n\nconst QUOTE_REFRESH_INTERVAL_MS = 30000;\nconst EXACT_OUT_INPUT_DEBOUNCE_MS = 1300;\nconst DRAWER_CLOSE_MS = 220;\nconst BALANCE_REFRESH_AFTER_TERMINAL_MS = 3000;\nconst MODAL_HEIGHT_TRANSITION_MS = 220;\nconst ROOT_HEIGHT_TRANSITION_MS = 140;\nconst ASSET_SELECTOR_DRAWER_HEIGHT = \"90%\";\nconst TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT = 620;\nconst CONFIGURED_RECEIVE_SELECTOR_BASE_HEIGHT = 170;\nconst CONFIGURED_RECEIVE_SELECTOR_ROW_HEIGHT = 62;\nconst BASIS_POINTS = 10000;\nconst PREDICTIVE_EXACT_IN_DISCOUNT_BPS = 50;\nconst PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_PCT = 0.1;\nconst PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_MAX_USD = 2;\nconst PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_PCT = 0.02;\nconst PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_MAX_USD = 1;\nconst PREDICTIVE_QUOTE_DISPLAY_DECIMALS = 8;\nconst DEPOSIT_TOKEN_DISPLAY_DECIMALS = 8;\nconst ETHEREUM_MAINNET_CHAIN_ID = 1;\nconst SDK_EXACT_OUT_STABLE_SYMBOLS = new Set([\n \"USDC\",\n \"USDT\",\n \"DAI\",\n \"BUSD\",\n \"TUSD\",\n \"FRAX\",\n \"LUSD\",\n \"USDD\",\n \"USDP\",\n \"GUSD\",\n]);\nconst SWAP_HISTORY_STORAGE_KEY_PREFIX = \"nexus-widget-transaction-history-v1\";\nconst TIMEOUT_LABEL = \"Timed Out\";\nconst PROGRESS_EVENT_NAMES = {\n BRIDGE_PLAN_LIST: \"bridge_plan_list\",\n BRIDGE_PLAN_PROGRESS: \"bridge_plan_progress\",\n SWAP_PLAN_LIST: \"swap_plan_list\",\n SWAP_PLAN_PROGRESS: \"swap_plan_progress\",\n} as const;\nconst PLAN_FINAL_STATES = new Set([\"completed\", \"confirmed\", \"submitted\"]);\nconst PLAN_STEP_FUNDS_MAY_HAVE_MOVED_STATES = new Set([\n \"completed\",\n \"confirmed\",\n \"submitted\",\n \"failed\",\n]);\nconst theme = nexusWidgetTheme;\nconst tooltipSurface = theme.colors.surface;\nconst tooltipText = theme.colors.textStrong;\nconst tooltipBorder = theme.colors.border;\nconst uiFont = theme.fonts.sans;\nconst modalHeightTransitionStyle = {\n interpolateSize: \"allow-keywords\",\n} as React.CSSProperties;\n\nconst getCappedTokenDisplayDecimals = (decimals?: number) => {\n const parsedDecimals = Number(decimals);\n if (!Number.isFinite(parsedDecimals) || parsedDecimals < 0) {\n return DEPOSIT_TOKEN_DISPLAY_DECIMALS;\n }\n return Math.min(Math.floor(parsedDecimals), DEPOSIT_TOKEN_DISPLAY_DECIMALS);\n};\nconst modalHeightTransition = `height ${MODAL_HEIGHT_TRANSITION_MS}ms ease, max-height ${MODAL_HEIGHT_TRANSITION_MS}ms ease`;\n\nconst normalizeSdkExactOutStableSymbol = (symbol?: string) =>\n (symbol ?? \"\")\n .trim()\n .toUpperCase()\n .replaceAll(\"\\u20ae\", \"T\")\n .replaceAll(/[^A-Z0-9]/g, \"\");\n\nconst isSdkExactOutStableSymbol = (symbol?: string) =>\n SDK_EXACT_OUT_STABLE_SYMBOLS.has(normalizeSdkExactOutStableSymbol(symbol));\n\nconst applyPredictiveExactOutBufferUsd = (\n amount: Decimal,\n pct: number,\n maxUsd: number,\n) => Decimal.min(amount.mul(pct), new Decimal(maxUsd));\n\nconst getPredictiveExactOutSourceTargetUsd = (\n destinationUsdNeedingSources: Decimal,\n cachedSourceUsdRatio?: Decimal,\n) => {\n if (destinationUsdNeedingSources.lte(0)) return new Decimal(0);\n if (cachedSourceUsdRatio?.gt(0)) {\n return destinationUsdNeedingSources.mul(cachedSourceUsdRatio);\n }\n\n const destinationBuffer = applyPredictiveExactOutBufferUsd(\n destinationUsdNeedingSources,\n PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_PCT,\n PREDICTIVE_EXACT_OUT_DESTINATION_BUFFER_MAX_USD,\n );\n const destinationBuffered =\n destinationUsdNeedingSources.plus(destinationBuffer);\n const sourceBuffer = applyPredictiveExactOutBufferUsd(\n destinationBuffered,\n PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_PCT,\n PREDICTIVE_EXACT_OUT_SOURCE_BUFFER_MAX_USD,\n );\n\n return destinationBuffered.plus(sourceBuffer);\n};\n\nconst getSwapHistoryStorageKey = (ownerAddress?: string) =>\n `${SWAP_HISTORY_STORAGE_KEY_PREFIX}:${ownerAddress?.toLowerCase() || \"anonymous\"}`;\n\nconst getTokenSelectionKey = (token?: SwapTokenOption | null) => {\n if (!token) return \"\";\n if (token.isUnified) {\n return `unified:${token.unifiedSymbol ?? token.symbol}`;\n }\n return `${token.chainId ?? \"unknown\"}:${token.contractAddress.toLowerCase()}`;\n};\n\nconst getDisplaySourceTokenKey = (token?: SwapTokenOption | null) => {\n if (!token) return \"\";\n const chainId = token.chainId ?? \"unknown\";\n const address = token.contractAddress?.toLowerCase();\n return address\n ? `${chainId}:${address}`\n : `${chainId}:symbol:${token.symbol.toUpperCase()}`;\n};\n\nconst sumDecimalStrings = (left?: string, right?: string) => {\n const leftAmount = parseDecimalLoose(left) ?? new Decimal(0);\n const rightAmount = parseDecimalLoose(right) ?? new Decimal(0);\n const sum = leftAmount.plus(rightAmount);\n return sum.gt(0) ? sum.toFixed() : undefined;\n};\n\nconst mergeDisplaySourceTokens = (tokens: SwapTokenOption[]) => {\n const merged = new Map();\n\n for (const token of tokens) {\n const key = getDisplaySourceTokenKey(token);\n if (!key) continue;\n const existing = merged.get(key);\n if (!existing) {\n merged.set(key, token);\n continue;\n }\n\n const userAmount = sumDecimalStrings(\n existing.userAmount || existing.balance,\n token.userAmount || token.balance,\n );\n const userAmountUsd = sumDecimalStrings(\n existing.userAmountUsd || existing.balanceInFiat,\n token.userAmountUsd || token.balanceInFiat,\n );\n\n merged.set(key, {\n ...existing,\n balance: userAmount ?? existing.balance,\n balanceInFiat: userAmountUsd ?? existing.balanceInFiat,\n chainLogo: existing.chainLogo || token.chainLogo,\n logo: existing.logo || token.logo,\n userAmount: userAmount ?? existing.userAmount,\n userAmountUsd: userAmountUsd ?? existing.userAmountUsd,\n });\n }\n\n return Array.from(merged.values());\n};\n\nconst getTokenQuoteKey = (token?: SwapTokenOption | null) => {\n if (!token) return \"\";\n return [\n getTokenSelectionKey(token),\n token.symbol ?? \"\",\n token.decimals ?? \"\",\n ].join(\":\");\n};\n\nconst getSourceTokensQuoteKey = (tokens: SwapTokenOption[]) =>\n tokens\n .filter((token) => {\n const amt = token.userAmount ?? \"\";\n const cleaned = amt.replaceAll(/[^0-9.]/g, \"\");\n const num = Number.parseFloat(cleaned);\n return !Number.isNaN(num) && num > 0;\n })\n .map((token) =>\n [\n getTokenSelectionKey(token),\n token.symbol ?? \"\",\n token.decimals ?? \"\",\n token.userAmount ?? \"\",\n token.userAmountUsd ?? \"\",\n token.userAmountMode ?? \"\",\n ].join(\":\"),\n )\n .join(\"|\");\n\nconst getSourceTokenSyncKey = (token: SwapTokenOption) =>\n [\n getTokenSelectionKey(token),\n token.userAmount ?? \"\",\n token.userAmountUsd ?? \"\",\n token.balance ?? \"\",\n token.balanceInFiat ?? \"\",\n ].join(\":\");\n\nconst getSourceTokenListSyncKey = (tokens: SwapTokenOption[]) =>\n tokens.map(getSourceTokenSyncKey).join(\"|\");\n\nconst isSameTokenSelection = (\n a?: SwapTokenOption | null,\n b?: SwapTokenOption | null,\n) => Boolean(a && b && getTokenSelectionKey(a) === getTokenSelectionKey(b));\n\nconst getDepositConfigIdentity = (\n deposit?: NexusWidgetDepositOpportunityMetadata | null,\n) => {\n if (!deposit) return \"\";\n return [\n deposit.chainId,\n deposit.tokenAddress.toLowerCase(),\n deposit.tokenSymbol,\n deposit.tokenDecimals,\n deposit.protocol ?? \"\",\n deposit.title ?? \"\",\n ].join(\":\");\n};\n\nconst isSameDepositConfig = (\n a?: NexusWidgetDepositOpportunityConfig | null,\n b?: NexusWidgetDepositOpportunityConfig | null,\n) => {\n if (!a || !b) return false;\n return getDepositConfigIdentity(a) === getDepositConfigIdentity(b);\n};\n\nconst getConfiguredReceiveSelectorRootHeight = (tokenCount: number) => {\n if (tokenCount <= 0) return TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT;\n const visibleRows = Math.min(Math.max(tokenCount, 1), 6);\n return Math.min(\n TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT,\n Math.ceil(\n (CONFIGURED_RECEIVE_SELECTOR_BASE_HEIGHT +\n visibleRows * CONFIGURED_RECEIVE_SELECTOR_ROW_HEIGHT) /\n 0.9,\n ),\n );\n};\n\nconst getConfiguredDeposit = (\n config: RuntimeNexusWidgetConfig,\n): NexusWidgetDepositOpportunityConfig | undefined =>\n config.deposit ?? config.deposits?.[0];\n\ntype RuntimeDestinationPair = {\n chain: number;\n decimals?: number;\n logo?: string;\n symbol?: string;\n token: `0x${string}`;\n};\n\ntype RuntimeNexusWidgetAppearance = NexusWidgetAppearance & {\n logoUrl?: string;\n themeMode?: \"auto\" | \"light\" | \"dark\";\n widgetHeading?: string;\n};\n\ntype RuntimeNexusWidgetConfig = {\n allowedDestinationChains?: number[];\n allowedDestinationPairs?: RuntimeDestinationPair[];\n allowedSourcePairs?: RuntimeDestinationPair[];\n appearance?: RuntimeNexusWidgetAppearance;\n deposit?: NexusWidgetDepositOpportunityConfig;\n deposits?: NexusWidgetDepositOpportunityConfig[];\n mode: NexusWidgetMode;\n prefill?: NexusWidgetRuntimePrefill;\n};\n\ntype RuntimeNexusWidgetAmountInput = {\n max?: string;\n min?: string;\n mode: \"fixed\" | \"user\";\n value?: string;\n};\n\ntype NormalizedNexusWidgetConfig = {\n activeMode: NexusWidgetMode;\n amountInput?: RuntimeNexusWidgetAmountInput;\n appearance?: RuntimeNexusWidgetAppearance;\n config: RuntimeNexusWidgetConfig;\n depositOptions: NexusWidgetDepositOpportunityConfig[];\n isAmountFixed: boolean;\n isRecipientLocked: boolean;\n};\n\nconst normalizeConfiguredString = (value: unknown) => {\n if (value === null || value === undefined) return undefined;\n const text = String(value).trim();\n return text || undefined;\n};\n\nconst normalizeConfiguredAddress = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n return text && isAddress(text) ? text : undefined;\n};\n\nconst isPositiveGasLimit = (value: unknown): value is bigint =>\n typeof value === \"bigint\" && value > BigInt(0);\n\nconst normalizePositiveNumericString = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n if (!text) return undefined;\n try {\n const decimal = new Decimal(text);\n return decimal.isFinite() && decimal.gt(0) ? text : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst normalizeNonNegativeNumericString = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n if (!text) return undefined;\n try {\n const decimal = new Decimal(text);\n return decimal.isFinite() && decimal.gte(0) ? text : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst normalizeHttpUrlString = (value: unknown) => {\n const text = normalizeConfiguredString(value);\n if (!text) return undefined;\n return /^https?:\\/\\//i.test(text) ? text : undefined;\n};\n\nconst normalizeNexusWidgetPrimaryColor = (value?: string) => {\n return normalizeConfiguredString(value);\n};\n\nconst expandHexColor = (value: string) => {\n const text = value.trim();\n if (/^#[0-9a-fA-F]{6}$/.test(text)) return text;\n if (/^#[0-9a-fA-F]{3}$/.test(text)) {\n const [r, g, b] = text.slice(1);\n return `#${r}${r}${g}${g}${b}${b}`;\n }\n return undefined;\n};\n\nconst parseNumberComponent = (value: string, max: number) => {\n const text = value.trim();\n if (!text) return undefined;\n if (text.endsWith(\"%\")) {\n const percent = Number.parseFloat(text.slice(0, -1));\n return Number.isFinite(percent)\n ? Math.max(0, Math.min(max, (percent / 100) * max))\n : undefined;\n }\n const parsed = Number.parseFloat(text);\n return Number.isFinite(parsed)\n ? Math.max(0, Math.min(max, parsed))\n : undefined;\n};\n\nconst parseAngleComponent = (value: string) => {\n const parsed = Number.parseFloat(value.trim());\n if (!Number.isFinite(parsed)) return undefined;\n return ((parsed % 360) + 360) % 360;\n};\n\nconst hslToRgb = (\n h: number,\n s: number,\n l: number,\n): [number, number, number] => {\n const saturation = Math.max(0, Math.min(100, s)) / 100;\n const lightness = Math.max(0, Math.min(100, l)) / 100;\n const c = (1 - Math.abs(2 * lightness - 1)) * saturation;\n const hp = h / 60;\n const x = c * (1 - Math.abs((hp % 2) - 1));\n let r1 = 0;\n let g1 = 0;\n let b1 = 0;\n if (hp >= 0 && hp < 1) {\n r1 = c;\n g1 = x;\n } else if (hp >= 1 && hp < 2) {\n r1 = x;\n g1 = c;\n } else if (hp >= 2 && hp < 3) {\n g1 = c;\n b1 = x;\n } else if (hp >= 3 && hp < 4) {\n g1 = x;\n b1 = c;\n } else if (hp >= 4 && hp < 5) {\n r1 = x;\n b1 = c;\n } else {\n r1 = c;\n b1 = x;\n }\n const m = lightness - c / 2;\n return [\n Math.round((r1 + m) * 255),\n Math.round((g1 + m) * 255),\n Math.round((b1 + m) * 255),\n ];\n};\n\nconst parseCssColorToRgb = (\n color: string,\n allowBrowserResolution = true,\n): [number, number, number] | undefined => {\n const text = normalizeConfiguredString(color);\n if (!text) return undefined;\n\n const hex = expandHexColor(text);\n if (hex) {\n return [\n Number.parseInt(hex.slice(1, 3), 16),\n Number.parseInt(hex.slice(3, 5), 16),\n Number.parseInt(hex.slice(5, 7), 16),\n ];\n }\n\n const rgbMatch = text.match(/^rgba?\\((.*)\\)$/i);\n if (rgbMatch) {\n const parts = rgbMatch[1]\n .trim()\n .split(/[\\s,\\/]+/)\n .filter(Boolean);\n if (parts.length >= 3) {\n const r = parseNumberComponent(parts[0], 255);\n const g = parseNumberComponent(parts[1], 255);\n const b = parseNumberComponent(parts[2], 255);\n if (r !== undefined && g !== undefined && b !== undefined) {\n return [Math.round(r), Math.round(g), Math.round(b)];\n }\n }\n }\n\n const hslMatch = text.match(/^hsla?\\((.*)\\)$/i);\n if (hslMatch) {\n const parts = hslMatch[1]\n .trim()\n .split(/[\\s,\\/]+/)\n .filter(Boolean);\n if (parts.length >= 3) {\n const h = parseAngleComponent(parts[0]);\n const s = parseNumberComponent(parts[1], 100);\n const l = parseNumberComponent(parts[2], 100);\n if (h !== undefined && s !== undefined && l !== undefined) {\n return hslToRgb(h, s, l);\n }\n }\n }\n\n if (allowBrowserResolution && typeof document !== \"undefined\") {\n const canvas = document.createElement(\"canvas\");\n const context = canvas.getContext(\"2d\");\n if (context) {\n const baseline = \"#010203\";\n context.fillStyle = baseline;\n context.fillStyle = text;\n const resolved = String(context.fillStyle);\n if (resolved !== baseline || text.toLowerCase() === baseline) {\n return parseCssColorToRgb(resolved, false);\n }\n }\n }\n\n return undefined;\n};\n\nconst getReadableTextColor = (background: string) => {\n const rgb = parseCssColorToRgb(background);\n if (!rgb) return \"#FFFFFE\";\n const [rValue, gValue, bValue] = rgb;\n const r = rValue / 255;\n const g = gValue / 255;\n const b = bValue / 255;\n const linear = [r, g, b].map((channel) =>\n channel <= 0.03928\n ? channel / 12.92\n : Math.pow((channel + 0.055) / 1.055, 2.4),\n );\n const luminance =\n 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];\n return luminance > 0.54 ? \"#161615\" : \"#FFFFFE\";\n};\n\nconst normalizeNexusWidgetAmountInput = (\n config: NexusWidgetConfig,\n): RuntimeNexusWidgetAmountInput | undefined => {\n if (config.mode === \"swap\") return undefined;\n const value = normalizePositiveNumericString(\n \"amount\" in (config.prefill ?? {}) ? config.prefill?.amount : undefined,\n );\n const min = normalizeNonNegativeNumericString(config.validation?.minAmount);\n const max = normalizePositiveNumericString(config.validation?.maxAmount);\n return value || min || max ? { mode: \"user\", value, min, max } : undefined;\n};\n\nconst normalizeNexusWidgetAppearance = (\n appearance?: NexusWidgetAppearance,\n): RuntimeNexusWidgetAppearance | undefined => {\n if (!appearance) return undefined;\n const appName = normalizeConfiguredString(appearance.appName);\n const appLogoURL = normalizeHttpUrlString(appearance.appLogoURL);\n const heading = normalizeConfiguredString(appearance.heading);\n const mode =\n appearance.mode === \"dark\" || appearance.mode === \"light\"\n ? appearance.mode\n : \"system\";\n const primaryColor = normalizeConfiguredString(appearance.primaryColor);\n return {\n ...appearance,\n appLogoURL,\n appName,\n heading,\n logoUrl: appLogoURL,\n mode,\n primaryColor,\n themeMode: mode === \"system\" ? \"auto\" : mode,\n widgetHeading: heading,\n };\n};\n\nconst normalizeConfiguredChainId = (value: unknown) => {\n const chainId = Number(value);\n return Number.isFinite(chainId) && chainId > 0 ? chainId : undefined;\n};\n\nconst getConfiguredDestinationChainId = (\n destination?: NexusWidgetDestination,\n) => normalizeConfiguredChainId(destination?.chain);\n\nconst getConfiguredDestinationTokenSymbol = (\n token: NexusWidgetDestinationToken,\n) => normalizeConfiguredString(token.symbol);\n\nconst getConfiguredDestinationTokenAddress = (\n token: NexusWidgetDestinationToken,\n) => normalizeConfiguredAddress(token.address);\n\nconst toSwapTokenOptionFromConfiguredDestinationToken = (\n token: NexusWidgetDestinationToken,\n chainId: number,\n): SwapTokenOption | undefined => {\n const contractAddress = getConfiguredDestinationTokenAddress(token);\n const symbol = getConfiguredDestinationTokenSymbol(token);\n if (!contractAddress || !symbol) return undefined;\n const chainMeta = CHAIN_METADATA[chainId];\n return {\n balance: \"0\",\n balanceInFiat: \"$0.00\",\n chainId,\n chainLogo: chainMeta?.logo,\n chainName: getShortChainName(\n chainId,\n chainMeta?.name ?? `Chain ${chainId}`,\n ),\n contractAddress,\n decimals: token.decimals ?? 18,\n logo: token.logo ?? \"\",\n name: symbol,\n symbol,\n };\n};\n\nconst toSwapTokenOptionFromDepositConfig = (\n deposit: NexusWidgetDepositOpportunityConfig,\n): SwapTokenOption => {\n const chainMeta = CHAIN_METADATA[deposit.chainId];\n return {\n balance: \"0\",\n balanceInFiat: \"$0.00\",\n chainId: deposit.chainId,\n chainLogo: chainMeta?.logo,\n chainName: getShortChainName(\n deposit.chainId,\n chainMeta?.name ?? `Chain ${deposit.chainId}`,\n ),\n contractAddress: deposit.tokenAddress,\n decimals: deposit.tokenDecimals,\n logo: deposit.tokenLogo || deposit.logo || \"\",\n name: deposit.tokenSymbol,\n symbol: deposit.tokenSymbol,\n };\n};\n\nconst applyNexusWidgetAppearanceToDeposit = (\n deposit: NexusWidgetDepositOpportunityConfig,\n appearance?: RuntimeNexusWidgetAppearance,\n): NexusWidgetDepositOpportunityConfig => {\n const appName = normalizeConfiguredString(appearance?.appName);\n const logoUrl = normalizeConfiguredString(appearance?.logoUrl);\n if (!appName && !logoUrl) return deposit;\n return {\n ...deposit,\n depositTargetLogo: logoUrl ?? deposit.depositTargetLogo,\n logo: logoUrl ?? deposit.logo,\n protocol: appName ?? deposit.protocol,\n title: appName ?? deposit.title,\n };\n};\n\nconst getConfiguredDestinationPairs = (\n destination?: NexusWidgetDestination,\n) => {\n const pairs: RuntimeNexusWidgetConfig[\"allowedDestinationPairs\"] = [];\n const chain = getConfiguredDestinationChainId(destination);\n if (!chain) return pairs;\n for (const token of destination?.tokens ?? []) {\n const address = getConfiguredDestinationTokenAddress(token);\n if (!address) continue;\n pairs.push({ chain, token: address });\n }\n return pairs;\n};\n\nconst hasConfiguredDestinationTokenList = (\n destination?: NexusWidgetDestination,\n) => Boolean(destination?.tokens?.length);\n\nconst getConfiguredPrefillTokenPair = (prefill?: {\n token?: NexusWidgetPrefillToken;\n}): RuntimeDestinationPair | undefined => {\n const token = prefill?.token;\n if (!token) return undefined;\n const chain = normalizeConfiguredChainId(token.chain);\n const address = getConfiguredDestinationTokenAddress(token);\n const symbol = getConfiguredDestinationTokenSymbol(token);\n if (!chain || !address || !symbol) return undefined;\n return {\n chain,\n decimals: token.decimals,\n logo: normalizeConfiguredString(token.logo),\n symbol,\n token: address,\n };\n};\n\nconst getConfiguredDestinationChainIds = (\n destination?: NexusWidgetDestination,\n) => {\n const chain = getConfiguredDestinationChainId(destination);\n return chain ? [chain] : [];\n};\n\nconst toDepositConfigFromDestination = (\n config: NexusWidgetDepositConfig,\n token: NexusWidgetDestinationToken,\n appearance?: RuntimeNexusWidgetAppearance,\n): NexusWidgetDepositOpportunityConfig | undefined => {\n const chainId = getConfiguredDestinationChainId(config.destination);\n const depositAddress = normalizeConfiguredAddress(config.depositAddress);\n const tokenAddress = getConfiguredDestinationTokenAddress(token);\n const tokenSymbol = getConfiguredDestinationTokenSymbol(token);\n if (\n !chainId ||\n !depositAddress ||\n !tokenAddress ||\n !tokenSymbol ||\n typeof config.executeDeposit !== \"function\"\n ) {\n return undefined;\n }\n\n const chainMeta = CHAIN_METADATA[chainId];\n return {\n chainId,\n depositTargetLogo: appearance?.logoUrl,\n executeDeposit: config.executeDeposit,\n logo: appearance?.logoUrl ?? token.logo ?? undefined,\n protocol: appearance?.appName ?? appearance?.widgetHeading ?? \"Deposit\",\n subtitle: chainMeta?.name\n ? `on ${getShortChainName(chainId, chainMeta.name)}`\n : undefined,\n title: appearance?.appName ?? appearance?.widgetHeading ?? \"Deposit\",\n tokenAddress,\n tokenDecimals: token.decimals ?? 18,\n tokenLogo: token.logo,\n tokenSymbol,\n };\n};\n\nconst getNexusWidgetDepositOptions = (\n config: NexusWidgetConfig,\n appearance?: RuntimeNexusWidgetAppearance,\n) => {\n if (config.mode !== \"deposit\") return [];\n return (config.destination.tokens ?? [])\n .map((token) => toDepositConfigFromDestination(config, token, appearance))\n .filter((deposit): deposit is NexusWidgetDepositOpportunityConfig =>\n Boolean(deposit),\n );\n};\n\nconst normalizeNexusWidgetConfig = (\n rawConfig: NexusWidgetConfig,\n): NormalizedNexusWidgetConfig => {\n const appearance = normalizeNexusWidgetAppearance(rawConfig.appearance);\n const amountInput = normalizeNexusWidgetAmountInput(rawConfig);\n const prefill: NexusWidgetRuntimePrefill = {};\n let activeMode: NexusWidgetMode =\n rawConfig.mode === \"deposit\" ||\n rawConfig.mode === \"send\" ||\n rawConfig.mode === \"swap\"\n ? rawConfig.mode\n : \"swap\";\n let isRecipientLocked = false;\n const depositOptions = getNexusWidgetDepositOptions(\n rawConfig,\n appearance,\n ).map((deposit) => applyNexusWidgetAppearanceToDeposit(deposit, appearance));\n let allowedDestinationPairs:\n | RuntimeNexusWidgetConfig[\"allowedDestinationPairs\"]\n | undefined;\n let allowedDestinationChains:\n | RuntimeNexusWidgetConfig[\"allowedDestinationChains\"]\n | undefined;\n\n if (amountInput?.value) {\n prefill.amount = amountInput.value;\n }\n\n if (rawConfig.mode === \"deposit\") {\n activeMode = \"deposit\";\n const pairs = getConfiguredDestinationPairs(rawConfig.destination);\n if (pairs.length > 0) {\n allowedDestinationPairs = pairs;\n prefill.destination = pairs[0];\n }\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (chainIds.length > 0) {\n allowedDestinationChains = chainIds;\n }\n } else if (rawConfig.mode === \"send\") {\n const recipient = normalizeConfiguredAddress(rawConfig.recipientAddress);\n if (recipient) {\n prefill.recipient = recipient;\n isRecipientLocked = true;\n }\n\n const hasDestinationTokens = hasConfiguredDestinationTokenList(\n rawConfig.destination,\n );\n const pairs = getConfiguredDestinationPairs(rawConfig.destination);\n if (pairs.length > 0) {\n allowedDestinationPairs = pairs;\n prefill.destination ??= pairs[0];\n } else if (!hasDestinationTokens) {\n const prefillToken = getConfiguredPrefillTokenPair(rawConfig.prefill);\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (\n prefillToken &&\n (!chainIds.length || chainIds.includes(prefillToken.chain))\n ) {\n prefill.destination = prefillToken;\n }\n }\n\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (chainIds.length > 0) {\n allowedDestinationChains = chainIds;\n }\n } else if (rawConfig.mode === \"swap\") {\n const recipient = normalizeConfiguredAddress(rawConfig.recipientAddress);\n if (recipient) {\n prefill.recipient = recipient;\n }\n\n const hasDestinationTokens = hasConfiguredDestinationTokenList(\n rawConfig.destination,\n );\n const pairs = getConfiguredDestinationPairs(rawConfig.destination);\n if (pairs.length > 0) {\n allowedDestinationPairs = pairs;\n prefill.destination ??= pairs[0];\n } else if (!hasDestinationTokens) {\n const prefillToken = getConfiguredPrefillTokenPair(rawConfig.prefill);\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (\n prefillToken &&\n (!chainIds.length || chainIds.includes(prefillToken.chain))\n ) {\n prefill.destination = prefillToken;\n }\n }\n\n const chainIds = getConfiguredDestinationChainIds(rawConfig.destination);\n if (chainIds.length > 0) {\n allowedDestinationChains = chainIds;\n }\n }\n\n const runtimeConfig: RuntimeNexusWidgetConfig = {\n allowedDestinationChains,\n allowedDestinationPairs,\n deposit: activeMode === \"deposit\" ? depositOptions[0] : undefined,\n deposits: activeMode === \"deposit\" ? depositOptions : [],\n appearance,\n mode: activeMode,\n prefill,\n };\n\n return {\n activeMode,\n amountInput,\n appearance,\n config: runtimeConfig,\n depositOptions,\n isAmountFixed: false,\n isRecipientLocked,\n };\n};\n\nconst getDepositForTokenSelection = (\n deposits: NexusWidgetDepositOpportunityConfig[],\n token?: SwapTokenOption | null,\n) => {\n if (!token) return undefined;\n return deposits.find(\n (deposit) =>\n deposit.chainId === token.chainId &&\n deposit.tokenAddress.toLowerCase() ===\n token.contractAddress.toLowerCase(),\n );\n};\n\nconst sanitizeOpportunityForHistory = (\n opportunity?: NexusWidgetDepositOpportunityMetadata,\n): NexusWidgetDepositOpportunityMetadata | undefined => {\n if (!opportunity) return undefined;\n return {\n label: opportunity.label,\n protocol: opportunity.protocol,\n logo: opportunity.logo,\n title: opportunity.title,\n subtitle: opportunity.subtitle,\n chainId: opportunity.chainId,\n tokenSymbol: opportunity.tokenSymbol,\n tokenDecimals: opportunity.tokenDecimals,\n tokenLogo: opportunity.tokenLogo,\n tokenAddress: opportunity.tokenAddress,\n apy: opportunity.apy,\n description: opportunity.description,\n };\n};\n\nconst sanitizeHistoryEntry = (entry: SwapHistoryEntry): SwapHistoryEntry => ({\n ...entry,\n createdAt: entry.createdAt ?? entry.startedAt ?? Date.now(),\n failureMessage:\n entry.status === \"timeout\" ? TIMEOUT_LABEL : entry.failureMessage,\n opportunity: sanitizeOpportunityForHistory(entry.opportunity),\n});\n\nconst sortSwapHistoryEntries = (entries: SwapHistoryEntry[]) =>\n [...entries].sort(\n (a, b) =>\n (b.createdAt ?? b.startedAt ?? 0) - (a.createdAt ?? a.startedAt ?? 0),\n );\n\nconst isStoredHistoryStatus = (value: unknown): value is SwapHistoryStatus =>\n value === \"pending\" ||\n value === \"fulfilled\" ||\n value === \"failed\" ||\n value === \"timeout\" ||\n value === \"refund-initiated\";\n\nconst isStoredMode = (value: unknown): value is NexusWidgetMode =>\n value === \"swap\" || value === \"deposit\" || value === \"send\";\n\nconst normalizeStoredHistoryEntry = (\n value: unknown,\n): SwapHistoryEntry | null => {\n if (!value || typeof value !== \"object\") return null;\n const entry = value as Partial;\n const startedAt =\n typeof entry.startedAt === \"number\" && Number.isFinite(entry.startedAt)\n ? entry.startedAt\n : undefined;\n const createdAt =\n typeof entry.createdAt === \"number\" && Number.isFinite(entry.createdAt)\n ? entry.createdAt\n : startedAt;\n\n if (\n !entry.id ||\n typeof entry.id !== \"string\" ||\n !isStoredMode(entry.mode) ||\n !isStoredHistoryStatus(entry.status) ||\n !createdAt ||\n !startedAt\n ) {\n return null;\n }\n\n return {\n ...entry,\n id: entry.id,\n mode: entry.mode,\n status: entry.status,\n createdAt,\n startedAt,\n failureMessage:\n entry.status === \"timeout\" ? TIMEOUT_LABEL : entry.failureMessage,\n intentData: entry.intentData ?? null,\n fromTokens: Array.isArray(entry.fromTokens) ? entry.fromTokens : [],\n opportunity: sanitizeOpportunityForHistory(entry.opportunity),\n } as SwapHistoryEntry;\n};\n\nconst readSwapHistoryFromStorage = (storageKey: string): SwapHistoryEntry[] => {\n if (typeof window === \"undefined\") return [];\n\n try {\n const raw = window.localStorage.getItem(storageKey);\n if (!raw) return [];\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n return sortSwapHistoryEntries(\n parsed\n .map(normalizeStoredHistoryEntry)\n .filter((entry): entry is SwapHistoryEntry => Boolean(entry)),\n );\n } catch {\n return [];\n }\n};\n\nconst writeSwapHistoryToStorage = (\n storageKey: string,\n entries: SwapHistoryEntry[],\n) => {\n if (typeof window === \"undefined\") return;\n\n try {\n const persistableEntries =\n sortSwapHistoryEntries(entries).map(sanitizeHistoryEntry);\n window.localStorage.setItem(\n storageKey,\n JSON.stringify(persistableEntries, (_key, value) =>\n typeof value === \"bigint\" ? value.toString() : value,\n ),\n );\n } catch {\n // localStorage can be unavailable or full; in-memory history still works.\n }\n};\n\nfunction QuoteRefreshCountdown({\n progress,\n isRefreshing,\n secondsRemaining,\n}: {\n progress: number;\n isRefreshing: boolean;\n secondsRemaining: number;\n}) {\n const [showTooltip, setShowTooltip] = useState(false);\n const radius = 7;\n const circumference = 2 * Math.PI * radius;\n const clampedProgress = Math.max(0, Math.min(1, progress));\n const tooltipLabel = isRefreshing\n ? \"Refreshing quotes...\"\n : `Refreshing quotes in ${Math.max(0, secondsRemaining)} second${\n secondsRemaining === 1 ? \"\" : \"s\"\n }`;\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n style={{\n alignItems: \"center\",\n backgroundColor: \"#FFFFFE\",\n borderRadius: \"999px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n height: \"22px\",\n justifyContent: \"center\",\n outline: \"1px solid #E8E8E7\",\n position: \"relative\",\n width: \"22px\",\n }}\n tabIndex={0}\n >\n {showTooltip && (\n \n {tooltipLabel}\n \n )}\n \n \n \n \n \n );\n}\n\nconst normalizeDecimalInputText = (value: unknown) => {\n const raw = String(value).trim();\n if (!raw) return \"\";\n if (SCIENTIFIC_DECIMAL_REGEX.test(raw)) return raw;\n return raw.replace(/[^0-9.-]/g, \"\");\n};\n\nconst parseDecimalLoose = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = normalizeDecimalInputText(value);\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst toViemDecimalString = (value: unknown, decimals: number) => {\n const parsed = parseDecimalLoose(value);\n if (!parsed || parsed.lte(0)) return \"0\";\n return parsed\n .toDecimalPlaces(Math.max(0, decimals), Decimal.ROUND_DOWN)\n .toFixed();\n};\n\nconst formatDecimalDisplay = (\n value: unknown,\n options: { min?: number; max?: number } = {},\n) => {\n const amount = parseDecimalLoose(value) ?? new Decimal(0);\n const max = options.max ?? 2;\n return amount.toDecimalPlaces(max).toFixed();\n};\n\nconst formatUsdDisplay = (value: unknown) => {\n const amount = parseDecimalLoose(value) ?? new Decimal(0);\n if (amount.gt(0) && amount.lt(0.01)) return \"<$0.01\";\n return `$${formatDecimalDisplay(amount, { min: 2, max: 2 })}`;\n};\n\nconst formatTokenDisplay = (value: unknown) => {\n const amount = parseDecimalLoose(value) ?? new Decimal(0);\n return formatDecimalDisplay(amount, { max: 8 });\n};\n\nconst getSwapTokenUsdValue = (token: SwapTokenOption) =>\n parseDecimalLoose(token.userAmountUsd) ??\n parseDecimalLoose(token.balanceInFiat) ??\n new Decimal(0);\n\nconst getSwapTokenBalanceUsdValue = (token: SwapTokenOption) =>\n parseDecimalLoose(token.balanceInFiat) ?? new Decimal(0);\n\nconst sortSwapTokensByUsdDesc = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const usdDelta = getSwapTokenUsdValue(b).cmp(getSwapTokenUsdValue(a));\n if (usdDelta !== 0) return usdDelta;\n return (a.symbol ?? \"\").localeCompare(b.symbol ?? \"\");\n });\n\nconst sortDisplaySourcesByBalanceUsdDesc = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const balanceUsdDelta = getSwapTokenBalanceUsdValue(b).cmp(\n getSwapTokenBalanceUsdValue(a),\n );\n if (balanceUsdDelta !== 0) return balanceUsdDelta;\n\n const spendUsdDelta = getSwapTokenUsdValue(b).cmp(getSwapTokenUsdValue(a));\n if (spendUsdDelta !== 0) return spendUsdDelta;\n\n return `${a.symbol ?? \"\"} ${a.chainName ?? \"\"}`.localeCompare(\n `${b.symbol ?? \"\"} ${b.chainName ?? \"\"}`,\n );\n });\n\nconst getIntentSourceUsdValue = (source: SwapIntentData[\"sources\"][number]) =>\n parseDecimalLoose(source.value) ?? new Decimal(0);\n\nconst sortIntentSourcesByUsdDesc = (sources: SwapIntentData[\"sources\"]) =>\n [...sources].sort((a, b) => {\n const usdDelta = getIntentSourceUsdValue(b).cmp(getIntentSourceUsdValue(a));\n if (usdDelta !== 0) return usdDelta;\n return (a.token?.symbol ?? \"\").localeCompare(b.token?.symbol ?? \"\");\n });\n\nconst extractIntentIdFromUrl = (url?: string | null) => {\n if (!url) return undefined;\n const match = url.match(/(\\d+)(?:\\/)?$/);\n if (!match) return undefined;\n const parsed = Number(match[1]);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;\n};\n\nconst getNonEmptyString = (...values: unknown[]) => {\n for (const value of values) {\n if (typeof value !== \"string\") continue;\n const trimmed = value.trim();\n if (trimmed.length > 0) return trimmed;\n }\n return null;\n};\n\nconst isHttpUrl = (value?: string | null): value is string =>\n Boolean(value && /^https?:\\/\\//i.test(value));\n\nconst hasValidIntentExplorer = (\n entry: Pick,\n) => isHttpUrl(entry.intentExplorerUrl);\n\nconst getHistoryExplorerUrl = (\n entry: Pick<\n SwapHistoryEntry,\n \"finalExplorerUrl\" | \"intentExplorerUrl\" | \"sourceExplorerUrl\"\n >,\n) =>\n [\n entry.intentExplorerUrl,\n entry.finalExplorerUrl,\n entry.sourceExplorerUrl,\n ].find(isHttpUrl) ?? null;\n\nconst getFiniteNumber = (...values: unknown[]) => {\n for (const value of values) {\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value !== \"string\") continue;\n const parsed = Number(value);\n if (Number.isFinite(parsed)) return parsed;\n }\n return undefined;\n};\n\nconst getObjectChainId = (value: any) =>\n getFiniteNumber(\n value?.chainId,\n value?.chain?.id,\n value?.chain?.chainId,\n value?.toChainId,\n value?.destinationChainId,\n value?.data?.chainId,\n value?.data?.chain?.id,\n value?.data?.chain?.chainId,\n value?.data?.toChainId,\n value?.data?.destinationChainId,\n value?.result?.chainId,\n value?.result?.chain?.id,\n );\n\nconst getExplorerBaseUrl = (chainId?: number, ...candidates: unknown[]) => {\n const directCandidates = candidates.flatMap((candidate: any) => [\n candidate?.blockExplorerUrl,\n candidate?.blockExplorerURL,\n candidate?.chainBlockExplorerUrl,\n candidate?.explorerBaseUrl,\n candidate?.explorerUrlBase,\n candidate?.blockExplorerUrls?.[0],\n candidate?.blockExplorers?.default?.url,\n candidate?.chain?.blockExplorerUrl,\n candidate?.chain?.blockExplorerURL,\n candidate?.chain?.chainBlockExplorerUrl,\n candidate?.chain?.explorerBaseUrl,\n candidate?.chain?.explorerUrlBase,\n candidate?.chain?.blockExplorerUrls?.[0],\n candidate?.chain?.blockExplorers?.default?.url,\n candidate?.data?.blockExplorerUrl,\n candidate?.data?.blockExplorerURL,\n candidate?.data?.chainBlockExplorerUrl,\n candidate?.data?.blockExplorerUrls?.[0],\n candidate?.data?.blockExplorers?.default?.url,\n candidate?.data?.chain?.blockExplorerUrl,\n candidate?.data?.chain?.blockExplorerURL,\n candidate?.data?.chain?.chainBlockExplorerUrl,\n candidate?.data?.chain?.blockExplorerUrls?.[0],\n candidate?.data?.chain?.blockExplorers?.default?.url,\n ]);\n return getNonEmptyString(\n ...directCandidates,\n chainId ? CHAIN_METADATA[chainId]?.blockExplorerUrls?.[0] : undefined,\n chainId\n ? (CHAIN_METADATA[chainId] as any)?.blockExplorers?.default?.url\n : undefined,\n );\n};\n\nconst getTransactionHash = (...values: unknown[]) => {\n for (const value of values) {\n if (typeof value !== \"string\") continue;\n const trimmed = value.trim();\n if (/^0x[a-fA-F0-9]{64}$/.test(trimmed)) return trimmed;\n }\n return null;\n};\n\nconst getIntentHash = (...values: unknown[]) => getTransactionHash(...values);\n\nconst getObjectIntentHash = (value: any) =>\n getIntentHash(\n value?.intentHash,\n value?.intent_hash,\n value?.intent?.hash,\n value?.intent?.intentHash,\n value?.intent?.intent_hash,\n value?.requestHash,\n value?.request_hash,\n value?.request?.hash,\n value?.request?.requestHash,\n value?.rffHash,\n value?.rff_hash,\n value?.rff?.hash,\n value?.data?.intentHash,\n value?.data?.intent_hash,\n value?.data?.intent?.hash,\n value?.data?.intent?.intentHash,\n value?.data?.intent?.intent_hash,\n value?.data?.requestHash,\n value?.data?.request_hash,\n value?.data?.request?.hash,\n value?.data?.request?.requestHash,\n value?.data?.rffHash,\n value?.data?.rff_hash,\n value?.data?.rff?.hash,\n value?.result?.intentHash,\n value?.result?.intent_hash,\n value?.result?.intent?.hash,\n value?.result?.requestHash,\n value?.result?.request_hash,\n value?.result?.rffHash,\n value?.result?.rff_hash,\n );\n\nconst getNexusExplorerNetwork = (network?: unknown) => {\n const normalized =\n typeof network === \"string\" ? network.trim().toLowerCase() : \"\";\n if (normalized === \"canary\" || normalized === \"testnet\") return normalized;\n return \"mainnet\";\n};\n\nconst getRffExplorerUrl = (network: unknown, intentHash?: string | null) =>\n intentHash\n ? `https://nexus-v2.${getNexusExplorerNetwork(network)}.avail.so/rff/${intentHash}`\n : null;\n\nconst getObjectTransactionHash = (value: any) =>\n getTransactionHash(\n value?.txHash,\n value?.transactionHash,\n value?.executeTxHash,\n value?.executeTransactionHash,\n value?.transferTransactionHash,\n value?.receipt?.transactionHash,\n value?.tx?.hash,\n value?.transaction?.hash,\n value?.data?.txHash,\n value?.data?.transactionHash,\n value?.data?.executeTxHash,\n value?.data?.executeTransactionHash,\n value?.data?.transferTransactionHash,\n value?.data?.receipt?.transactionHash,\n value?.data?.tx?.hash,\n value?.data?.transaction?.hash,\n value?.result?.txHash,\n value?.result?.transactionHash,\n value?.result?.receipt?.transactionHash,\n );\n\nconst getExplorerTxUrl = (\n chainId?: number,\n txHash?: string | null,\n ...candidates: unknown[]\n) => {\n if (!chainId || !txHash) return null;\n const baseUrl = getExplorerBaseUrl(chainId, ...candidates);\n return baseUrl ? `${String(baseUrl).replace(/\\/$/, \"\")}/tx/${txHash}` : null;\n};\n\nconst getSdkSwapResult = (result: any) => {\n const candidate = result?.swapResult ?? result?.result;\n return candidate && typeof candidate === \"object\" ? candidate : null;\n};\n\nconst getSdkTransactionHash = (result: any) =>\n getObjectTransactionHash(result) ||\n getObjectTransactionHash(result?.executeResponse) ||\n getObjectTransactionHash(result?.execute) ||\n getObjectTransactionHash(result?.transfer) ||\n getObjectTransactionHash(result?.swapResult) ||\n getObjectTransactionHash(result?.result) ||\n null;\n\nconst getSdkExplorerUrl = (result: any) =>\n getNonEmptyString(\n result?.explorerUrl,\n result?.explorerURL,\n result?.txExplorerUrl,\n result?.transactionExplorerUrl,\n result?.execute?.explorerUrl,\n result?.execute?.explorerURL,\n result?.execute?.txExplorerUrl,\n result?.execute?.transactionExplorerUrl,\n result?.executeResponse?.explorerUrl,\n result?.executeResponse?.explorerURL,\n result?.executeResponse?.txExplorerUrl,\n result?.executeResponse?.transactionExplorerUrl,\n result?.executeExplorerUrl,\n result?.transferExplorerUrl,\n result?.swapResult?.explorerUrl,\n result?.swapResult?.explorerURL,\n result?.swapResult?.txExplorerUrl,\n result?.swapResult?.transactionExplorerUrl,\n result?.result?.explorerUrl,\n result?.result?.explorerURL,\n result?.result?.txExplorerUrl,\n result?.result?.transactionExplorerUrl,\n );\n\nconst getSdkIntentExplorerUrl = (result: any, swapResult?: any) =>\n getNonEmptyString(\n swapResult?.intentExplorerUrl,\n swapResult?.intentExplorerURL,\n swapResult?.intentUrl,\n swapResult?.intentURL,\n swapResult?.rffUrl,\n swapResult?.rffURL,\n swapResult?.rffExplorerUrl,\n swapResult?.rffExplorerURL,\n swapResult?.explorerUrl,\n swapResult?.explorerURL,\n result?.intentExplorerUrl,\n result?.intentExplorerURL,\n result?.intentUrl,\n result?.intentURL,\n result?.rffUrl,\n result?.rffURL,\n result?.rffExplorerUrl,\n result?.rffExplorerURL,\n result?.swapResult?.intentExplorerUrl,\n result?.swapResult?.intentExplorerURL,\n result?.swapResult?.intentUrl,\n result?.swapResult?.intentURL,\n result?.swapResult?.rffUrl,\n result?.swapResult?.rffURL,\n result?.swapResult?.rffExplorerUrl,\n result?.swapResult?.rffExplorerURL,\n result?.swapResult?.explorerUrl,\n result?.swapResult?.explorerURL,\n result?.result?.intentExplorerUrl,\n result?.result?.intentExplorerURL,\n result?.result?.intentUrl,\n result?.result?.intentURL,\n result?.result?.rffUrl,\n result?.result?.rffURL,\n result?.result?.rffExplorerUrl,\n result?.result?.rffExplorerURL,\n result?.result?.explorerUrl,\n result?.result?.explorerURL,\n );\n\nconst getSdkIntentExplorerUrlForNetwork = (\n network: unknown,\n result: any,\n swapResult?: any,\n) =>\n getSdkIntentExplorerUrl(result, swapResult) ||\n getRffExplorerUrl(\n network,\n getObjectIntentHash(swapResult) || getObjectIntentHash(result),\n );\n\nfunction MiniLogo({\n src,\n label,\n size = 30,\n fontSize = 13,\n outline,\n style,\n}: {\n src?: string;\n label?: string;\n size?: number;\n fontSize?: number;\n outline?: string;\n style?: React.CSSProperties;\n}) {\n const [failed, setFailed] = useState(!src);\n\n useEffect(() => {\n setFailed(!src);\n }, [src]);\n\n if (!failed && src) {\n return (\n setFailed(true)}\n src={src}\n style={{\n background: \"#FFFFFE\",\n borderRadius: \"999px\",\n height: size,\n objectFit: \"cover\",\n outline,\n width: size,\n ...style,\n }}\n />\n );\n }\n\n return (\n \n {(label || \"?\").trim().slice(0, 1).toUpperCase()}\n \n );\n}\n\nfunction TokenLogoPair({\n tokenLogo,\n chainLogo,\n tokenSymbol,\n chainName,\n tokenOutline,\n size = 34,\n}: {\n tokenLogo?: string;\n chainLogo?: string;\n tokenSymbol?: string;\n chainName?: string;\n tokenOutline?: string;\n size?: number;\n}) {\n return (\n \n \n {chainLogo && (\n \n )}\n \n );\n}\n\nfunction SourceLogoStack({\n sources,\n size = 24,\n maxVisible = 3,\n}: {\n sources: HistorySourceRow[];\n size?: number;\n maxVisible?: number;\n}) {\n const visibleSources = sources.slice(0, maxVisible);\n const hiddenCount = Math.max(0, sources.length - visibleSources.length);\n\n return (\n \n {visibleSources.map((source, index) => (\n \n \n \n ))}\n {hiddenCount > 0 && (\n \n +{hiddenCount}\n \n )}\n \n );\n}\n\nfunction TruncatedAddress({\n address,\n color = \"var(--foreground-brand)\",\n}: {\n address: string;\n color?: string;\n}) {\n const [showTooltip, setShowTooltip] = useState(false);\n const label =\n address.length > 12\n ? `${address.slice(0, 6)}...${address.slice(-4)}`\n : address;\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n style={{\n color,\n display: \"inline-flex\",\n fontFamily: uiFont,\n fontSize: \"15px\",\n fontWeight: 500,\n lineHeight: \"20px\",\n outline: \"none\",\n position: \"relative\",\n }}\n tabIndex={0}\n >\n {label}\n {showTooltip && (\n \n {address}\n \n )}\n \n );\n}\n\nconst getDisplayDestinationSourceRow = (\n entry: SwapHistoryEntry,\n): HistorySourceRow | null => {\n if (entry.mode !== \"deposit\" && entry.mode !== \"send\") return null;\n if (!entry.toToken || !entry.requestedToAmount) return null;\n\n const requestedAmount = parseDecimalLoose(entry.requestedToAmount);\n const intentDestinationAmount = parseDecimalLoose(\n entry.intentData?.destination.amount,\n );\n const destinationBalanceAmount = parseDecimalLoose(\n entry.toToken.balance?.replace(entry.toToken.symbol, \"\"),\n );\n if (\n !requestedAmount ||\n !destinationBalanceAmount ||\n requestedAmount.lte(0) ||\n destinationBalanceAmount.lte(0)\n ) {\n return null;\n }\n\n const intentCoversAmount = intentDestinationAmount ?? new Decimal(0);\n const displayAmount = Decimal.min(\n destinationBalanceAmount,\n Decimal.max(0, requestedAmount.minus(intentCoversAmount)),\n );\n if (displayAmount.lte(0)) return null;\n\n const requestedValue = parseDecimalLoose(entry.requestedToValue);\n const destinationValue = parseDecimalLoose(\n entry.intentData?.destination.value,\n );\n const rate =\n requestedValue && requestedAmount.gt(0)\n ? requestedValue.div(requestedAmount)\n : destinationValue && intentCoversAmount.gt(0)\n ? destinationValue.div(intentCoversAmount)\n : undefined;\n\n return {\n key: `destination-balance-${entry.toToken.chainId}-${entry.toToken.contractAddress}`,\n chainId: entry.toToken.chainId,\n contractAddress: entry.toToken.contractAddress,\n tokenLogo: entry.intentData?.destination.token.logo || entry.toToken.logo,\n chainLogo: entry.toToken.chainLogo,\n symbol: entry.toToken.symbol,\n chainName: getShortChainName(\n entry.toToken.chainId,\n entry.toToken.chainName,\n ),\n amount: displayAmount\n .toDecimalPlaces(\n Math.max(0, entry.toToken.decimals ?? 18),\n Decimal.ROUND_DOWN,\n )\n .toFixed(),\n value: rate\n ? displayAmount.mul(rate).toFixed()\n : entry.toToken.balanceInFiat,\n };\n};\n\nconst getHistorySourceRowMergeKey = (row: HistorySourceRow) => {\n const chainKey = row.chainId ?? row.chainName;\n const address = row.contractAddress?.toLowerCase();\n return address\n ? `${chainKey}:${address}`\n : `${chainKey}:symbol:${row.symbol.toUpperCase()}`;\n};\n\nconst mergeHistorySourceRows = (rows: HistorySourceRow[]) => {\n const merged = new Map();\n\n for (const row of rows) {\n const key = getHistorySourceRowMergeKey(row);\n const existing = merged.get(key);\n if (!existing) {\n merged.set(key, row);\n continue;\n }\n\n merged.set(key, {\n ...existing,\n amount: sumDecimalStrings(existing.amount, row.amount) ?? existing.amount,\n chainLogo: existing.chainLogo || row.chainLogo,\n key: existing.key,\n tokenLogo: existing.tokenLogo || row.tokenLogo,\n value: sumDecimalStrings(\n String(existing.value ?? \"\"),\n String(row.value ?? \"\"),\n ),\n });\n }\n\n return Array.from(merged.values());\n};\n\nconst getProgressStepType = (step?: SwapStepType | BridgeStepType | null) =>\n String((step as any)?.type ?? (step as any)?.typeID ?? \"\").toUpperCase();\n\nconst isSourceMovementProgressStep = (type: string, rawStepType: string) =>\n rawStepType === \"source_swap\" ||\n rawStepType === \"eoa_to_ephemeral_transfer\" ||\n rawStepType === \"bridge_deposit\" ||\n rawStepType === \"vault_deposit\" ||\n type.includes(\"SOURCE_SWAP\") ||\n type.includes(\"EOA_TO_EPHEMERAL_TRANSFER\") ||\n type.includes(\"BRIDGE_DEPOSIT\") ||\n type.includes(\"VAULT_DEPOSIT\");\n\nconst isBridgeRefundStepType = (type: string) =>\n type.includes(\"BRIDGE_INTENT_SUBMISSION\") || type.includes(\"BRIDGE_DEPOSIT\");\n\nconst isSwapSkippedStepType = (type: string) => type.includes(\"SWAP_SKIPPED\");\n\nconst isAutoRefundAvailableProgressEvent = (event?: NexusWidgetProgressEvent) =>\n event?.name === PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS &&\n isBridgeRefundStepType(getProgressStepType(event.step));\n\nconst normalizeBridgeProvider = (\n value: unknown,\n): BridgeProvider | undefined => {\n if (value === \"nexus\" || value === \"mayan\" || value === null) {\n return value;\n }\n return undefined;\n};\n\nconst normalizePlanStepType = (stepType: unknown, state?: unknown) => {\n const normalized = String(stepType ?? \"\").toLowerCase();\n const normalizedState = String(state ?? \"\").toLowerCase();\n\n if (normalized === \"execute_transaction\") {\n return normalizedState === \"confirmed\" || normalizedState === \"completed\"\n ? \"TRANSACTION_CONFIRMED\"\n : \"TRANSACTION_SENT\";\n }\n\n const mapped: Record = {\n allowance_approval: \"APPROVAL\",\n bridge_deposit: \"BRIDGE_DEPOSIT\",\n bridge_fill: \"BRIDGE_FILL\",\n bridge_intent_submission: \"BRIDGE_INTENT_SUBMISSION\",\n destination_swap: \"DESTINATION_SWAP\",\n eoa_to_ephemeral_transfer: \"EOA_TO_EPHEMERAL_TRANSFER\",\n execute_approval: \"APPROVAL\",\n request_signing: \"REQUEST_SIGNING\",\n request_submission: \"REQUEST_SUBMISSION\",\n source_swap: \"SOURCE_SWAP\",\n vault_deposit: \"BRIDGE_DEPOSIT\",\n };\n\n return mapped[normalized] ?? normalized.toUpperCase();\n};\n\nconst normalizePlanStep = (\n stepLike: unknown,\n fallbackStepType?: unknown,\n state?: unknown,\n completed?: boolean,\n): SwapStepType | BridgeStepType => {\n const source =\n stepLike && typeof stepLike === \"object\" ? (stepLike as any) : {};\n const rawStepType = fallbackStepType ?? source.stepType ?? source.type;\n const progressType = normalizePlanStepType(\n rawStepType ?? source.typeID,\n state,\n );\n const progressKey =\n source.id ?? source.stepId ?? source.typeID ?? progressType;\n\n return {\n ...source,\n completed,\n rawType: rawStepType,\n type: progressType,\n typeID: String(progressKey),\n } as SwapStepType | BridgeStepType;\n};\n\nconst getPlanStepChainId = (event: any, step: any) =>\n getObjectChainId(event) ?? getObjectChainId(step);\n\nconst getPlanStepTransactionHash = (event: any, step: any) =>\n getObjectTransactionHash(event) ?? getObjectTransactionHash(step);\n\nconst getPlanStepExplorerUrl = (event: any, step: any) => {\n const directExplorerUrl = getNonEmptyString(\n event?.explorerUrl,\n event?.explorerURL,\n event?.txExplorerUrl,\n event?.transactionExplorerUrl,\n step?.explorerUrl,\n step?.explorerURL,\n step?.txExplorerUrl,\n step?.transactionExplorerUrl,\n step?.data?.explorerUrl,\n step?.data?.explorerURL,\n step?.data?.txExplorerUrl,\n step?.data?.transactionExplorerUrl,\n );\n if (directExplorerUrl) return directExplorerUrl;\n\n return getExplorerTxUrl(\n getPlanStepChainId(event, step),\n getPlanStepTransactionHash(event, step),\n event,\n step,\n );\n};\n\nconst getPlanStepIntentExplorerUrl = (event: any, step: any) =>\n getNonEmptyString(\n event?.intentExplorerUrl,\n event?.intentExplorerURL,\n event?.intentUrl,\n event?.intentURL,\n event?.rffUrl,\n event?.rffURL,\n event?.rffExplorerUrl,\n event?.rffExplorerURL,\n step?.intentExplorerUrl,\n step?.intentExplorerURL,\n step?.intentUrl,\n step?.intentURL,\n step?.rffUrl,\n step?.rffURL,\n step?.rffExplorerUrl,\n step?.rffExplorerURL,\n step?.data?.intentExplorerUrl,\n step?.data?.intentExplorerURL,\n step?.data?.intentUrl,\n step?.data?.intentURL,\n step?.data?.rffUrl,\n step?.data?.rffURL,\n step?.data?.rffExplorerUrl,\n step?.data?.rffExplorerURL,\n );\n\nconst isIntentSubmissionLikeEvent = (event: any, step?: any) => {\n const text = [\n event?.type,\n event?.event,\n event?.name,\n event?.status,\n event?.stepType,\n step?.type,\n step?.typeID,\n step?.rawType,\n ]\n .filter((value) => value !== null && value !== undefined)\n .join(\" \")\n .toLowerCase();\n\n return (\n text.includes(\"intent\") ||\n text.includes(\"request_submission\") ||\n text.includes(\"rff\")\n );\n};\n\nconst getGenericEventHash = (event: any, step?: any) =>\n getTransactionHash(\n event?.hash,\n event?.data?.hash,\n event?.result?.hash,\n step?.hash,\n step?.data?.hash,\n step?.result?.hash,\n );\n\nconst getEventIntentExplorerUrl = (\n network: unknown,\n event: any,\n step?: any,\n) => {\n const directUrl = step\n ? getPlanStepIntentExplorerUrl(event, step)\n : getNonEmptyString(\n event?.intentExplorerUrl,\n event?.intentExplorerURL,\n event?.intentUrl,\n event?.intentURL,\n event?.rffUrl,\n event?.rffURL,\n event?.rffExplorerUrl,\n event?.rffExplorerURL,\n event?.data?.intentExplorerUrl,\n event?.data?.intentExplorerURL,\n event?.data?.intentUrl,\n event?.data?.intentURL,\n event?.data?.rffUrl,\n event?.data?.rffURL,\n event?.data?.rffExplorerUrl,\n event?.data?.rffExplorerURL,\n );\n if (directUrl) return directUrl;\n\n return getRffExplorerUrl(\n network,\n getObjectIntentHash(event) ||\n getObjectIntentHash(step) ||\n (isIntentSubmissionLikeEvent(event, step)\n ? getGenericEventHash(event, step)\n : null),\n );\n};\n\nconst isTimeoutLikeError = (error: unknown) => {\n const err = error as {\n code?: unknown;\n name?: unknown;\n message?: unknown;\n shortMessage?: unknown;\n };\n const text = [\n err?.code,\n err?.name,\n err?.message,\n err?.shortMessage,\n typeof error === \"string\" ? error : undefined,\n ]\n .filter((value) => value !== null && value !== undefined)\n .join(\" \");\n\n return /timeout|timed out|time out|deadline exceeded|expired while waiting|wait.*expired|poll.*expired/i.test(\n text,\n );\n};\n\nconst getSdkEventType = (event: any) =>\n event?.type ?? event?.name ?? event?.event ?? \"unknown\";\n\nconst summarizeSdkProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n index?: number,\n) => ({\n completed: (step as any)?.completed,\n index,\n rawType: (step as any)?.rawType ?? (step as any)?.stepType,\n state: (step as any)?.state,\n type: getProgressStepType(step),\n});\n\nconst isNexusWidgetSdkDebugEnabled = () => {\n if (typeof window === \"undefined\") return false;\n if ((window as any).__NEXUS_WIDGET_DEBUG__ === true) return true;\n try {\n return window.localStorage.getItem(\"nexus-widget:debug\") === \"true\";\n } catch {\n return false;\n }\n};\n\nconst logSdkSwapEvent = (\n label: string,\n event: any,\n meta?: Record,\n) => {\n if (!isNexusWidgetSdkDebugEnabled()) return;\n console.log(`[NexusWidget SDK][swap] ${label}`, {\n event,\n eventType: getSdkEventType(event),\n ...meta,\n });\n};\n\nconst logSdkIntentEvent = (\n label: string,\n data: any,\n meta?: Record,\n) => {\n if (!isNexusWidgetSdkDebugEnabled()) return;\n console.log(`[NexusWidget SDK][intent] ${label}`, {\n hasAllow: typeof data?.allow === \"function\",\n hasDeny: typeof data?.deny === \"function\",\n hasRefresh: typeof data?.refresh === \"function\",\n intent: data?.intent,\n raw: data,\n ...meta,\n });\n};\n\nconst logSdkIntentInput = (\n operation: string,\n input: unknown,\n meta?: Record,\n) => {\n if (!isNexusWidgetSdkDebugEnabled()) return;\n console.log(`[NexusWidget SDK][intent input] ${operation}`, {\n input,\n ...meta,\n });\n};\n\nconst normalizeSdkIntentString = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n return String(value);\n};\n\nconst normalizeSdkIntentAmount = (value: unknown, fallback: string = \"0\") =>\n normalizeSdkIntentString(value) ?? fallback;\n\nconst getLogoFromMetadata = (metadata: any) =>\n metadata?.logo ??\n metadata?.logoURI ??\n metadata?.logoUri ??\n metadata?.logoUrl ??\n metadata?.icon ??\n \"\";\n\nconst normalizeSdkIntentChain = (chain: any) => {\n const id = Number(chain?.id ?? chain?.chainId);\n if (!Number.isFinite(id)) return undefined;\n const chainMeta = CHAIN_METADATA[id];\n return {\n id,\n logo: getLogoFromMetadata(chain) || chainMeta?.logo || \"\",\n name: chain?.name ?? chainMeta?.name ?? \"\",\n };\n};\n\nconst normalizeSdkIntentToken = (token: any, chainId?: number) => {\n const chainMeta = chainId ? CHAIN_METADATA[chainId] : undefined;\n const decimals = Number(\n token?.decimals ?? chainMeta?.nativeCurrency.decimals,\n );\n return {\n contractAddress:\n token?.contractAddress ??\n token?.address ??\n token?.tokenAddress ??\n zeroAddress,\n decimals: Number.isFinite(decimals) ? decimals : 18,\n logo: getLogoFromMetadata(token) || undefined,\n symbol: token?.symbol ?? token?.tokenSymbol ?? \"\",\n };\n};\n\nconst normalizeSdkIntentGas = (\n gas: any,\n chainId?: number,\n): SwapIntentDestination[\"gas\"] => ({\n amount: normalizeSdkIntentAmount(gas?.amount),\n value: normalizeSdkIntentString(gas?.value),\n token: normalizeSdkIntentToken(gas?.token ?? gas, chainId),\n});\n\nconst normalizeSdkIntentSource = (\n source: any,\n): SwapIntentSource | undefined => {\n const chain = normalizeSdkIntentChain(source?.chain);\n if (!chain) return undefined;\n return {\n ...source,\n amount: normalizeSdkIntentAmount(source?.amount),\n chain,\n token: normalizeSdkIntentToken(source?.token, chain.id),\n value: normalizeSdkIntentString(source?.value),\n };\n};\n\nconst normalizeSdkIntentDestination = (\n destination: any,\n): SwapIntentDestination | undefined => {\n const chain = normalizeSdkIntentChain(destination?.chain);\n if (!chain) return undefined;\n return {\n ...destination,\n amount: normalizeSdkIntentAmount(destination?.amount),\n chain,\n gas: normalizeSdkIntentGas(destination?.gas, chain.id),\n token: normalizeSdkIntentToken(destination?.token, chain.id),\n value: normalizeSdkIntentString(destination?.value),\n };\n};\n\nconst normalizeSwapIntentData = (intent: any): SwapIntentData | null => {\n const destination = normalizeSdkIntentDestination(intent?.destination);\n if (!destination) return null;\n\n return {\n ...intent,\n destination,\n sources: Array.isArray(intent?.sources)\n ? intent.sources\n .map(normalizeSdkIntentSource)\n .filter(\n (\n source: SwapIntentSource | undefined,\n ): source is SwapIntentSource => Boolean(source),\n )\n : [],\n };\n};\n\nconst normalizeSwapAndExecuteRequirementIntent = (\n intent: any,\n): SwapIntentData | null => {\n const requirement =\n intent?.executeRequirement ?? intent?.executionRequirement;\n if (!requirement) return null;\n const destination = normalizeSdkIntentDestination({\n amount: requirement?.token?.amount,\n chain: requirement?.chain,\n gas: requirement?.gas,\n token: requirement?.token,\n value: requirement?.token?.value,\n });\n if (!destination) return null;\n\n return {\n ...intent,\n bridgeProvider: normalizeBridgeProvider(intent?.bridgeProvider),\n destination,\n feesAndBuffer: intent?.feesAndBuffer,\n sources: [],\n };\n};\n\nconst normalizeRenderableSwapIntentData = (\n rawIntent: any,\n bridgeProvider?: BridgeProvider,\n): SwapIntentData | null => {\n const direct = normalizeSwapIntentData(rawIntent);\n const normalizedIntent = direct\n ? null\n : normalizeSwapIntentData(rawIntent?.normalizedIntent);\n const nestedSwap =\n direct || normalizedIntent\n ? null\n : normalizeSwapIntentData(rawIntent?.swap);\n const requirement =\n direct || normalizedIntent || nestedSwap\n ? null\n : normalizeSwapAndExecuteRequirementIntent(rawIntent);\n const normalized = direct ?? normalizedIntent ?? nestedSwap ?? requirement;\n if (!normalized) return null;\n\n return bridgeProvider === undefined\n ? normalized\n : { ...normalized, bridgeProvider };\n};\n\nconst logSwapPlanSteps = (\n eventType: \"plan_preview\" | \"plan_confirmed\",\n stepList: Array,\n rawSteps: unknown,\n) => {\n if (!isNexusWidgetSdkDebugEnabled()) return;\n console.log(`[NexusWidget SDK][swap] ${eventType} step list`, {\n count: stepList.length,\n eventType,\n rawSteps,\n steps: stepList.map((step, index) => summarizeSdkProgressStep(step, index)),\n });\n};\n\nconst logSwapPlanProgress = (\n event: any,\n step: SwapStepType | BridgeStepType,\n eventName: string,\n completed: boolean,\n) => {\n if (!isNexusWidgetSdkDebugEnabled()) return;\n console.log(\"[NexusWidget SDK][swap] plan_progress\", {\n completed,\n eventName,\n eventType: getSdkEventType(event),\n normalizedStep: summarizeSdkProgressStep(step),\n rawEvent: event,\n rawStep: event?.step,\n state: event?.state,\n stepType: event?.stepType,\n });\n};\n\nconst getFailureMessageForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n mode: NexusWidgetMode,\n autoRefundAvailable = false,\n) => {\n if (autoRefundAvailable) {\n return \"Swap Failed. Refund Initiated\";\n }\n\n const type = getProgressStepType(step);\n if (\n type.includes(\"CREATE_PERMIT_FOR_SOURCE_SWAP\") ||\n type.includes(\"CREATE_PERMIT_EOA_TO_EPHEMERAL\") ||\n type.includes(\"EOA_EXECUTE_CALL\") ||\n type.includes(\"SOURCE_SWAP\") ||\n type.includes(\"COLLECTION\")\n ) {\n return \"Collection Failed\";\n }\n if (type.includes(\"DESTINATION_SWAP\") || type.includes(\"FULFIL\")) {\n return \"Destination Swap Failed\";\n }\n if (\n type.includes(\"TRANSACTION\") ||\n type.includes(\"APPROVAL\") ||\n type.includes(\"DEPOSIT\")\n ) {\n return mode === \"send\"\n ? \"Send failed. Funds are in your wallet\"\n : mode === \"deposit\"\n ? \"Deposit failed. Funds are in your wallet\"\n : \"Swap Failed\";\n }\n if (\n type.includes(\"SWAP\") ||\n type.includes(\"BRIDGE\") ||\n type.includes(\"INTENT\") ||\n type.includes(\"DETERMINING\")\n ) {\n return \"Swap Failed\";\n }\n return mode === \"send\"\n ? \"Send failed. Funds are in your wallet\"\n : mode === \"deposit\"\n ? \"Deposit failed. Funds are in your wallet\"\n : \"Swap Failed\";\n};\n\nconst getBridgeTokenSymbolForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n) => {\n const rawStep = step as any;\n return (\n getNonEmptyString(\n rawStep?.bridgeToken?.symbol,\n rawStep?.data?.bridgeToken?.symbol,\n rawStep?.bridgeTokenSymbol,\n rawStep?.data?.bridgeTokenSymbol,\n rawStep?.swaps?.[0]?.input?.symbol,\n rawStep?.data?.swaps?.[0]?.input?.symbol,\n rawStep?.input?.symbol,\n rawStep?.data?.input?.symbol,\n rawStep?.asset?.symbol,\n rawStep?.data?.asset?.symbol,\n ) ?? \"USDC\"\n );\n};\n\nconst getFailureDescriptionForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n autoRefundAvailable = false,\n) => {\n if (autoRefundAvailable) return undefined;\n const type = getProgressStepType(step);\n if (!type.includes(\"DESTINATION_SWAP\")) return undefined;\n const bridgeTokenSymbol = getBridgeTokenSymbolForProgressStep(step);\n return `${bridgeTokenSymbol} has been bridged and you have those funds in your wallet.`;\n};\n\nconst getSourceRows = (entry: SwapHistoryEntry): HistorySourceRow[] => {\n const sources = entry.intentData?.sources ?? [];\n const displayDestinationSourceRow = getDisplayDestinationSourceRow(entry);\n if (sources.length > 0) {\n const sourceRows = sources.map((source, index) => {\n const fallback = entry.fromTokens.find(\n (token) =>\n token.chainId === source.chain.id &&\n (token.contractAddress?.toLowerCase() ===\n source.token.contractAddress?.toLowerCase() ||\n token.symbol === source.token.symbol),\n );\n\n return {\n key: `${source.chain.id}-${source.token.contractAddress}-${index}`,\n chainId: source.chain.id,\n contractAddress: source.token.contractAddress,\n tokenLogo: source.token.logo || fallback?.logo,\n chainLogo: source.chain.logo || fallback?.chainLogo,\n symbol: source.token.symbol,\n chainName: getShortChainName(source.chain.id, source.chain.name),\n amount: source.amount,\n value: source.value,\n };\n });\n\n return mergeHistorySourceRows(\n displayDestinationSourceRow\n ? [displayDestinationSourceRow, ...sourceRows]\n : sourceRows,\n );\n }\n\n const fallbackRows = entry.fromTokens.map((token, index) => ({\n key: `${token.chainId}-${token.contractAddress}-${index}`,\n chainId: token.chainId,\n contractAddress: token.contractAddress,\n tokenLogo: token.logo,\n chainLogo: token.chainLogo,\n symbol: token.symbol,\n chainName: getShortChainName(token.chainId, token.chainName),\n amount: token.userAmount || \"0\",\n value: token.balanceInFiat,\n }));\n\n return mergeHistorySourceRows(\n displayDestinationSourceRow\n ? [displayDestinationSourceRow, ...fallbackRows]\n : fallbackRows,\n );\n};\n\nfunction SourceRowsList({\n entry,\n maxHeight = 236,\n borderTopFirst = true,\n scrollAfterRows = 4,\n}: {\n entry: SwapHistoryEntry;\n maxHeight?: number;\n borderTopFirst?: boolean;\n scrollAfterRows?: number;\n}) {\n const rows = getSourceRows(entry);\n const shouldScroll = rows.length > scrollAfterRows;\n const scrollRef = useRef(null);\n\n return (\n
\n \n {rows.map((row, index) => (\n 0 ? \"1px solid #E8E8E7\" : \"none\",\n display: \"flex\",\n justifyContent: \"space-between\",\n minHeight: \"64px\",\n padding: \"10px 20px\",\n }}\n >\n \n \n
\n \n {row.symbol}\n \n \n on {row.chainName || \"Unknown chain\"}\n \n
\n
\n \n \n {formatTokenDisplay(row.amount)} {row.symbol}\n \n \n {formatUsdDisplay(row.value)}\n \n \n \n ))}\n \n {shouldScroll && (\n \n scrollRef.current?.scrollBy({ top: 72, behavior: \"smooth\" })\n }\n style={{\n alignItems: \"center\",\n background: \"#FFFFFE\",\n border: \"1px solid #E8E8E7\",\n borderRadius: \"999px\",\n bottom: \"6px\",\n boxShadow: \"0 2px 8px rgba(22,22,21,0.08)\",\n display: \"flex\",\n height: \"22px\",\n justifyContent: \"center\",\n left: \"50%\",\n padding: 0,\n position: \"absolute\",\n transform: \"translateX(-50%)\",\n width: \"22px\",\n }}\n type=\"button\"\n >\n \n \n )}\n \n );\n}\n\nfunction SwapReceiptPanel({\n entry,\n onDone,\n}: {\n entry: SwapHistoryEntry;\n onDone: () => void;\n}) {\n const [showSourceDetails, setShowSourceDetails] = useState(false);\n const destination = entry.intentData?.destination;\n const isFailed = entry.status === \"failed\";\n const isTimeout = entry.status === \"timeout\";\n const isDeposit = entry.mode === \"deposit\";\n const isSend = entry.mode === \"send\";\n const isRecipientTransfer = isSend || Boolean(entry.recipientAddress);\n const tokenSymbol = destination?.token.symbol || entry.toToken?.symbol || \"\";\n const chainName = getShortChainName(\n destination?.chain.id ?? entry.toToken?.chainId,\n destination?.chain.name || entry.toToken?.chainName || \"\",\n );\n const depositVenue =\n entry.opportunity?.title || entry.opportunity?.protocol || chainName;\n const amount = destination?.amount || \"\";\n const destinationLogo =\n destination?.token.logo ||\n entry.toToken?.logo ||\n entry.opportunity?.tokenLogo ||\n (isDeposit ? entry.opportunity?.logo : undefined);\n const requestedExactOutAmount =\n (isDeposit || isSend) && entry.requestedToAmount\n ? entry.requestedToAmount\n : undefined;\n const requestedExactOutValue =\n (isDeposit || isSend) && entry.requestedToValue\n ? entry.requestedToValue\n : undefined;\n const value = requestedExactOutValue || destination?.value;\n const displayAmount = requestedExactOutAmount || amount;\n const showIntentExplorer = hasValidIntentExplorer(entry);\n const intentLabel = entry.intentId\n ? `Intent #${entry.intentId}`\n : \"View Explorer\";\n const sourceRows = getSourceRows(entry);\n const sourceCount = sourceRows.length;\n const sourceTotalUsd = sourceRows.reduce(\n (sum, source) => sum.plus(parseDecimalLoose(source.value) ?? 0),\n new Decimal(0),\n );\n const defaultSwapFailureHeadline = entry.autoRefundAvailable\n ? \"Swap Failed. Refund Initiated\"\n : \"Swap Failed\";\n const entryFailureMessage =\n entry.status === \"timeout\" ? TIMEOUT_LABEL : entry.failureMessage;\n const storedFailureMessage =\n !entry.autoRefundAvailable && entryFailureMessage?.includes(\"Refund\")\n ? undefined\n : entryFailureMessage;\n const failureHeadline =\n storedFailureMessage ||\n (isDeposit\n ? \"Deposit failed. Funds are in your wallet\"\n : isRecipientTransfer\n ? \"Send failed. Funds are in your wallet\"\n : defaultSwapFailureHeadline);\n const failureDescription = isFailed ? entry.failureDescription : undefined;\n const timeoutHeadline = TIMEOUT_LABEL;\n const timeoutDescription = isTimeout\n ? entry.failureDescription ||\n \"This transaction is still pending. Check the intent explorer for the latest status.\"\n : undefined;\n const receiptLocation = isDeposit ? depositVenue : chainName;\n const receiptSummary = receiptLocation ? `on ${receiptLocation}` : \"\";\n\n return (\n \n \n \n \n \n {isFailed ? \"x\" : isTimeout ? \"!\" : \"✓\"}\n \n \n
\n {isTimeout\n ? timeoutHeadline\n : isFailed\n ? failureHeadline\n : isDeposit\n ? \"You deposited\"\n : isRecipientTransfer\n ? \"You sent\"\n : \"You received\"}\n
\n {(failureDescription || timeoutDescription) && (\n \n {failureDescription || timeoutDescription}\n \n )}\n \n {displayAmount ? formatTokenDisplay(displayAmount) : \"--\"}\n \n {tokenSymbol}\n \n \n
\n ≈ {formatUsdDisplay(value)}\n
\n {receiptSummary && (\n \n {receiptSummary}\n \n )}\n \n\n \n \n \n {isDeposit || isSend ? \"You Paid\" : \"You Swapped\"}\n \n \n \n {formatUsdDisplay(sourceTotalUsd)}\n \n setShowSourceDetails((current) => !current)}\n style={{\n alignItems: \"center\",\n background: \"transparent\",\n border: \"none\",\n color: \"var(--foreground-brand)\",\n cursor: \"pointer\",\n display: \"inline-flex\",\n fontFamily: uiFont,\n fontSize: \"12px\",\n gap: \"4px\",\n padding: 0,\n }}\n type=\"button\"\n >\n {showSourceDetails\n ? \"Hide Details\"\n : `${sourceCount} asset${sourceCount === 1 ? \"\" : \"s\"}`}\n \n \n \n \n \n
\n \n
\n \n {isRecipientTransfer && entry.recipientAddress && (\n \n \n Recipient\n \n \n \n )}\n {showIntentExplorer && (\n \n \n Intent Explorer\n \n \n {intentLabel} ↗\n \n \n )}\n {entry.finalExplorerUrl && (\n \n \n Final Transaction\n \n \n View Explorer ↗\n \n \n )}\n \n \n Total Fees\n \n \n {formatUsdDisplay(entry.feeUsd)}\n \n \n \n\n \n Done\n \n \n );\n}\n\nconst getRelativeTime = (time: number, now: number) => {\n const seconds = Math.max(1, Math.floor((now - time) / 1000));\n if (seconds < 60) return `${seconds} second${seconds === 1 ? \"\" : \"s\"} ago`;\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes} minute${minutes === 1 ? \"\" : \"s\"} ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours} hour${hours === 1 ? \"\" : \"s\"} ago`;\n const days = Math.floor(hours / 24);\n return `${days} day${days === 1 ? \"\" : \"s\"} ago`;\n};\n\nfunction HistoryStatusPill({ status }: { status: SwapHistoryStatus }) {\n const config =\n status === \"fulfilled\"\n ? { label: \"Fulfilled\", bg: \"#E8F6EF\", fg: \"#168A47\" }\n : status === \"pending\"\n ? { label: \"Pending\", bg: \"#FFF3DE\", fg: \"#B7791F\" }\n : status === \"timeout\"\n ? { label: TIMEOUT_LABEL, bg: \"#FFF3DE\", fg: \"#B7791F\" }\n : status === \"refund-initiated\"\n ? { label: \"Refund Initiated\", bg: \"#FFF3DE\", fg: \"#B7791F\" }\n : { label: \"Failed\", bg: \"#FFE6EA\", fg: \"#E92C2C\" };\n\n return (\n \n {config.label}\n \n );\n}\n\nfunction SwapHistoryPanel({\n entries,\n now,\n}: {\n entries: SwapHistoryEntry[];\n now: number;\n}) {\n if (entries.length === 0) {\n return (\n \n \n \n ↻\n \n \n \n No transactions yet\n \n \n Your transaction history will appear here once you make your first\n swap, deposit, or send.\n \n \n );\n }\n\n const sortedEntries = sortSwapHistoryEntries(entries);\n const shouldScroll = sortedEntries.length > 5;\n\n return (\n \n {sortedEntries.map((entry) => {\n const destination = entry.intentData?.destination;\n const destinationLogo =\n destination?.token.logo ||\n entry.toToken?.logo ||\n entry.opportunity?.tokenLogo ||\n \"\";\n const destinationChainLogo =\n destination?.chain.logo || entry.toToken?.chainLogo || \"\";\n const destinationChainName = getShortChainName(\n destination?.chain.id ?? entry.toToken?.chainId,\n destination?.chain.name || entry.toToken?.chainName || \"\",\n );\n const destinationSymbol =\n destination?.token.symbol || entry.toToken?.symbol || \"\";\n const destinationValue =\n (entry.mode === \"deposit\" || entry.mode === \"send\") &&\n entry.requestedToValue\n ? entry.requestedToValue\n : destination?.value;\n const destinationAmount =\n (entry.mode === \"deposit\" || entry.mode === \"send\") &&\n entry.requestedToAmount\n ? entry.requestedToAmount\n : destination?.amount || \"\";\n const canShowRefund =\n entry.status === \"failed\" && Boolean(entry.autoRefundAvailable);\n const status = canShowRefund ? \"refund-initiated\" : entry.status;\n const sourceRows = getSourceRows(entry);\n const historyExplorerUrl = getHistoryExplorerUrl(entry);\n\n return (\n \n \n \n \n
\n \n {destinationAmount\n ? formatTokenDisplay(destinationAmount)\n : \"--\"}\n \n {destinationSymbol}\n \n
\n \n ≈ {formatUsdDisplay(destinationValue)}\n \n \n \n \n \n \n {getRelativeTime(entry.createdAt ?? entry.startedAt, now)}\n \n \n \n\n {canShowRefund && (\n \n \n Refund Initiated\n \n \n )}\n\n \n \n {sourceRows.length > 0 && (\n \n )}\n \n →\n \n \n \n {historyExplorerUrl && (\n \n \n View\n \n \n \n \n \n \n )}\n \n \n );\n })}\n \n );\n}\n\n// ---------------------------------------------------------------------------\n// NexusWidget\n// ---------------------------------------------------------------------------\n\nexport function NexusWidget(props: NexusWidgetProps) {\n return (\n \n
\n Something went wrong\n
\n \n An unexpected error occurred. Please refresh the page or try\n resetting the widget.\n \n window.location.reload()}\n style={{\n backgroundColor: \"var(--foreground-brand)\",\n border: \"none\",\n borderRadius: \"8px\",\n color: \"#FFFFFE\",\n cursor: \"pointer\",\n fontSize: \"15px\",\n fontWeight: 500,\n padding: \"8px 16px\",\n transition: \"background-color 0.15s ease-out\",\n }}\n type=\"button\"\n >\n Reload Page\n \n \n }\n >\n \n \n );\n}\n\nfunction NexusWidgetInner({\n config: rawConfig,\n embed = true,\n className,\n connectedAddress,\n open: controlledOpen,\n onOpenChange,\n defaultOpen = false,\n onComplete,\n onConnectClick,\n onStart,\n onError,\n onReceiveAssetChange,\n onClose,\n onConnectWallet,\n}: NexusWidgetProps) {\n const {\n nexusSDK,\n bridgableBalance,\n swapBalance,\n swapBalanceLoading,\n getFiatValue,\n resolveTokenUsdRate,\n swapSupportedChainsAndTokens,\n supportedChainsAndTokens,\n fetchSwapBalance,\n handleInit,\n swapIntent: providerSwapIntent,\n network,\n loading: nexusLoading,\n } = useNexus();\n const appConfig = useMemo(\n () => ({\n nexusNetwork: network ?? \"mainnet\",\n }),\n [network],\n );\n const isSwapBalancePending =\n Boolean(nexusSDK) && (swapBalanceLoading || swapBalance === null);\n\n const normalizedWidgetConfig = useMemo(\n () => normalizeNexusWidgetConfig(rawConfig),\n [rawConfig],\n );\n const config = normalizedWidgetConfig.config;\n const activeMode = normalizedWidgetConfig.activeMode;\n const amountInputConfig = normalizedWidgetConfig.amountInput;\n const appearanceConfig = normalizedWidgetConfig.appearance;\n const configuredDepositOptions = normalizedWidgetConfig.depositOptions;\n const isConfiguredAmountFixed = normalizedWidgetConfig.isAmountFixed;\n const isConfiguredRecipientLocked = normalizedWidgetConfig.isRecipientLocked;\n const primaryColor = normalizeNexusWidgetPrimaryColor(\n appearanceConfig?.primaryColor,\n );\n const primaryButtonBackground =\n primaryColor ?? NEXUS_WIDGET_DEFAULT_PRIMARY_COLOR;\n const primaryButtonForeground = getReadableTextColor(primaryButtonBackground);\n const theme = useMemo(\n () => ({\n ...nexusWidgetTheme,\n colors: {\n ...nexusWidgetTheme.colors,\n primary: \"var(--foreground-brand)\",\n primaryText: \"var(--foreground-brand)\",\n },\n }),\n [primaryColor],\n );\n const configuredDeposit = getConfiguredDeposit(config);\n const configuredDepositIdentity = getDepositConfigIdentity(configuredDeposit);\n const configuredDestinationTokenOptions = useMemo(() => {\n const destination = rawConfig.destination;\n const destinationChainId = getConfiguredDestinationChainId(destination);\n const destinationTokens =\n rawConfig.mode === \"deposit\"\n ? configuredDepositOptions.map(toSwapTokenOptionFromDepositConfig)\n : destination?.tokens && destinationChainId\n ? destination.tokens\n .map((token) =>\n toSwapTokenOptionFromConfiguredDestinationToken(\n token,\n destinationChainId,\n ),\n )\n .filter((token): token is SwapTokenOption => Boolean(token))\n : [];\n\n const tokensByKey = new Map();\n for (const token of destinationTokens) {\n tokensByKey.set(getTokenSelectionKey(token), token);\n }\n return Array.from(tokensByKey.values());\n }, [configuredDepositOptions, rawConfig.destination]);\n if (activeMode === \"deposit\" && !configuredDeposit) {\n throw new Error(\n \"NexusWidget deposit mode requires destination.chain, at least one destination token, depositAddress, and executeDeposit.\",\n );\n }\n const showCloseButton = !embed && Boolean(onClose);\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const isControlledOpen = controlledOpen !== undefined;\n const isModalOpen = isControlledOpen ? controlledOpen : internalOpen;\n\n // Preload receive tokens once SDK is available\n useEffect(() => {\n if (nexusSDK) {\n console.log(\n \"[preloadReceiveTokens] Calling preloadReceiveTokens from NexusWidget useEffect (nexusSDK available)\",\n );\n preloadReceiveTokens();\n }\n }, [nexusSDK]);\n\n const {\n address: accountAddress,\n connector,\n status: walletStatus,\n } = useAccount();\n const {\n connectors,\n connectAsync,\n isPending: isWalletConnectPending,\n } = useConnect();\n const { data: walletClient } = useWalletClient();\n const { data: connectorClient } = useConnectorClient();\n const publicClient = usePublicClient();\n const walletClientAddress = walletClient?.account?.address;\n const ownerAddress =\n connectedAddress &&\n isAddress(connectedAddress) &&\n connectedAddress.toLowerCase() !== zeroAddress\n ? connectedAddress\n : accountAddress &&\n isAddress(accountAddress) &&\n accountAddress.toLowerCase() !== zeroAddress\n ? accountAddress\n : walletClientAddress &&\n isAddress(walletClientAddress) &&\n walletClientAddress.toLowerCase() !== zeroAddress\n ? walletClientAddress\n : undefined;\n const getEffectiveWalletProvider = useCallback(\n async (activeConnector = connector) => {\n let connectorProvider: unknown;\n try {\n connectorProvider = await activeConnector?.getProvider();\n } catch {\n connectorProvider = undefined;\n }\n\n const connectorClientProvider = connectorClient\n ? {\n request: (args: unknown) => connectorClient.request(args as any),\n }\n : undefined;\n const walletClientProvider = walletClient\n ? {\n request: (args: unknown) => walletClient.request(args as any),\n }\n : undefined;\n const windowProvider =\n typeof window !== \"undefined\"\n ? (window as Window & { ethereum?: EthereumProvider }).ethereum\n : undefined;\n const effectiveProvider =\n connectorProvider &&\n typeof (connectorProvider as EthereumProvider).request === \"function\"\n ? (connectorProvider as EthereumProvider)\n : (connectorClientProvider ?? walletClientProvider ?? windowProvider);\n\n return effectiveProvider &&\n typeof effectiveProvider.request === \"function\"\n ? (effectiveProvider as EthereumProvider)\n : undefined;\n },\n [connector, connectorClient, walletClient],\n );\n const historyStorageKey = getSwapHistoryStorageKey(ownerAddress);\n\n useEffect(() => {\n if (walletStatus !== \"connected\" || !ownerAddress) {\n return;\n }\n\n let cancelled = false;\n void (async () => {\n const effectiveProvider = await getEffectiveWalletProvider();\n if (cancelled || !effectiveProvider) {\n return;\n }\n await handleInit(effectiveProvider, ownerAddress);\n })().catch((error) => {\n if (!cancelled) {\n console.error(\"Unable to initialize Nexus for wallet address:\", error);\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [getEffectiveWalletProvider, handleInit, ownerAddress, walletStatus]);\n\n // Global form state\n const [amount, setAmount] = useState(\"\");\n const [recipientAddress, setRecipientAddress] = useState(\"\");\n const [editingAssetIndex, setEditingAssetIndex] = useState(\n null,\n );\n const [txError, setTxError] = useState(null);\n const [walletActionPending, setWalletActionPending] = useState(false);\n const defaultRecipientAddress = ownerAddress ?? \"\";\n const effectiveRecipientAddress =\n activeMode === \"swap\"\n ? recipientAddress || defaultRecipientAddress\n : recipientAddress;\n const hasSameOwnerSendRecipient =\n activeMode === \"send\" &&\n Boolean(\n ownerAddress &&\n recipientAddress &&\n isAddress(recipientAddress) &&\n recipientAddress.toLowerCase() === ownerAddress.toLowerCase(),\n );\n const hasCustomSwapRecipient =\n activeMode === \"swap\" &&\n Boolean(\n recipientAddress &&\n (!defaultRecipientAddress ||\n recipientAddress.toLowerCase() !==\n defaultRecipientAddress.toLowerCase()),\n );\n const transferRecipientAddress =\n activeMode === \"send\"\n ? recipientAddress\n : hasCustomSwapRecipient\n ? recipientAddress\n : undefined;\n\n // Swap-specific\n const [swapType, setSwapType] = useState(\"exactIn\");\n const [swapStep, setSwapStep] = useState(\"idle\");\n const [depositFundingStep, setDepositFundingStep] =\n useState(\"method\");\n const [depositOnrampSessionState, setDepositOnrampSessionState] = useState<\n string | null\n >(null);\n const drawerCloseTimerRef = useRef | null>(\n null,\n );\n const terminalBalanceRefreshTimerRef = useRef | null>(null);\n const [closingDrawerStep, setClosingDrawerStep] = useState(\n null,\n );\n const rootContentRef = useRef(null);\n const [rootContentHeight, setRootContentHeight] = useState(\n null,\n );\n const rootContentHeightRef = useRef(null);\n const [hasMeasuredRootContent, setHasMeasuredRootContent] = useState(false);\n const [shouldAnimateRootHeight, setShouldAnimateRootHeight] = useState(false);\n const [isPreviewTransitioning, setIsPreviewTransitioning] = useState(false);\n const rootHeightTransitionTimerRef = useRef | null>(null);\n const [fromTokens, setFromTokens] = useState([]);\n const [sourceSelectionTouched, setSourceSelectionTouched] = useState(false);\n const [sourceSelectionRevision, setSourceSelectionRevision] = useState(0);\n const [\n sourceSelectionRequiredUsdDisplay,\n setSourceSelectionRequiredUsdDisplay,\n ] = useState(undefined);\n const [sourcePickerDraftTokens, setSourcePickerDraftTokens] = useState<\n SwapTokenOption[] | null\n >(null);\n const sourcePickerDraftTokensRef = useRef(null);\n const sourcePickerDraftDepositFilterRef = useRef(\"all\");\n const sourcePickerDraftTouchedRef = useRef(false);\n const sourcePickerDraftModeRef = useRef<\"all\" | \"selected\">(\"all\");\n const [exactOutQuoteSourceMode, setExactOutQuoteSourceMode] = useState<\n \"all\" | \"selected\"\n >(\"all\");\n const exactOutQuoteSourceModeRef = useRef<\"all\" | \"selected\">(\"all\");\n const [toToken, setToToken] = useState(\n undefined,\n );\n const [fromTokensQuoteKey, setFromTokensQuoteKey] = useState(\"\");\n\n useEffect(() => {\n setDepositFundingStep(activeMode === \"deposit\" ? \"method\" : \"wallet\");\n }, [activeMode]);\n\n useEffect(() => {\n const key = getSourceTokensQuoteKey(\n activeMode === \"swap\" && swapType === \"exactIn\"\n ? getReadyExactInSourceTokens(fromTokens)\n : fromTokens,\n );\n setFromTokensQuoteKey(key);\n }, [activeMode, swapType, fromTokens]);\n const toTokenQuoteKey = getTokenQuoteKey(toToken);\n const appliedTokenPrefillRef = useRef(null);\n\n useEffect(() => {\n if (!toToken?.chainId || !toToken.contractAddress) return;\n\n let active = true;\n const selectedTokenKey = getTokenSelectionKey(toToken);\n const applyLoadedReceiveToken = () => {\n if (!active) return;\n const loadedToken = getCachedReceiveTokenMatch(toToken);\n if (!loadedToken) return;\n\n setToToken((current) => {\n if (!current || getTokenSelectionKey(current) !== selectedTokenKey) {\n return current;\n }\n\n const chainMeta = current.chainId\n ? CHAIN_METADATA[current.chainId]\n : undefined;\n const next = {\n ...current,\n chainLogo:\n loadedToken.chainLogo || current.chainLogo || chainMeta?.logo,\n chainName: getShortChainName(\n current.chainId,\n loadedToken.chainName || current.chainName || chainMeta?.name,\n ),\n decimals: loadedToken.decimals ?? current.decimals,\n logo: loadedToken.logo || current.logo,\n name: loadedToken.name || current.name,\n priceUSD: loadedToken.priceUSD ?? current.priceUSD,\n symbol: loadedToken.symbol || current.symbol,\n };\n\n if (\n current.decimals === next.decimals &&\n current.chainLogo === next.chainLogo &&\n current.chainName === next.chainName &&\n current.logo === next.logo &&\n current.name === next.name &&\n current.priceUSD === next.priceUSD &&\n current.symbol === next.symbol\n ) {\n return current;\n }\n\n return next;\n });\n };\n\n applyLoadedReceiveToken();\n const receiveTokensPromise = preloadReceiveTokens();\n receiveTokensPromise?.then(applyLoadedReceiveToken).catch((error) => {\n if (active) {\n console.warn(\"Unable to refresh receive token metadata\", error);\n }\n });\n\n return () => {\n active = false;\n };\n }, [\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n const setExactOutQuoteSourceModeValue = useCallback(\n (mode: \"all\" | \"selected\") => {\n exactOutQuoteSourceModeRef.current = mode;\n setExactOutQuoteSourceMode(mode);\n },\n [],\n );\n\n useEffect(() => {\n if (!nexusSDK) return;\n void fetchSwapBalance();\n }, [fetchSwapBalance, nexusSDK]);\n\n useEffect(() => {\n setSourceSelectionTouched(false);\n setExactOutQuoteSourceModeValue(\"all\");\n }, [activeMode, setExactOutQuoteSourceModeValue]);\n\n useEffect(() => {\n if (activeMode !== \"swap\") return;\n\n setRecipientAddress((current) => {\n if (!defaultRecipientAddress) return current ? \"\" : current;\n return current.toLowerCase() === defaultRecipientAddress.toLowerCase()\n ? current\n : defaultRecipientAddress;\n });\n }, [activeMode, defaultRecipientAddress]);\n\n const {\n steps,\n seed,\n onStepsList,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n const [progressEvents, setProgressEvents] = useState<\n NexusWidgetProgressEvent[]\n >([]);\n const progressEventsRef = useRef([]);\n const swapStepsListRef = useRef([]);\n const [failedProgressStep, setFailedProgressStep] = useState<\n SwapStepType | BridgeStepType | null\n >(null);\n const [explorerUrls, setExplorerUrls] = useState<{\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n }>({ sourceExplorerUrl: null, destinationExplorerUrl: null });\n const swapRunIdRef = useRef(0);\n\n const widgetSessionIdRef = useRef(\n typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`,\n );\n const widgetAttemptIdRef = useRef(null);\n const widgetOpenedTsRef = useRef(Date.now());\n const previewViewedTsRef = useRef(null);\n const previewConfirmedTsRef = useRef(null);\n const attemptCountRef = useRef(0);\n const fundsMovedRef = useRef(false);\n const intentUrlRef = useRef(null);\n const hadSimulationSuccessRef = useRef(false);\n const hadPreviewViewedRef = useRef(false);\n const widgetOpenedFiredRef = useRef(false);\n const reachedTerminalRef = useRef(false);\n const lastIntentSourceTokensRef = useRef([]);\n const lastAutoIntentSourceTokensRef = useRef([]);\n const immediateQuoteAfterSourceEditRef = useRef(false);\n const amountEnteredLastValueRef = useRef(\"\");\n const lastInputMethodRef = useRef<\n | \"typed\"\n | \"percent_20\"\n | \"percent_25\"\n | \"percent_50\"\n | \"percent_75\"\n | \"percent_max\"\n >(\"typed\");\n const prevSourceTouchedRef = useRef(false);\n const previousAutoSourceCountRef = useRef(0);\n const analyticsRef = useRef<{\n track: (event: string, properties?: Record) => void;\n } | null>(null);\n const selectedOpportunityRef = useRef<\n NexusWidgetDepositOpportunityConfig | undefined\n >(undefined);\n\n const newAttemptId = useCallback(() => {\n return typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`;\n }, []);\n\n const rotateAttempt = useCallback(() => {\n widgetAttemptIdRef.current = newAttemptId();\n previewViewedTsRef.current = null;\n previewConfirmedTsRef.current = null;\n fundsMovedRef.current = false;\n intentUrlRef.current = null;\n hadSimulationSuccessRef.current = false;\n hadPreviewViewedRef.current = false;\n reachedTerminalRef.current = false;\n }, [newAttemptId]);\n const [intentToAmount, setIntentToAmount] = useState(\n undefined,\n );\n const [intentFeeUsd, setIntentFeeUsd] = useState(\n undefined,\n );\n const [intentLoading, setIntentLoading] = useState(false);\n const [quoteRefreshing, setQuoteRefreshing] = useState(false);\n const [receiveMaxCalculating, setReceiveMaxCalculating] = useState(false);\n const [maxCalculationPercent, setMaxCalculationPercent] = useState<\n number | null\n >(null);\n const intentDestinationUsdRateCacheRef = useRef<\n Record\n >({});\n const intentSymbolUsdRateCacheRef = useRef<\n Record\n >({});\n const predictiveQuoteCacheRef = useRef<\n Record\n >({});\n const predictiveQuoteRunRef = useRef(0);\n const latestIntentFetchRunIdRef = useRef(0);\n const intentFetchTimingRef = useRef(null);\n const intentUiRenderLoggedRunIdRef = useRef(null);\n const [intentFetchTiming, setIntentFetchTiming] =\n useState(null);\n const [predictiveQuote, setPredictiveQuote] =\n useState(null);\n const maxPercentRunRef = useRef(0);\n const [previewQuoteRefreshing, setPreviewQuoteRefreshing] = useState(false);\n const [quoteRefreshProgress, setQuoteRefreshProgress] = useState(0);\n const [quoteRefreshSecondsRemaining, setQuoteRefreshSecondsRemaining] =\n useState(0);\n const [intentData, setIntentData] = useState(null);\n const [swapQuoteIssue, setSwapQuoteIssue] = useState(\n null,\n );\n const [receiveAmountIssue, setReceiveAmountIssue] =\n useState(null);\n const receiveAmountIssueRef = useRef(null);\n const receiveAmountIssueKeyRef = useRef(\"\");\n const [transferExplorerUrl, setTransferExplorerUrl] = useState(\n null,\n );\n const swapStepRef = useRef(swapStep);\n const syncingIntentSourcesRef = useRef(false);\n const lastSwapIntentRefreshAtRef = useRef(0);\n const [destinationBalance, setDestinationBalance] = useState(\n null,\n );\n const [swapHistory, setSwapHistory] = useState(() =>\n readSwapHistoryFromStorage(historyStorageKey),\n );\n const [currentSwapId, setCurrentSwapId] = useState(null);\n const [historyNow, setHistoryNow] = useState(() => Date.now());\n const currentSwapIdRef = useRef(null);\n const currentSwapStartedAtRef = useRef(0);\n const historyStorageKeyRef = useRef(historyStorageKey);\n const skipNextHistoryPersistRef = useRef(false);\n const explorerUrlsRef = useRef<{\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n }>({ sourceExplorerUrl: null, destinationExplorerUrl: null });\n const activeQuoteInputKeyRef = useRef(\"\");\n\n // Ref to store swap intent hook allow/deny callbacks\n const swapIntentRef = useRef<{\n intent?: SwapIntentData;\n allow: () => void;\n deny: () => void;\n refresh: () => Promise;\n runId?: number;\n quoteInputKey?: string;\n } | null>(null);\n\n const startIntentFetchTiming = useCallback(\n ({\n background,\n operation,\n quoteInputKey,\n runId,\n }: {\n background: boolean;\n operation: string;\n quoteInputKey: string;\n runId: number;\n }) => {\n const startTimeMs = Date.now();\n const timing: IntentFetchTiming = {\n background,\n operation,\n quoteInputKey,\n runId,\n startTimeMs,\n walletAddress: ownerAddress ?? null,\n };\n\n latestIntentFetchRunIdRef.current = runId;\n intentFetchTimingRef.current = timing;\n intentUiRenderLoggedRunIdRef.current = null;\n setIntentFetchTiming(timing);\n console.log(\"[NexusWidget] intent fetch start\", {\n runId,\n walletAddress: timing.walletAddress,\n operation,\n quoteInputKey,\n background,\n dateNow: startTimeMs,\n date: new Date(startTimeMs),\n });\n return timing;\n },\n [ownerAddress],\n );\n\n const finishIntentFetchTiming = useCallback(\n (runId: number, status: \"resolved\" | \"failed\" = \"resolved\") => {\n const timing = intentFetchTimingRef.current;\n if (!timing || timing.runId !== runId) return;\n if (timing.endTimeMs !== undefined) return;\n\n const endTimeMs = Date.now();\n if (latestIntentFetchRunIdRef.current !== runId) {\n return;\n }\n\n const fetchDurationMs = endTimeMs - timing.startTimeMs;\n const completedTiming: IntentFetchTiming = {\n ...timing,\n endTimeMs,\n fetchDurationMs,\n };\n intentFetchTimingRef.current = completedTiming;\n setIntentFetchTiming(completedTiming);\n console.log(\"[NexusWidget] intent fetch end\", {\n runId,\n walletAddress: timing.walletAddress,\n operation: timing.operation,\n quoteInputKey: timing.quoteInputKey,\n status,\n dateNow: endTimeMs,\n date: new Date(endTimeMs),\n });\n console.log(\"[NexusWidget] intent fetch total\", {\n runId,\n walletAddress: timing.walletAddress,\n operation: timing.operation,\n quoteInputKey: timing.quoteInputKey,\n totalTimeSeconds: fetchDurationMs / 1000,\n elapsedMs: fetchDurationMs,\n dateNow: endTimeMs,\n });\n },\n [],\n );\n\n useEffect(() => {\n swapStepRef.current = swapStep;\n }, [swapStep]);\n\n useEffect(() => {\n return () => {\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n }\n if (terminalBalanceRefreshTimerRef.current) {\n clearTimeout(terminalBalanceRefreshTimerRef.current);\n }\n if (rootHeightTransitionTimerRef.current) {\n clearTimeout(rootHeightTransitionTimerRef.current);\n }\n };\n }, []);\n\n const isQuoteEditLocked = useCallback(\n () => swapStepRef.current === \"choose-swap-asset\",\n [],\n );\n\n const getQuoteRequestDelay = useCallback(() => {\n if (immediateQuoteAfterSourceEditRef.current) {\n immediateQuoteAfterSourceEditRef.current = false;\n return 0;\n }\n return EXACT_OUT_INPUT_DEBOUNCE_MS;\n }, []);\n\n const startRootHeightTransition = useCallback(() => {\n setShouldAnimateRootHeight(true);\n if (rootHeightTransitionTimerRef.current) {\n clearTimeout(rootHeightTransitionTimerRef.current);\n }\n rootHeightTransitionTimerRef.current = setTimeout(() => {\n setShouldAnimateRootHeight(false);\n rootHeightTransitionTimerRef.current = null;\n }, ROOT_HEIGHT_TRANSITION_MS);\n }, []);\n\n const closeDrawerToIdle = useCallback(() => {\n const isDrawerStep =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n swapStep === \"enter-recipient\";\n\n if (!isDrawerStep) {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n return;\n }\n\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n }\n\n setClosingDrawerStep(swapStep);\n drawerCloseTimerRef.current = setTimeout(() => {\n if (\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\"\n ) {\n startRootHeightTransition();\n }\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n setClosingDrawerStep(null);\n drawerCloseTimerRef.current = null;\n }, DRAWER_CLOSE_MS);\n }, [startRootHeightTransition, swapStep]);\n\n const openDrawerStep = useCallback(\n (nextStep: SwapStep) => {\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n drawerCloseTimerRef.current = null;\n }\n if (\n nextStep === \"choose-swap-asset\" ||\n nextStep === \"choose-receive-asset\"\n ) {\n startRootHeightTransition();\n }\n setClosingDrawerStep(null);\n swapStepRef.current = nextStep;\n setSwapStep(nextStep);\n },\n [startRootHeightTransition],\n );\n\n const syncRootContentHeight = useCallback((animate = false) => {\n const element = rootContentRef.current;\n if (!element) return;\n\n const nextHeight = Math.ceil(\n Math.max(element.getBoundingClientRect().height, element.scrollHeight),\n );\n if (nextHeight <= 0) return;\n\n if (rootContentHeightRef.current === nextHeight) {\n setHasMeasuredRootContent(true);\n return;\n }\n\n rootContentHeightRef.current = nextHeight;\n setShouldAnimateRootHeight(animate);\n if (rootHeightTransitionTimerRef.current) {\n clearTimeout(rootHeightTransitionTimerRef.current);\n rootHeightTransitionTimerRef.current = null;\n }\n if (animate) {\n rootHeightTransitionTimerRef.current = setTimeout(() => {\n setShouldAnimateRootHeight(false);\n rootHeightTransitionTimerRef.current = null;\n }, ROOT_HEIGHT_TRANSITION_MS);\n }\n setRootContentHeight(nextHeight);\n setHasMeasuredRootContent(true);\n }, []);\n\n useLayoutEffect(() => {\n syncRootContentHeight(true);\n\n const element = rootContentRef.current;\n if (!element || typeof ResizeObserver === \"undefined\") return;\n\n let frame = 0;\n const observer = new ResizeObserver(() => {\n if (frame) {\n window.cancelAnimationFrame(frame);\n }\n frame = window.requestAnimationFrame(() => {\n syncRootContentHeight(false);\n });\n });\n\n observer.observe(element);\n\n return () => {\n if (frame) {\n window.cancelAnimationFrame(frame);\n }\n observer.disconnect();\n };\n }, [activeMode, swapStep, syncRootContentHeight]);\n\n useEffect(() => {\n currentSwapIdRef.current = currentSwapId;\n }, [currentSwapId]);\n\n useEffect(() => {\n if (historyStorageKeyRef.current === historyStorageKey) return;\n historyStorageKeyRef.current = historyStorageKey;\n skipNextHistoryPersistRef.current = true;\n setSwapHistory(readSwapHistoryFromStorage(historyStorageKey));\n }, [historyStorageKey]);\n\n useEffect(() => {\n if (skipNextHistoryPersistRef.current) {\n skipNextHistoryPersistRef.current = false;\n return;\n }\n\n writeSwapHistoryToStorage(historyStorageKey, swapHistory);\n }, [historyStorageKey, swapHistory]);\n\n useEffect(() => {\n if (swapStep !== \"history\") return;\n const timer = window.setInterval(() => setHistoryNow(Date.now()), 30000);\n return () => window.clearInterval(timer);\n }, [swapStep]);\n\n const normalizeAddress = (value?: string | null) =>\n (value ?? \"\").toLowerCase();\n\n const getTokenLookupAddress = (value?: string | null) => {\n if (!value) return \"\";\n return isNativeTokenAddress(value) ? zeroAddress : value.toLowerCase();\n };\n\n const findBalanceTokenLogo = (\n chainId?: number,\n contractAddress?: string,\n symbol?: string,\n ) => {\n if (!chainId) return \"\";\n const lookupAddress = getTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n let symbolMatchLogo = \"\";\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== chainId) continue;\n const breakdownAddress = getTokenLookupAddress(\n breakdown.contractAddress,\n );\n const addressMatches =\n lookupAddress && breakdownAddress === lookupAddress;\n const symbolMatches =\n lookupSymbol &&\n [breakdown.symbol, asset.symbol]\n .filter(Boolean)\n .some((candidate) => candidate.toUpperCase() === lookupSymbol);\n const logo =\n getLogoFromMetadata(breakdown) || getLogoFromMetadata(asset);\n\n if (addressMatches && logo) return logo;\n if (!symbolMatchLogo && symbolMatches && logo) {\n symbolMatchLogo = logo;\n }\n }\n }\n\n return symbolMatchLogo;\n };\n\n const findSupportedTokenLogo = (\n chains: any[] | null | undefined,\n chainId?: number,\n contractAddress?: string,\n symbol?: string,\n ) => {\n if (!chainId) return \"\";\n const chain = chains?.find(\n (item: any) => Number(item?.id ?? item?.chainId) === chainId,\n );\n const tokens = chain?.tokens ?? chain?.assets ?? [];\n const lookupAddress = getTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n\n const addressMatch =\n lookupAddress &&\n tokens.find((token: any) => {\n const tokenAddress = getTokenLookupAddress(\n token?.contractAddress ?? token?.address ?? token?.tokenAddress,\n );\n return tokenAddress === lookupAddress;\n });\n const symbolMatch =\n lookupSymbol &&\n tokens.find((token: any) => {\n const tokenSymbol = (token?.symbol ?? token?.tokenSymbol ?? \"\")\n .toString()\n .toUpperCase();\n return tokenSymbol === lookupSymbol;\n });\n\n return (\n getLogoFromMetadata(addressMatch) || getLogoFromMetadata(symbolMatch)\n );\n };\n\n const resolveTokenLogo = (\n token:\n | {\n address?: string;\n contractAddress?: string;\n logo?: string;\n symbol?: string;\n tokenAddress?: string;\n tokenSymbol?: string;\n }\n | undefined,\n chainId?: number,\n ) => {\n const explicitLogo = getLogoFromMetadata(token);\n if (explicitLogo) return explicitLogo;\n\n const contractAddress =\n token?.contractAddress ?? token?.address ?? token?.tokenAddress;\n const symbol = token?.symbol ?? token?.tokenSymbol;\n const citreaToken = findCitreaReceiveToken({\n address: contractAddress,\n chainId,\n symbol,\n });\n const tokenMeta = symbol\n ? TOKEN_METADATA[symbol as keyof typeof TOKEN_METADATA]\n : undefined;\n\n return (\n findBalanceTokenLogo(chainId, contractAddress, symbol) ||\n findSupportedTokenLogo(\n swapSupportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol,\n ) ||\n findSupportedTokenLogo(\n supportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol,\n ) ||\n citreaToken?.logo ||\n tokenMeta?.logo ||\n (contractAddress && isNativeTokenAddress(contractAddress)\n ? CHAIN_METADATA[chainId ?? 0]?.logo\n : \"\") ||\n \"\"\n );\n };\n\n const enrichIntentToken = <\n T extends {\n address?: string;\n contractAddress?: string;\n logo?: string;\n symbol?: string;\n tokenAddress?: string;\n tokenSymbol?: string;\n },\n >(\n token: T,\n chainId?: number,\n ): T => {\n const logo = resolveTokenLogo(token, chainId);\n return logo && logo !== token.logo ? { ...token, logo } : token;\n };\n\n const enrichTokenOptionLogo = (\n token?: SwapTokenOption,\n ): SwapTokenOption | undefined => {\n if (!token) return undefined;\n\n const chainMeta = token.chainId ? CHAIN_METADATA[token.chainId] : undefined;\n const logo = resolveTokenLogo(token, token.chainId);\n const next = {\n ...token,\n chainLogo: token.chainLogo || chainMeta?.logo,\n logo: token.logo || logo,\n };\n\n if (next.chainLogo === token.chainLogo && next.logo === token.logo) {\n return token;\n }\n\n return next;\n };\n\n const enrichSwapIntentTokenMetadata = (\n intent: SwapIntentData,\n ): SwapIntentData => ({\n ...intent,\n destination: {\n ...intent.destination,\n token: enrichIntentToken(\n intent.destination.token,\n intent.destination.chain.id,\n ),\n },\n sources: (intent.sources ?? []).map((source) => ({\n ...source,\n token: enrichIntentToken(source.token, source.chain.id),\n })),\n });\n\n const buildIntentSourceToken = (\n source: SwapIntentData[\"sources\"][number],\n ): SwapTokenOption => {\n let matchedAsset: any;\n let matchedBreakdown: any;\n const sourceAddress = normalizeAddress(source.token.contractAddress);\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const addressMatches =\n normalizeAddress(breakdown.contractAddress) === sourceAddress;\n const symbolMatches =\n breakdown.symbol === source.token.symbol ||\n asset.symbol === source.token.symbol;\n if (\n breakdown.chain?.id === source.chain.id &&\n (addressMatches || symbolMatches)\n ) {\n matchedAsset = asset;\n matchedBreakdown = breakdown;\n break;\n }\n }\n if (matchedBreakdown) break;\n }\n\n const chainMeta = CHAIN_METADATA[source.chain.id];\n const sourceValue = Number((source as any).value ?? 0);\n const isNativeSource = isNativeTokenAddress(source.token.contractAddress);\n const nativeCurrency = chainMeta?.nativeCurrency;\n const sourceSymbol =\n isNativeSource && (!source.token.symbol || !matchedAsset?.logo)\n ? nativeCurrency?.symbol || source.token.symbol\n : source.token.symbol || nativeCurrency?.symbol || \"\";\n const sourceDecimals =\n isNativeSource && nativeCurrency?.decimals !== undefined\n ? nativeCurrency.decimals\n : source.token.decimals;\n const sourceLogo = resolveTokenLogo(source.token, source.chain.id);\n\n return {\n contractAddress: source.token.contractAddress,\n symbol: sourceSymbol,\n name: sourceSymbol,\n logo: sourceLogo ?? \"\",\n decimals: sourceDecimals,\n balance: matchedBreakdown?.balance\n ? `${matchedBreakdown.balance} ${sourceSymbol}`\n : `${source.amount} ${sourceSymbol}`,\n balanceInFiat:\n matchedBreakdown?.balanceInFiat != null\n ? `$${Number(matchedBreakdown.balanceInFiat).toFixed(2)}`\n : Number.isFinite(sourceValue)\n ? `$${sourceValue.toFixed(2)}`\n : \"$0.00\",\n chainId: source.chain.id,\n chainName: getShortChainName(\n source.chain.id,\n chainMeta?.name ?? source.chain.name,\n ),\n chainLogo: chainMeta?.logo ?? source.chain.logo,\n userAmount: source.amount,\n userAmountUsd: Number.isFinite(sourceValue) ? source.value : undefined,\n userAmountMode: \"token\",\n };\n };\n\n const clearPendingSwapIntent = (\n clearQuote = true,\n options: { keepQuoteRefreshing?: boolean } = {},\n ) => {\n swapRunIdRef.current += 1;\n swapIntentRef.current?.deny();\n swapIntentRef.current = null;\n setIntentLoading(false);\n setTxError(null);\n if (!options.keepQuoteRefreshing) {\n setQuoteRefreshing(false);\n }\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setSwapQuoteIssue(null);\n resetProgressEvents();\n if (swapStepsListRef.current.length > 0 || steps.length > 0) {\n swapStepsListRef.current = [];\n resetSteps();\n } else {\n swapStepsListRef.current = [];\n }\n if (clearQuote) {\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n if (!options.keepQuoteRefreshing) {\n setPredictiveQuote(null);\n }\n }\n };\n\n const clearSelectedSources = () => {\n setFromTokens((current) => (current.length === 0 ? current : []));\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setExactOutQuoteSourceModeValue(\"all\");\n };\n\n const resetExactOutSourcesToAuto = () => {\n setFromTokens((current) => (current.length === 0 ? current : []));\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setExactOutQuoteSourceModeValue(\"all\");\n setSourceSelectionRevision((current) => current + 1);\n };\n\n const getSourceAmountInput = (tokens: SwapTokenOption[]) => {\n const total = tokens.reduce(\n (sum, token) => sum + Number(token.userAmount || 0),\n 0,\n );\n return total > 0 ? String(total) : \"\";\n };\n\n const parseFiatNumber = (value: unknown) => {\n if (value === null || value === undefined || value === \"\") return undefined;\n if (Decimal.isDecimal(value)) return value;\n const cleaned = normalizeDecimalInputText(value);\n if (!cleaned || cleaned === \"-\" || cleaned === \".\" || cleaned === \"-.\") {\n return undefined;\n }\n try {\n const parsed = new Decimal(cleaned);\n return parsed.isFinite() ? parsed : undefined;\n } catch {\n return undefined;\n }\n };\n\n const minimumSourceUsd = new Decimal(1);\n const hasMinimumSourceUsdValue = (value: unknown) =>\n (parseFiatNumber(value) ?? new Decimal(0)).gte(minimumSourceUsd);\n const hasMinimumSourceUsdBalance = (\n token: Pick,\n ) => hasMinimumSourceUsdValue(token.balanceInFiat);\n const filterMinimumSourceUsdTokens = (tokens: SwapTokenOption[]) =>\n tokens.filter(hasMinimumSourceUsdBalance);\n\n const getTokenUsdRateCacheKeyFromParts = (\n chainId?: number,\n contractAddress?: string,\n symbol?: string,\n ) => {\n if (!chainId || !symbol) return \"\";\n return [\n chainId,\n (contractAddress || zeroAddress).toLowerCase(),\n symbol.toUpperCase(),\n ].join(\":\");\n };\n\n const getTokenUsdRateCacheKey = (\n token?: Pick,\n ) =>\n getTokenUsdRateCacheKeyFromParts(\n token?.chainId,\n token?.contractAddress,\n token?.symbol,\n );\n\n const getSymbolUsdRateCacheKey = (symbol?: string) =>\n symbol ? symbol.trim().toUpperCase() : \"\";\n\n const getCachedIntentUsdRate = (\n token?: Pick,\n ) => {\n const tokenKey = getTokenUsdRateCacheKey(token);\n const cached = tokenKey\n ? intentDestinationUsdRateCacheRef.current[tokenKey]\n : undefined;\n const rate = parseFiatNumber(cached?.rate);\n return rate && rate.gt(0) ? rate : undefined;\n };\n\n const cacheDestinationUsdRateFromIntent = (\n intent?: SwapIntentData | null,\n ) => {\n const destination = intent?.destination;\n const amount = parseFiatNumber(destination?.amount);\n const value = parseFiatNumber(destination?.value);\n const chainId = destination?.chain?.id;\n const symbol = destination?.token?.symbol;\n\n if (\n !amount ||\n !value ||\n amount.lte(0) ||\n value.lte(0) ||\n !chainId ||\n !symbol\n ) {\n return;\n }\n\n const rate = value.div(amount);\n if (!rate.isFinite() || rate.lte(0)) return;\n\n const cached: CachedIntentUsdRate = {\n amount: amount.toFixed(),\n rate: rate.toDecimalPlaces(18).toFixed(),\n updatedAt: Date.now(),\n value: value.toFixed(),\n };\n const tokenKey = getTokenUsdRateCacheKeyFromParts(\n chainId,\n destination?.token?.contractAddress,\n symbol,\n );\n if (tokenKey) {\n intentDestinationUsdRateCacheRef.current[tokenKey] = cached;\n }\n\n const symbolKey = getSymbolUsdRateCacheKey(symbol);\n if (symbolKey) {\n intentSymbolUsdRateCacheRef.current[symbolKey] = cached;\n }\n };\n\n const getSwapBalanceTotalUsd = () =>\n (swapBalance ?? []).reduce((sum, asset) => {\n const breakdown = asset.breakdown ?? [];\n if (breakdown.length > 0) {\n return sum.plus(\n breakdown.reduce((breakdownSum, item) => {\n const value = parseFiatNumber(item.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd)\n ? breakdownSum.plus(value)\n : breakdownSum;\n }, new Decimal(0)),\n );\n }\n\n const value = parseFiatNumber(asset.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd) ? sum.plus(value) : sum;\n }, new Decimal(0));\n\n const getTokenUsdRate = (token: SwapTokenOption) => {\n const tokenBalance = parseFiatNumber(token.balance) ?? new Decimal(0);\n const fiatBalance = parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n if (tokenBalance.gt(0) && fiatBalance.gt(0)) {\n return fiatBalance.div(tokenBalance);\n }\n\n const priceUsd = parseFiatNumber(token.priceUSD);\n if (priceUsd && priceUsd.gt(0)) {\n return priceUsd;\n }\n\n const fallbackRate = getFiatValue(1, token.symbol);\n if (Number.isFinite(fallbackRate) && fallbackRate > 0) {\n return new Decimal(fallbackRate);\n }\n\n return getCachedIntentUsdRate(token) ?? new Decimal(0);\n };\n const getUsdRateForSymbol = (symbol?: string) => {\n if (!symbol) return new Decimal(0);\n const fiat = getFiatValue(1, symbol);\n if (Number.isFinite(fiat) && fiat > 0) {\n return new Decimal(fiat);\n }\n\n const cached =\n intentSymbolUsdRateCacheRef.current[getSymbolUsdRateCacheKey(symbol)];\n const rate = parseFiatNumber(cached?.rate);\n return rate && rate.gt(0) ? rate : new Decimal(0);\n };\n\n const getFeeTokenLookupAddress = (value?: string | null) => {\n const lower = (value ?? \"\").toLowerCase();\n if (!lower || lower === \"0x\" || isNativeTokenAddress(lower)) {\n return zeroAddress;\n }\n return lower;\n };\n\n const getTokenUsdRateFromBalances = (\n chainId?: number,\n contractAddress?: string,\n symbol?: string,\n ) => {\n if (!chainId) return undefined;\n const lookupAddress = getFeeTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== chainId) continue;\n const breakdownAddress = getFeeTokenLookupAddress(\n breakdown.contractAddress,\n );\n const addressMatches =\n lookupAddress && breakdownAddress === lookupAddress;\n const symbolMatches =\n lookupSymbol &&\n [breakdown.symbol, asset.symbol]\n .filter(Boolean)\n .some((candidate) => candidate.toUpperCase() === lookupSymbol);\n if (!addressMatches && !symbolMatches) continue;\n\n const balance = parseFiatNumber(breakdown.balance);\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n if (balance && fiatBalance && balance.gt(0) && fiatBalance.gt(0)) {\n return fiatBalance.div(balance);\n }\n }\n }\n\n return undefined;\n };\n\n const getTokenUsdRateFromSupportedChains = (\n chains: any[] | null | undefined,\n chainId?: number,\n contractAddress?: string,\n symbol?: string,\n ) => {\n if (!chainId) return undefined;\n const chain = chains?.find(\n (item: any) => Number(item?.id ?? item?.chainId) === chainId,\n );\n const tokens = chain?.tokens ?? chain?.assets ?? [];\n const lookupAddress = getFeeTokenLookupAddress(contractAddress);\n const lookupSymbol = symbol?.toUpperCase();\n const matchedToken = tokens.find((token: any) => {\n const tokenAddress = getFeeTokenLookupAddress(\n token?.contractAddress ?? token?.address ?? token?.tokenAddress,\n );\n const addressMatches = lookupAddress && tokenAddress === lookupAddress;\n const tokenSymbol = (token?.symbol ?? token?.tokenSymbol ?? \"\")\n .toString()\n .toUpperCase();\n const symbolMatches = lookupSymbol && tokenSymbol === lookupSymbol;\n return addressMatches || symbolMatches;\n });\n const priceUsd = parseFiatNumber(\n matchedToken?.priceUSD ??\n matchedToken?.priceUsd ??\n matchedToken?.usdPrice ??\n matchedToken?.price,\n );\n return priceUsd && priceUsd.gt(0) ? priceUsd : undefined;\n };\n\n const getIntentDestinationGasUsdValue = (intent?: SwapIntentData | null) => {\n const gas = intent?.destination?.gas;\n const explicitValue = parseFiatNumber(gas?.value);\n if (explicitValue && explicitValue.gt(0)) return explicitValue;\n\n const amount = parseFiatNumber(gas?.amount);\n if (!amount || amount.lte(0)) return undefined;\n\n const chainId = intent?.destination?.chain?.id;\n const contractAddress = gas?.token?.contractAddress;\n const symbol = gas?.token?.symbol;\n const rate =\n getTokenUsdRateFromBalances(chainId, contractAddress, symbol) ??\n getTokenUsdRateFromSupportedChains(\n swapSupportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol,\n ) ??\n getTokenUsdRateFromSupportedChains(\n supportedChainsAndTokens,\n chainId,\n contractAddress,\n symbol,\n ) ??\n getUsdRateForSymbol(symbol);\n\n return rate.gt(0) ? amount.mul(rate) : undefined;\n };\n\n const getCachedDestinationUsdRate = (token?: SwapTokenOption) => {\n return getCachedIntentUsdRate(token);\n };\n\n const resolveUsdRateForSymbol = async (symbol?: string) => {\n if (!symbol) return new Decimal(0);\n\n const localRate = getUsdRateForSymbol(symbol);\n if (localRate.gt(0)) return localRate;\n\n try {\n const resolvedRate = await resolveTokenUsdRate(symbol);\n return resolvedRate && resolvedRate > 0\n ? new Decimal(resolvedRate)\n : new Decimal(0);\n } catch {\n return new Decimal(0);\n }\n };\n\n const getTokenUsdValue = (\n token: SwapTokenOption,\n fallbackAmount?: string,\n ) => {\n const amountNumber =\n parseFiatNumber(token.userAmount || fallbackAmount) ?? new Decimal(0);\n if (amountNumber.lte(0)) return new Decimal(0);\n const quotedUsd = parseFiatNumber(token.userAmountUsd);\n if (quotedUsd && quotedUsd.gte(0)) return quotedUsd;\n if (token.userAmountMode === \"usd\") return amountNumber;\n\n const rate = getTokenUsdRate(token);\n return rate.gt(0) ? amountNumber.mul(rate) : new Decimal(0);\n };\n\n const getTokenBalanceAmount = (token: SwapTokenOption) =>\n parseFiatNumber(token.balance) ?? new Decimal(0);\n\n const getTokenBalanceUsd = (token: SwapTokenOption) =>\n parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n\n const getSdkExactOutSourcePriority = (token: SwapTokenOption) => {\n const isSameChain =\n Boolean(token.chainId && toToken?.chainId) &&\n token.chainId === toToken?.chainId;\n const isSameToken =\n Boolean(token.contractAddress && toToken?.contractAddress) &&\n ((isNativeTokenAddress(token.contractAddress) &&\n isNativeTokenAddress(toToken?.contractAddress)) ||\n token.contractAddress.toLowerCase() ===\n toToken?.contractAddress.toLowerCase());\n const isStable = isSdkExactOutStableSymbol(token.symbol);\n const isGas = isNativeTokenAddress(token.contractAddress);\n const isEthereum = token.chainId === ETHEREUM_MAINNET_CHAIN_ID;\n\n if (isSameChain) {\n if (isSameToken) return 1;\n if (isStable) return 2;\n if (isGas) return 3;\n return 4;\n }\n\n if (isEthereum) {\n if (isSameToken) return 8;\n if (isStable) return 9;\n if (isGas) return 10;\n return 11;\n }\n\n if (isSameToken) return 5;\n if (isStable) return 6;\n return 7;\n };\n\n const sortExactOutSourcesBySdkPriority = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const priorityDiff =\n getSdkExactOutSourcePriority(a) - getSdkExactOutSourcePriority(b);\n if (priorityDiff !== 0) return priorityDiff;\n\n const usdDiff = getTokenBalanceUsd(b).cmp(getTokenBalanceUsd(a));\n if (usdDiff !== 0) return usdDiff;\n\n return `${a.symbol} ${a.chainName ?? \"\"}`.localeCompare(\n `${b.symbol} ${b.chainName ?? \"\"}`,\n );\n });\n\n const getTokenAmountForUsd = (token: SwapTokenOption, usdAmount: Decimal) => {\n const rate = getTokenUsdRate(token);\n if (rate.lte(0) || usdAmount.lte(0)) return new Decimal(0);\n return usdAmount.div(rate);\n };\n\n const getUsdForTokenAmount = (\n token: SwapTokenOption,\n tokenAmount: Decimal,\n ) => {\n const rate = getTokenUsdRate(token);\n if (rate.lte(0) || tokenAmount.lte(0)) return new Decimal(0);\n return tokenAmount.mul(rate);\n };\n\n const getExactOutDestinationBalanceCoverage = ({\n requestedAmount,\n requestedUsd,\n producedAmount,\n producedUsd,\n token = toToken,\n }: {\n requestedAmount?: Decimal;\n requestedUsd?: Decimal;\n producedAmount?: Decimal;\n producedUsd?: Decimal;\n token?: SwapTokenOption;\n }) => {\n if (\n (activeMode !== \"deposit\" && activeMode !== \"send\") ||\n !token ||\n !requestedAmount ||\n requestedAmount.lte(0)\n ) {\n return null;\n }\n\n const balanceAmount =\n parseFiatNumber(destinationBalance) ??\n parseFiatNumber(token.balance) ??\n new Decimal(0);\n if (balanceAmount.lte(0)) return null;\n\n const externalAmount =\n producedAmount && producedAmount.gt(0) ? producedAmount : new Decimal(0);\n const uncoveredAmount = Decimal.max(\n requestedAmount.minus(externalAmount),\n new Decimal(0),\n );\n const coveredAmount = Decimal.min(balanceAmount, uncoveredAmount);\n if (coveredAmount.lte(0)) return null;\n\n const requestedRate =\n requestedUsd && requestedUsd.gt(0)\n ? requestedUsd.div(requestedAmount)\n : undefined;\n const producedRate =\n producedUsd && producedUsd.gt(0) && producedAmount && producedAmount.gt(0)\n ? producedUsd.div(producedAmount)\n : undefined;\n const fallbackRate = getTokenUsdRate(token);\n const usdRate =\n requestedRate && requestedRate.gt(0)\n ? requestedRate\n : producedRate && producedRate.gt(0)\n ? producedRate\n : fallbackRate.gt(0)\n ? fallbackRate\n : undefined;\n\n return {\n amount: coveredAmount,\n usd: usdRate ? coveredAmount.mul(usdRate) : undefined,\n };\n };\n\n const buildDestinationBalanceDisplayToken = (\n coverage: ReturnType,\n token?: SwapTokenOption,\n ): SwapTokenOption | null => {\n if (!coverage || !token || coverage.amount.lte(0)) return null;\n\n const amount = coverage.amount\n .toDecimalPlaces(Math.max(0, token.decimals ?? 18), Decimal.ROUND_DOWN)\n .toFixed();\n const usd = coverage.usd?.toDecimalPlaces(6, Decimal.ROUND_DOWN).toFixed();\n const balanceUsd = coverage.usd\n ? `$${coverage.usd.toDecimalPlaces(2, Decimal.ROUND_DOWN).toFixed()}`\n : token.balanceInFiat || \"$0.00\";\n\n return {\n ...token,\n balance: `${amount} ${token.symbol}`,\n balanceInFiat: balanceUsd,\n userAmount: amount,\n userAmountMode: \"token\",\n userAmountUsd: usd,\n };\n };\n\n const cacheSymbolUsdRate = (symbol: string | undefined, rate: Decimal) => {\n const symbolKey = getSymbolUsdRateCacheKey(symbol);\n if (!symbolKey || rate.lte(0)) return;\n\n intentSymbolUsdRateCacheRef.current[symbolKey] = {\n amount: \"1\",\n rate: rate.toDecimalPlaces(18).toFixed(),\n updatedAt: Date.now(),\n value: rate.toFixed(),\n };\n };\n\n const getPredictiveDestinationKey = (token?: SwapTokenOption) => {\n const tokenKey = getTokenUsdRateCacheKey(token);\n return tokenKey ? `destination:${tokenKey}` : \"\";\n };\n\n const getPredictiveSourceKey = (token: SwapTokenOption) =>\n [\n token.chainId ?? \"unknown\",\n (token.contractAddress || zeroAddress).toLowerCase(),\n token.symbol.toUpperCase(),\n ].join(\":\");\n\n const getPredictiveQuoteCacheKey = (\n mode = activeMode,\n type = swapType,\n destination = toToken,\n sources = fromTokens,\n ) => {\n const destinationKey = getPredictiveDestinationKey(destination);\n if (!destinationKey) return \"\";\n if (mode !== \"swap\" || type !== \"exactIn\") {\n return `exactOut:${destinationKey}`;\n }\n\n const sourceKey = getExpandedSourceTokens(sources)\n .map(getPredictiveSourceKey)\n .sort()\n .join(\"+\");\n return sourceKey ? `exactIn:${sourceKey}->${destinationKey}` : \"\";\n };\n\n const getPredictiveDisplayAmount = (\n amount: Decimal,\n token?: Pick,\n ) => {\n const decimals = Math.min(\n PREDICTIVE_QUOTE_DISPLAY_DECIMALS,\n Math.max(0, token?.decimals ?? 18),\n );\n return amount.toDecimalPlaces(decimals, Decimal.ROUND_DOWN).toFixed();\n };\n\n const resolveUsdRateForToken = async (token?: SwapTokenOption) => {\n if (!token?.symbol) return new Decimal(0);\n\n const localRate = getTokenUsdRate(token);\n if (localRate.gt(0)) return localRate;\n\n const resolvedRate = await resolveUsdRateForSymbol(token.symbol);\n if (resolvedRate.gt(0)) {\n cacheSymbolUsdRate(token.symbol, resolvedRate);\n }\n return resolvedRate;\n };\n\n const getDestinationReceiveLimitUsd = (token?: SwapTokenOption) => {\n if (!token?.chainId) return undefined;\n const limit = DESTINATION_RECEIVE_LIMIT_USD_BY_CHAIN_ID[token.chainId];\n return limit ? new Decimal(limit) : undefined;\n };\n\n const getImmediateDestinationReceiveUsdRate = (token?: SwapTokenOption) => {\n const priceUsd = parseFiatNumber(token?.priceUSD);\n if (priceUsd && priceUsd.gt(0)) return priceUsd;\n\n const cachedRate = getCachedDestinationUsdRate(token);\n if (cachedRate && cachedRate.gt(0)) return cachedRate;\n\n if (!token) return undefined;\n const localRate = getTokenUsdRate(token);\n return localRate.gt(0) ? localRate : undefined;\n };\n\n const getExactInSourceUsdForReceiveLimit = (\n sourceTokens: SwapTokenOption[],\n inputAmount: string,\n ) => {\n if (sourceTokens.length === 0) return undefined;\n let hasPositiveSourceAmount = false;\n let totalUsd = new Decimal(0);\n\n for (const token of sourceTokens) {\n const fallbackAmount =\n sourceTokens.length === 1 ? inputAmount : undefined;\n const sourceAmount = parseFiatNumber(token.userAmount || fallbackAmount);\n if (!sourceAmount || sourceAmount.lte(0)) continue;\n\n hasPositiveSourceAmount = true;\n const sourceUsd = getTokenUsdValue(token, fallbackAmount);\n if (sourceUsd.lte(0)) return undefined;\n totalUsd = totalUsd.plus(sourceUsd);\n }\n\n return hasPositiveSourceAmount ? totalUsd : undefined;\n };\n\n const buildReceiveAmountIssue = ({\n destinationRate,\n destinationToken = toToken,\n inputAmount = amount,\n mode = activeMode,\n sourceTokens = fromTokens,\n type = swapType,\n }: {\n destinationRate?: Decimal;\n destinationToken?: SwapTokenOption;\n inputAmount?: string;\n mode?: NexusWidgetMode;\n sourceTokens?: SwapTokenOption[];\n type?: SwapType;\n } = {}): ReceiveAmountIssue | null => {\n const limit = getDestinationReceiveLimitUsd(destinationToken);\n if (!limit || !destinationToken) return null;\n\n const parsedAmount = parseFiatNumber(inputAmount);\n if (!parsedAmount || parsedAmount.lte(0)) return null;\n if (mode === \"swap\" && type === \"exactIn\" && sourceTokens.length === 0) {\n return null;\n }\n\n const chainName = getShortChainName(\n destinationToken.chainId,\n destinationToken.chainName,\n );\n const resolvedDestinationRate =\n destinationRate ??\n getImmediateDestinationReceiveUsdRate(destinationToken);\n\n if (!resolvedDestinationRate || resolvedDestinationRate.lte(0)) {\n return {\n ctaLabel: \"Price unavailable\",\n message: `Unable to price ${destinationToken.symbol} on ${chainName}. Select another receive token.`,\n type: \"unpricedReceiveToken\",\n };\n }\n\n let receiveUsd: Decimal | undefined;\n if (mode === \"swap\" && type === \"exactIn\") {\n receiveUsd = getExactInSourceUsdForReceiveLimit(\n sourceTokens,\n inputAmount,\n );\n if (!receiveUsd || receiveUsd.lte(0)) {\n return {\n ctaLabel: \"Price unavailable\",\n message: `Unable to price selected assets for ${chainName}'s receive limit.`,\n type: \"unpricedReceiveToken\",\n };\n }\n } else if (mode === \"deposit\" && depositAmountMode === \"usd\") {\n receiveUsd = parsedAmount;\n } else {\n receiveUsd = parsedAmount.mul(resolvedDestinationRate);\n }\n\n if (receiveUsd.gt(limit)) {\n return {\n ctaLabel: \"Receive limit exceeded\",\n message: `Maximum receive amount on ${chainName} is ${formatUsdDisplay(limit)}.`,\n type: \"receiveLimitExceeded\",\n };\n }\n\n return null;\n };\n\n const buildConfiguredAmountIssue = (\n inputAmount = amount,\n ): ReceiveAmountIssue | null => {\n if (!amountInputConfig || !inputAmount) return null;\n const parsedAmount = parseFiatNumber(inputAmount);\n if (!parsedAmount || parsedAmount.lte(0)) return null;\n\n const minAmount = parseFiatNumber(amountInputConfig.min);\n if (minAmount && parsedAmount.lt(minAmount)) {\n return {\n ctaLabel: \"Below minimum\",\n message: `Minimum amount is ${minAmount.toFixed()}.`,\n type: \"configuredAmountLimit\",\n };\n }\n\n const maxAmount = parseFiatNumber(amountInputConfig.max);\n if (maxAmount && parsedAmount.gt(maxAmount)) {\n return {\n ctaLabel: \"Above maximum\",\n message: `Maximum amount is ${maxAmount.toFixed()}.`,\n type: \"configuredAmountLimit\",\n };\n }\n\n return null;\n };\n\n const clearPreviewForBlockingAmountIssue = () => {\n clearPendingSwapIntent(true);\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setTxError(null);\n setSwapQuoteIssue(null);\n };\n\n const applyReceiveAmountIssue = (issue: ReceiveAmountIssue | null) => {\n const key = issue ? `${issue.type}:${issue.message}` : \"\";\n receiveAmountIssueRef.current = issue;\n if (receiveAmountIssueKeyRef.current !== key) {\n receiveAmountIssueKeyRef.current = key;\n setReceiveAmountIssue(issue);\n }\n if (!issue) return;\n\n clearPendingSwapIntent(true);\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setTxError(null);\n };\n\n const getPredictiveExactInSourceTokens = () => {\n const expanded = getExpandedSourceTokens(fromTokens);\n if (expanded.length === 0) return [];\n\n return expanded\n .map((token) => {\n const userAmount =\n token.userAmount ||\n (expanded.length === 1 && hasPositiveDecimalInput(amount)\n ? amount\n : \"\");\n return { ...token, userAmount };\n })\n .filter((token) => hasPositiveDecimalInput(token.userAmount));\n };\n\n const sortUnifiedSourceTokens = (tokens: SwapTokenOption[]) =>\n [...tokens].sort((a, b) => {\n const fiatDiff = getTokenBalanceUsd(b).cmp(getTokenBalanceUsd(a));\n if (fiatDiff !== 0) return fiatDiff;\n return getTokenBalanceAmount(b).cmp(getTokenBalanceAmount(a));\n });\n\n const allocateUnifiedExactInToken = (\n token: SwapTokenOption,\n fallbackAmount?: string,\n ) => {\n if (!token.isUnified || !token.sourceTokens?.length) return [token];\n\n const rawAmount =\n parseFiatNumber(token.userAmount || fallbackAmount) ?? new Decimal(0);\n if (rawAmount.lte(0)) return [];\n\n const sortedSources = sortUnifiedSourceTokens(token.sourceTokens).filter(\n (source) =>\n source.chainId &&\n source.contractAddress &&\n getTokenBalanceAmount(source).gt(0) &&\n hasMinimumSourceUsdBalance(source),\n );\n const allocated: SwapTokenOption[] = [];\n\n if (token.userAmountMode === \"usd\") {\n let remainingUsd = rawAmount;\n\n for (const source of sortedSources) {\n if (remainingUsd.lte(0)) break;\n\n const availableUsd = getTokenBalanceUsd(source);\n if (availableUsd.lte(0)) continue;\n\n const targetUsd = Decimal.min(remainingUsd, availableUsd);\n const tokenAmount = getTokenAmountForUsd(\n source,\n targetUsd,\n ).toDecimalPlaces(\n Math.max(0, source.decimals || 18),\n Decimal.ROUND_DOWN,\n );\n if (tokenAmount.lte(0)) continue;\n\n const actualUsd = getUsdForTokenAmount(source, tokenAmount);\n allocated.push({\n ...source,\n userAmount: tokenAmount.toFixed(),\n userAmountMode: \"token\",\n userAmountUsd: actualUsd\n .toDecimalPlaces(6, Decimal.ROUND_DOWN)\n .toFixed(),\n });\n remainingUsd = remainingUsd.minus(targetUsd);\n }\n\n return allocated;\n }\n\n let remainingTokenAmount = rawAmount;\n\n for (const source of sortedSources) {\n if (remainingTokenAmount.lte(0)) break;\n\n const availableTokenAmount = getTokenBalanceAmount(source);\n if (availableTokenAmount.lte(0)) continue;\n\n const tokenAmount = Decimal.min(\n remainingTokenAmount,\n availableTokenAmount,\n ).toDecimalPlaces(Math.max(0, source.decimals || 18), Decimal.ROUND_DOWN);\n if (tokenAmount.lte(0)) continue;\n\n const actualUsd = getUsdForTokenAmount(source, tokenAmount);\n allocated.push({\n ...source,\n userAmount: tokenAmount.toFixed(),\n userAmountMode: \"token\",\n userAmountUsd: actualUsd\n .toDecimalPlaces(6, Decimal.ROUND_DOWN)\n .toFixed(),\n });\n remainingTokenAmount = remainingTokenAmount.minus(tokenAmount);\n }\n\n return allocated;\n };\n\n const getExactInSourceTokens = (\n tokens: SwapTokenOption[],\n fallbackAmount?: string,\n ) =>\n tokens\n .flatMap((token) =>\n token.isUnified\n ? allocateUnifiedExactInToken(token, fallbackAmount)\n : [token],\n )\n .filter(hasMinimumSourceUsdBalance);\n\n const hasPositiveDecimalInput = (value: unknown) =>\n Boolean(parseFiatNumber(value)?.gt(0));\n\n const hasPositiveSourceSpend = (token: SwapTokenOption) =>\n hasPositiveDecimalInput(token.userAmount) ||\n hasPositiveDecimalInput(token.userAmountUsd);\n\n const getReadyExactInSourceTokens = (tokens: SwapTokenOption[]) =>\n getExactInSourceTokens(tokens).filter(\n (token) =>\n Boolean(token.chainId && token.contractAddress) &&\n hasPositiveDecimalInput(token.userAmount),\n );\n\n const hasReadyExactInSwapInput = (\n tokens: SwapTokenOption[],\n destination?: SwapTokenOption,\n ) =>\n Boolean(\n destination?.chainId &&\n destination.contractAddress &&\n getReadyExactInSourceTokens(tokens).length > 0,\n );\n\n const getExpandedSourceTokens = (tokens: SwapTokenOption[]) => {\n const expanded = tokens.flatMap((token) =>\n token.isUnified && token.sourceTokens?.length\n ? token.sourceTokens\n : [token],\n );\n const seen = new Set();\n return expanded.filter((token) => {\n if (!token.chainId || !token.contractAddress) return false;\n if (\n !isSwapSupportedBySdkChainList(\n token.chainId,\n swapSupportedChainsAndTokens,\n )\n ) {\n return false;\n }\n const key = `${token.chainId}-${token.contractAddress.toLowerCase()}`;\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n });\n };\n\n const getNativeGasBalanceForChain = (chainId: number) => {\n const nativeSymbol =\n CHAIN_METADATA[chainId]?.nativeCurrency?.symbol?.toUpperCase();\n let balance = new Decimal(0);\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== chainId) continue;\n const breakdownSymbol = (\n breakdown.symbol ??\n asset.symbol ??\n \"\"\n ).toUpperCase();\n const assetSymbol = (asset.symbol ?? \"\").toUpperCase();\n const isNativeBalance =\n isNativeTokenAddress(breakdown.contractAddress) ||\n Boolean(\n nativeSymbol &&\n (breakdownSymbol === nativeSymbol || assetSymbol === nativeSymbol),\n );\n\n if (!isNativeBalance) continue;\n balance = balance.plus(\n parseFiatNumber(breakdown.balance) ?? new Decimal(0),\n );\n }\n }\n\n return balance;\n };\n\n const hasGasForSource = (token: SwapTokenOption) => {\n if (!token.chainId || !token.contractAddress) return false;\n const tokenBalance = parseFiatNumber(token.balance) ?? new Decimal(0);\n if (tokenBalance.lte(0)) return false;\n if (isNativeTokenAddress(token.contractAddress)) return true;\n return getNativeGasBalanceForChain(token.chainId).gt(0);\n };\n\n const getGasCapableBalanceSourceTokens = () => {\n const tokens: SwapTokenOption[] = [];\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const chainId = breakdown.chain?.id;\n const contractAddress = breakdown.contractAddress;\n const balance = parseFiatNumber(breakdown.balance) ?? new Decimal(0);\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n if (\n !chainId ||\n !contractAddress ||\n balance.lte(0) ||\n !fiatBalance ||\n fiatBalance.lt(minimumSourceUsd)\n )\n continue;\n\n const chainMeta = CHAIN_METADATA[chainId];\n const symbol = breakdown.symbol ?? asset.symbol;\n tokens.push({\n chainId,\n chainLogo: chainMeta?.logo ?? breakdown.chain?.logo,\n chainName: getShortChainName(\n chainId,\n chainMeta?.name ?? breakdown.chain?.name,\n ),\n contractAddress,\n decimals: breakdown.decimals ?? asset.decimals ?? 18,\n logo: asset.logo ?? \"\",\n name: symbol,\n symbol,\n balance: `${breakdown.balance} ${symbol}`,\n balanceInFiat:\n fiatBalance !== undefined\n ? `$${fiatBalance.toDecimalPlaces(2).toFixed()}`\n : \"$0.00\",\n });\n }\n }\n\n return getExpandedSourceTokens(tokens).filter(hasGasForSource);\n };\n\n const getMinimumBalanceSourceTokens = () =>\n filterMinimumSourceUsdTokens(\n getExpandedSourceTokens(\n swapBalance\n ? deriveTokenOptions(swapBalance, swapSupportedChainsAndTokens)\n : [],\n ),\n );\n const getHeldDestinationTokenOption = () => {\n if (!toToken?.chainId || !toToken.contractAddress) return undefined;\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const chainId = breakdown.chain?.id;\n if (chainId !== toToken.chainId) continue;\n\n const breakdownAddress = breakdown.contractAddress;\n const addressMatches =\n breakdownAddress &&\n (breakdownAddress.toLowerCase() ===\n toToken.contractAddress.toLowerCase() ||\n (isNativeTokenAddress(breakdownAddress) &&\n isNativeTokenAddress(toToken.contractAddress)));\n const symbolMatches =\n (breakdown.symbol ?? asset.symbol ?? \"\").toUpperCase() ===\n toToken.symbol.toUpperCase();\n\n if (!addressMatches && !symbolMatches) continue;\n\n const balanceAmount = parseFiatNumber(breakdown.balance);\n if (!balanceAmount || balanceAmount.lte(0)) continue;\n\n const chainMeta = CHAIN_METADATA[chainId];\n const symbol = breakdown.symbol ?? asset.symbol ?? toToken.symbol;\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n\n return {\n chainId,\n chainLogo:\n chainMeta?.logo ?? breakdown.chain?.logo ?? toToken.chainLogo,\n chainName: getShortChainName(\n chainId,\n chainMeta?.name ?? breakdown.chain?.name ?? toToken.chainName,\n ),\n contractAddress: breakdown.contractAddress ?? toToken.contractAddress,\n decimals:\n breakdown.decimals ?? asset.decimals ?? toToken.decimals ?? 18,\n logo: asset.logo ?? toToken.logo,\n name: symbol,\n symbol,\n balance: `${breakdown.balance} ${symbol}`,\n balanceInFiat:\n fiatBalance !== undefined\n ? `$${fiatBalance.toDecimalPlaces(2).toFixed()}`\n : \"$0.00\",\n } satisfies SwapTokenOption;\n }\n }\n\n return undefined;\n };\n\n const getDepositDestinationForSourceSelection = () => {\n const destination =\n activeMode === \"deposit\" ? selectedOpportunity : toToken;\n const chainId = destination?.chainId;\n const tokenAddress =\n activeMode === \"deposit\"\n ? selectedOpportunity?.tokenAddress\n : toToken?.contractAddress;\n const tokenSymbol =\n activeMode === \"deposit\"\n ? selectedOpportunity?.tokenSymbol\n : toToken?.symbol;\n\n if (!chainId || !tokenAddress || !tokenSymbol) return undefined;\n\n return {\n chainId,\n tokenAddress: tokenAddress as `0x${string}`,\n tokenSymbol,\n };\n };\n const getDestinationSourceIdForDeposit = () => {\n const destination = getDepositDestinationForSourceSelection();\n return destination\n ? getDepositSourceId(destination.tokenAddress, destination.chainId)\n : undefined;\n };\n const getDepositSourceTargetUsd = () => {\n if (activeMode !== \"deposit\") return undefined;\n const requestedUsd = depositUsdDecimal;\n if (!requestedUsd || requestedUsd.lte(0)) return undefined;\n\n const coverage = getExactOutDestinationBalanceCoverage({\n requestedAmount: depositTokenAmountForQuote,\n requestedUsd,\n token: toToken,\n });\n return Decimal.max(\n requestedUsd.minus(coverage?.usd ?? new Decimal(0)),\n new Decimal(0),\n );\n };\n\n const getDepositSourceIdsFromTokens = (tokens: SwapTokenOption[]) =>\n getExpandedSourceTokens(tokens)\n .filter((token) => token.chainId && token.contractAddress)\n .map((token) =>\n getDepositSourceId(token.contractAddress, token.chainId!),\n );\n\n const getDepositTokenOptionsBySourceId = () => {\n const map = new Map();\n const sourceTokens = [\n ...(swapBalance\n ? deriveTokenOptions(swapBalance, swapSupportedChainsAndTokens)\n : []),\n ...fromTokens,\n ];\n\n for (const token of getExpandedSourceTokens(sourceTokens)) {\n if (!token.chainId || !token.contractAddress) continue;\n const id = getDepositSourceId(token.contractAddress, token.chainId);\n if (!map.has(id)) {\n map.set(id, {\n ...token,\n userAmount: \"\",\n });\n }\n }\n\n return map;\n };\n\n const getDepositSourceTokensForIds = (sourceIds: string[]) => {\n const tokenBySourceId = getDepositTokenOptionsBySourceId();\n return sourceIds\n .map((sourceId) => tokenBySourceId.get(sourceId))\n .filter((token): token is SwapTokenOption => Boolean(token))\n .map((token) => ({ ...token, userAmount: \"\" }));\n };\n\n const getResolvedDepositSourceSelection = (options?: {\n filter?: DepositSourceFilter;\n selectedTokens?: SwapTokenOption[];\n isManualSelection?: boolean;\n targetAmountUsd?: Decimal;\n }) => {\n const destination = getDepositDestinationForSourceSelection();\n if (!destination) {\n return { sourcePoolIds: [], selectedSourceIds: [], fromSources: [] };\n }\n\n const manualSelection =\n options?.isManualSelection ?? sourceSelectionTouched;\n const selectedTokensForResolution = options?.selectedTokens ?? fromTokens;\n const selectedSourceIds = getDepositSourceIdsFromTokens(\n selectedTokensForResolution,\n );\n const destinationSourceId = getDestinationSourceIdForDeposit();\n const targetAmountUsd =\n options?.targetAmountUsd ??\n (activeMode === \"deposit\"\n ? getDepositSourceTargetUsd()\n : activeMode === \"send\"\n ? new Decimal(sendAmountUsd || 0)\n : undefined);\n\n return resolveDepositSourceSelection({\n swapBalance,\n destination,\n filter: manualSelection\n ? \"custom\"\n : (options?.filter ?? depositSourceFilter),\n selectedSourceIds,\n isManualSelection: manualSelection,\n minimumBalanceUsd: minimumSourceUsd.toNumber(),\n targetAmountUsd: targetAmountUsd?.toNumber(),\n excludedSourceIds: destinationSourceId ? [destinationSourceId] : [],\n });\n };\n\n const getExactOutSourceTokens = (\n mode: \"all\" | \"selected\" = exactOutQuoteSourceModeRef.current,\n targetAmountUsd?: Decimal,\n ) => {\n if (activeMode === \"deposit\") {\n const selection = getResolvedDepositSourceSelection({ targetAmountUsd });\n return getDepositSourceTokensForIds(\n mode === \"all\" ? selection.sourcePoolIds : selection.selectedSourceIds,\n );\n }\n\n if (activeMode === \"send\" && mode === \"selected\" && fromTokens.length > 0) {\n return filterMinimumSourceUsdTokens(\n getExpandedSourceTokens(fromTokens),\n ).filter(hasGasForSource);\n }\n\n return getGasCapableBalanceSourceTokens();\n };\n\n const buildExplicitSourcesPayload = (tokens: SwapTokenOption[]) => {\n const eligibleTokens = filterMinimumSourceUsdTokens(tokens).filter(\n (token) => token.chainId && token.contractAddress,\n );\n return {\n sources: eligibleTokens.map((token) => ({\n chainId: token.chainId!,\n tokenAddress: token.contractAddress as `0x${string}`,\n })),\n };\n };\n\n const getSdkSourceKey = (source: {\n chainId: number;\n tokenAddress: `0x${string}`;\n }) => {\n const normalizedAddress = isNativeTokenAddress(source.tokenAddress)\n ? zeroAddress\n : source.tokenAddress.toLowerCase();\n return `${source.chainId}:${normalizedAddress}`;\n };\n\n const dedupeSdkSources = (\n sources: Array<\n | {\n chainId: number;\n tokenAddress: `0x${string}`;\n }\n | undefined\n >,\n ) => {\n const seen = new Set();\n return sources.filter(\n (\n source,\n ): source is {\n chainId: number;\n tokenAddress: `0x${string}`;\n } => {\n if (!source) return false;\n const key = getSdkSourceKey(source);\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n },\n );\n };\n\n const getHeldDestinationTokenSource = () => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n if (\n !heldDestinationToken?.chainId ||\n !heldDestinationToken.contractAddress\n ) {\n return undefined;\n }\n\n return {\n chainId: heldDestinationToken.chainId,\n tokenAddress: heldDestinationToken.contractAddress as `0x${string}`,\n };\n };\n\n const shouldSendExactOutSourceAllowlist = () => {\n if (activeMode === \"deposit\") {\n return sourceSelectionTouched || depositSourceFilter !== \"all\";\n }\n\n if (activeMode === \"send\") {\n return exactOutQuoteSourceModeRef.current === \"selected\";\n }\n\n return true;\n };\n\n const buildExactOutSourcesPayload = (tokens: SwapTokenOption[]) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n return buildExplicitSourcesPayload(tokens);\n }\n\n const sourceTokens = shouldSendExactOutSourceAllowlist()\n ? tokens\n : getExactOutSourceTokens(\"all\");\n const explicitSources = buildExplicitSourcesPayload(sourceTokens).sources;\n const sources = dedupeSdkSources([\n ...explicitSources,\n getHeldDestinationTokenSource(),\n ]);\n\n return sources.length > 0 ? { sources } : {};\n };\n\n const buildPredictiveExactOutSources = async (requiredSourceUsd: Decimal) => {\n if (requiredSourceUsd.lte(0)) return [];\n\n const destinationKey = getTokenSelectionKey(toToken);\n const candidates = sortExactOutSourcesBySdkPriority(\n getExactOutSourceTokens(\n exactOutQuoteSourceModeRef.current,\n requiredSourceUsd,\n )\n .filter((token) => getTokenSelectionKey(token) !== destinationKey)\n .filter(hasMinimumSourceUsdBalance),\n );\n const sources: SwapTokenOption[] = [];\n let remainingUsd = requiredSourceUsd;\n\n for (const token of candidates) {\n if (remainingUsd.lte(0)) break;\n\n const availableUsd = getTokenBalanceUsd(token);\n if (availableUsd.lt(minimumSourceUsd)) continue;\n\n const rate = await resolveUsdRateForToken(token);\n if (rate.lte(0)) continue;\n\n const targetUsd = Decimal.min(remainingUsd, availableUsd);\n const tokenAmount = targetUsd\n .div(rate)\n .toDecimalPlaces(Math.max(0, token.decimals || 18), Decimal.ROUND_DOWN);\n if (tokenAmount.lte(0)) continue;\n\n sources.push({\n ...token,\n userAmount: tokenAmount.toFixed(),\n userAmountMode: \"token\",\n userAmountUsd: targetUsd\n .toDecimalPlaces(6, Decimal.ROUND_DOWN)\n .toFixed(),\n });\n remainingUsd = remainingUsd.minus(targetUsd);\n }\n\n return remainingUsd.gt(0.01) ? [] : sources;\n };\n\n const getErrorText = (error: unknown) => {\n const err = error as any;\n const parts = [\n err?.message,\n typeof error === \"string\" ? error : undefined,\n err?.code,\n ];\n\n try {\n if (err?.data) parts.push(JSON.stringify(err.data));\n } catch {\n // Ignore non-serializable SDK error metadata.\n }\n\n return parts.filter(Boolean).join(\" \");\n };\n\n const isInsufficientSourcesError = (error: unknown) => {\n const err = error as any;\n const message = getErrorText(error).toLowerCase();\n\n return (\n err?.code === ERROR_CODES.INSUFFICIENT_BALANCE ||\n message.includes(\"insufficient balance\") ||\n message.includes(\"sources are not enough\") ||\n (message.includes(\"source\") && message.includes(\"not enough\"))\n );\n };\n\n const isViemInvalidDecimalError = (error: unknown) =>\n getErrorText(error).toLowerCase().includes(\"not a valid decimal number\");\n\n const parseLabeledErrorDecimal = (text: string, label: string) => {\n const match = text.match(\n new RegExp(`${label}\\\\s*:\\\\s*\\\\$?\\\\s*([0-9][0-9,]*(?:\\\\.[0-9]+)?)`, \"i\"),\n );\n return match ? parseFiatNumber(match[1]) : undefined;\n };\n\n const getExactOutRequestedUsd = () => {\n const amountNumber = parseFiatNumber(amount);\n if (!amountNumber || amountNumber.lte(0) || !toToken?.symbol) {\n return undefined;\n }\n\n if (activeMode === \"deposit\" && depositAmountMode === \"usd\") {\n return amountNumber;\n }\n\n const fiatValue = getFiatValue(amountNumber.toNumber(), toToken.symbol);\n return Number.isFinite(fiatValue) && fiatValue > 0\n ? new Decimal(fiatValue)\n : undefined;\n };\n\n const getExactOutAvailableSourceUsd = (\n sourceTokensOverride?: SwapTokenOption[],\n ): Decimal => {\n const sumTokensWithDestinationCredit = (\n tokens: SwapTokenOption[],\n ): Decimal => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n const heldDestinationKey = getTokenSelectionKey(heldDestinationToken);\n let hasHeldDestinationToken = false;\n\n const sourceTotal = tokens.reduce((sum, token) => {\n const value = parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n const isHeldDestinationToken =\n heldDestinationKey &&\n getTokenSelectionKey(token) === heldDestinationKey;\n if (isHeldDestinationToken) {\n hasHeldDestinationToken = true;\n }\n\n return value.gt(0) &&\n (isHeldDestinationToken || value.gte(minimumSourceUsd))\n ? sum.plus(value)\n : sum;\n }, new Decimal(0));\n\n if (!heldDestinationToken || hasHeldDestinationToken) {\n return sourceTotal;\n }\n\n const heldDestinationUsd =\n parseFiatNumber(heldDestinationToken.balanceInFiat) ?? new Decimal(0);\n return heldDestinationUsd.gt(0)\n ? sourceTotal.plus(heldDestinationUsd)\n : sourceTotal;\n };\n\n if (\n sourceTokensOverride ||\n exactOutQuoteSourceModeRef.current === \"selected\"\n ) {\n const selectedSourceTokens = sourceTokensOverride ?? fromTokens;\n return sumTokensWithDestinationCredit(selectedSourceTokens);\n }\n\n return getExactOutTotalSourceBalanceUsd();\n };\n const getExactOutTotalSourceBalanceUsd = (): Decimal => {\n const allSourceTotal: Decimal = getExactOutAvailableSourceUsd(\n getMinimumBalanceSourceTokens(),\n );\n\n return allSourceTotal.gt(0) ? allSourceTotal : getSwapBalanceTotalUsd();\n };\n\n const getExactOutIntentSourceUsd = () => {\n const intentSourceUsd = (intentData?.sources ?? []).reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0),\n );\n return intentSourceUsd.gt(0) ? intentSourceUsd : undefined;\n };\n\n const getExactOutHeldDestinationUsd = () => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n const value = parseFiatNumber(heldDestinationToken?.balanceInFiat);\n return value && value.gt(0) ? value : new Decimal(0);\n };\n\n const exactOutIntentIncludesHeldDestination = () => {\n const heldDestinationToken = getHeldDestinationTokenOption();\n const heldDestinationKey = getTokenSelectionKey(heldDestinationToken);\n if (!heldDestinationKey) return false;\n\n return (intentData?.sources ?? []).some((source) => {\n const chainId = source.chain?.id;\n const tokenAddress = source.token?.contractAddress;\n if (!chainId || !tokenAddress) return false;\n\n return (\n getTokenSelectionKey({\n chainId,\n contractAddress: tokenAddress,\n } as SwapTokenOption) === heldDestinationKey\n );\n });\n };\n\n const getExactOutRequiredSourceUsd = () => {\n const intentSourceUsd = getExactOutIntentSourceUsd();\n if (intentSourceUsd) return intentSourceUsd;\n return undefined;\n };\n\n const getExactOutRequiredFundingUsd = () => {\n const sourceRequiredUsd = getExactOutRequiredSourceUsd();\n if (sourceRequiredUsd) {\n const destinationCreditUsd = exactOutIntentIncludesHeldDestination()\n ? new Decimal(0)\n : getExactOutHeldDestinationUsd();\n return sourceRequiredUsd.plus(destinationCreditUsd);\n }\n\n return getExactOutRequestedUsd();\n };\n\n const getExactInSourceDeficitUsd = () => {\n if (swapType !== \"exactIn\" || fromTokens.length === 0) return undefined;\n\n return fromTokens.reduce((sum, token) => {\n const requestedAmount = parseFiatNumber(token.userAmount);\n if (!requestedAmount || requestedAmount.lte(0)) return sum;\n\n if (token.userAmountMode === \"usd\") {\n const availableUsd = parseFiatNumber(token.balanceInFiat);\n if (!availableUsd || requestedAmount.lte(availableUsd)) return sum;\n return sum.plus(requestedAmount.minus(availableUsd));\n }\n\n const availableTokenAmount = parseFiatNumber(token.balance);\n if (!availableTokenAmount || requestedAmount.lte(availableTokenAmount)) {\n return sum;\n }\n\n const missingTokenAmount = requestedAmount.minus(availableTokenAmount);\n const fiatBalance = parseFiatNumber(token.balanceInFiat);\n if (fiatBalance && availableTokenAmount.gt(0)) {\n return sum.plus(\n missingTokenAmount.mul(fiatBalance.div(availableTokenAmount)),\n );\n }\n\n return sum;\n }, new Decimal(0));\n };\n\n const buildInsufficientSourcesIssue = (error: unknown): SwapQuoteIssue => {\n const errorText = getErrorText(error);\n const details =\n (error as any)?.data?.details ?? (error as any)?.details ?? {};\n const requiredFromError =\n parseFiatNumber(\n details.requiredUsd ??\n details.requiredUSD ??\n details.requiredAmountUsd ??\n details.requiredAmount ??\n details.required,\n ) ?? parseLabeledErrorDecimal(errorText, \"required\");\n const availableFromError =\n parseFiatNumber(\n details.availableUsd ??\n details.availableUSD ??\n details.availableAmountUsd ??\n details.availableAmount ??\n details.available,\n ) ?? parseLabeledErrorDecimal(errorText, \"available\");\n const requestedUsd = getExactOutRequiredFundingUsd();\n const availableUsd = getExactOutAvailableSourceUsd();\n const exactInSourceDeficitUsd = getExactInSourceDeficitUsd();\n\n let missingUsd =\n exactInSourceDeficitUsd && exactInSourceDeficitUsd.gt(0)\n ? exactInSourceDeficitUsd\n : requiredFromError && availableFromError\n ? requiredFromError.minus(availableFromError)\n : undefined;\n\n if (\n requestedUsd &&\n (!missingUsd || missingUsd.lte(0) || missingUsd.gt(requestedUsd.mul(5)))\n ) {\n missingUsd = requestedUsd.minus(availableUsd);\n }\n\n if (missingUsd && missingUsd.gt(0)) {\n const formattedMissing =\n missingUsd.gt(0) && missingUsd.lt(0.01)\n ? \"<$0.01\"\n : formatUsdDisplay(missingUsd);\n\n return {\n type: \"insufficientSources\",\n missingUsd: missingUsd.toDecimalPlaces(2).toFixed(),\n message: `Need ${formattedMissing} more across your assets`,\n };\n }\n\n return {\n type: \"insufficientSources\",\n message: \"Add more source balance across your assets\",\n };\n };\n\n const buildExactOutSourceBalanceIssue = (\n sourceTokensOverride?: SwapTokenOption[],\n ): SwapQuoteIssue | null => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return null;\n const requestedUsd = getExactOutRequiredFundingUsd();\n const availableUsd = getExactOutAvailableSourceUsd(sourceTokensOverride);\n if (!requestedUsd || requestedUsd.lte(0) || !availableUsd) return null;\n\n const missingUsd = requestedUsd.minus(availableUsd);\n if (missingUsd.lte(0.01)) return null;\n\n const formattedMissing =\n missingUsd.gt(0) && missingUsd.lt(0.01)\n ? \"<$0.01\"\n : formatUsdDisplay(missingUsd);\n\n return {\n type: \"insufficientSources\",\n missingUsd: missingUsd.toDecimalPlaces(2).toFixed(),\n message: `Need ${formattedMissing} more across your assets`,\n };\n };\n\n const isNativeTokenAddress = (address?: string) =>\n !address ||\n address.toLowerCase() === \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\" ||\n address.toLowerCase() === \"0x0000000000000000000000000000000000000000\";\n\n const formatReadableTokenAmount = (rawAmount: bigint, decimals: number) =>\n new Decimal(rawAmount.toString())\n .div(new Decimal(10).pow(decimals))\n .toFixed();\n\n const formatReadableTokenBalanceAmount = (\n rawAmount: bigint,\n decimals: number,\n ) =>\n new Decimal(rawAmount.toString())\n .div(new Decimal(10).pow(decimals))\n .toDecimalPlaces(6)\n .toFixed();\n\n const trimDecimalString = (value: string) =>\n value.replace(/(\\.\\d*?)0+$/, \"$1\").replace(/\\.$/, \"\");\n\n const buildSourceTokenSnapshotMap = useCallback(\n (balances: UserAsset[] | null | undefined) => {\n const snapshots = new Map();\n\n for (const asset of balances ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n const chainId = breakdown.chain?.id;\n const contractAddress = breakdown.contractAddress;\n const symbol = breakdown.symbol ?? asset.symbol;\n\n if (\n !chainId ||\n !contractAddress ||\n !symbol ||\n !isSwapSupportedBySdkChainList(\n chainId,\n swapSupportedChainsAndTokens,\n )\n ) {\n continue;\n }\n\n const chainMeta = CHAIN_METADATA[chainId];\n const fiatBalance = parseFiatNumber(breakdown.balanceInFiat);\n const snapshot: SwapTokenOption = {\n balance: `${breakdown.balance ?? \"0\"} ${symbol}`,\n balanceInFiat: fiatBalance\n ? formatUsdDisplay(fiatBalance)\n : \"$0.00\",\n chainId,\n chainLogo: chainMeta?.logo ?? breakdown.chain?.logo,\n chainName: getShortChainName(\n chainId,\n chainMeta?.name ?? breakdown.chain?.name,\n ),\n contractAddress,\n decimals: breakdown.decimals ?? asset.decimals ?? 18,\n logo: asset.logo ?? \"\",\n name: symbol,\n symbol,\n };\n snapshots.set(getTokenSelectionKey(snapshot), snapshot);\n }\n }\n\n return snapshots;\n },\n [swapSupportedChainsAndTokens],\n );\n\n const patchSourceTokensWithBalances = useCallback(\n (tokens: SwapTokenOption[], balances: UserAsset[]) => {\n const snapshots = buildSourceTokenSnapshotMap(balances);\n\n const updateToken = (token: SwapTokenOption): SwapTokenOption => {\n const preservedAmounts = {\n userAmount: token.userAmount,\n userAmountMode: token.userAmountMode,\n userAmountUsd: token.userAmountUsd,\n };\n\n if (token.isUnified) {\n const sourceTokens = (token.sourceTokens ?? []).map(updateToken);\n const totalBalance = sourceTokens.reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber(source.balance) ?? new Decimal(0)),\n new Decimal(0),\n );\n const totalFiat = sourceTokens.reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber(source.balanceInFiat) ?? new Decimal(0)),\n new Decimal(0),\n );\n\n return {\n ...token,\n ...preservedAmounts,\n balance: totalBalance.toDecimalPlaces(8).toFixed(),\n balanceInFiat: formatUsdDisplay(totalFiat),\n sourceTokens,\n };\n }\n\n const snapshot = snapshots.get(getTokenSelectionKey(token));\n if (!snapshot) {\n return {\n ...token,\n ...preservedAmounts,\n balance: `0 ${token.symbol}`,\n balanceInFiat: \"$0.00\",\n chainLogo:\n token.chainLogo ??\n (token.chainId ? CHAIN_METADATA[token.chainId]?.logo : undefined),\n chainName: getShortChainName(token.chainId, token.chainName),\n };\n }\n\n return {\n ...token,\n ...snapshot,\n ...preservedAmounts,\n };\n };\n\n return tokens.map(updateToken);\n },\n [buildSourceTokenSnapshotMap],\n );\n\n const refreshSelectedSourceBalances = useCallback(async () => {\n const refreshedBalance = await fetchSwapBalance();\n const balances = refreshedBalance ?? swapBalance;\n if (!balances) return;\n\n setFromTokens((current) =>\n current.length === 0\n ? current\n : patchSourceTokensWithBalances(current, balances),\n );\n setSourceSelectionRevision((current) => current + 1);\n }, [fetchSwapBalance, patchSourceTokensWithBalances, swapBalance]);\n\n const receiveMaxSafetyMultiplier = new Decimal(\"0.9\");\n const currentSwapEntry =\n currentSwapId !== null\n ? swapHistory.find((entry) => entry.id === currentSwapId)\n : undefined;\n\n const scheduleTerminalBalanceRefresh = () => {\n if (terminalBalanceRefreshTimerRef.current) {\n clearTimeout(terminalBalanceRefreshTimerRef.current);\n }\n\n terminalBalanceRefreshTimerRef.current = setTimeout(() => {\n terminalBalanceRefreshTimerRef.current = null;\n void fetchSwapBalance();\n }, BALANCE_REFRESH_AFTER_TERMINAL_MS);\n };\n\n const patchSwapHistoryEntry = (\n id: string | null | undefined,\n patch: Partial,\n ) => {\n if (!id) return;\n setSwapHistory((prev) =>\n sortSwapHistoryEntries(\n prev.map((entry) => (entry.id === id ? { ...entry, ...patch } : entry)),\n ),\n );\n };\n\n const patchCurrentSwapHistoryEntry = (patch: Partial) => {\n patchSwapHistoryEntry(currentSwapIdRef.current, patch);\n };\n\n const patchCurrentIntentExplorerUrl = (url?: string | null) => {\n if (!isHttpUrl(url)) return;\n if (intentUrlRef.current === url) return;\n\n intentUrlRef.current = url;\n const intentId = extractIntentIdFromUrl(url);\n patchCurrentSwapHistoryEntry({\n intentExplorerUrl: url,\n ...(intentId ? { intentId } : {}),\n });\n };\n\n const resetExplorerUrls = () => {\n const next = { sourceExplorerUrl: null, destinationExplorerUrl: null };\n explorerUrlsRef.current = next;\n setExplorerUrls(next);\n };\n\n const mergeExplorerUrls = (\n patch: Partial<{\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n }>,\n ) => {\n const next = { ...explorerUrlsRef.current, ...patch };\n explorerUrlsRef.current = next;\n setExplorerUrls(next);\n patchCurrentSwapHistoryEntry({\n sourceExplorerUrl: next.sourceExplorerUrl,\n finalExplorerUrl: next.destinationExplorerUrl ?? next.sourceExplorerUrl,\n });\n };\n\n const resetProgressEvents = () => {\n progressEventsRef.current = [];\n setProgressEvents((current) => (current.length === 0 ? current : []));\n setFailedProgressStep((current) => (current === null ? current : null));\n };\n\n const appendProgressEvent = (\n name: string,\n step: SwapStepType | BridgeStepType | undefined,\n defaultCompleted: boolean,\n event?: unknown,\n ) => {\n if (!step) return;\n const completed =\n typeof (step as any).completed === \"boolean\"\n ? Boolean((step as any).completed)\n : defaultCompleted;\n\n setProgressEvents((prev) => {\n const next = [\n ...prev,\n {\n id: `${Date.now()}-${prev.length}-${(step as any).typeID ?? (step as any).type ?? name}`,\n name,\n completed,\n event,\n step,\n },\n ];\n progressEventsRef.current = next;\n return next;\n });\n };\n\n const appendProgressListEvent = (\n name: string,\n stepList: Array,\n ) => {\n if (stepList.length === 0) return;\n\n setProgressEvents((prev) => {\n const next = [\n ...prev,\n {\n id: `${Date.now()}-${prev.length}-${name}`,\n name,\n completed: false,\n step: stepList[0],\n steps: stepList,\n },\n ];\n progressEventsRef.current = next;\n return next;\n });\n };\n\n const startSwapHistoryEntry = () => {\n const id = `${Date.now()}-${swapRunIdRef.current}`;\n const now = Date.now();\n const resolvedToToken =\n toToken && destinationBalance\n ? { ...toToken, balance: destinationBalance }\n : toToken;\n const entry: SwapHistoryEntry = {\n id,\n mode: activeMode,\n status: \"pending\",\n createdAt: now,\n startedAt: now,\n intentData,\n fromTokens,\n toToken: resolvedToToken,\n requestedToAmount:\n activeMode === \"deposit\" || activeMode === \"send\"\n ? previewDestinationAmount\n : undefined,\n requestedToValue:\n activeMode === \"deposit\" || activeMode === \"send\"\n ? previewToAmountUsd\n : undefined,\n recipientAddress: transferRecipientAddress,\n opportunity: selectedOpportunity,\n feeUsd: intentFeeUsd,\n sourceExplorerUrl: null,\n finalExplorerUrl: null,\n intentExplorerUrl: isHttpUrl(intentUrlRef.current)\n ? intentUrlRef.current\n : null,\n intentId: extractIntentIdFromUrl(intentUrlRef.current),\n autoRefundAvailable: false,\n };\n\n currentSwapStartedAtRef.current = 0;\n currentSwapIdRef.current = id;\n setCurrentSwapId(id);\n setSwapHistory((prev) => sortSwapHistoryEntries([entry, ...prev]));\n return id;\n };\n\n const finishCurrentSwapHistoryEntry = (\n status: \"fulfilled\" | \"failed\" | \"timeout\",\n patch: Partial = {},\n ) => {\n const now = Date.now();\n const startedAt = currentSwapStartedAtRef.current || now;\n patchSwapHistoryEntry(currentSwapIdRef.current, {\n status,\n endedAt: now,\n durationSeconds: Math.max(1, Math.round((now - startedAt) / 1000)),\n sourceExplorerUrl: explorerUrlsRef.current.sourceExplorerUrl,\n finalExplorerUrl:\n explorerUrlsRef.current.destinationExplorerUrl ??\n explorerUrlsRef.current.sourceExplorerUrl,\n ...patch,\n });\n scheduleTerminalBalanceRefresh();\n };\n\n const markSwapExecutionStarted = () => {\n if (currentSwapStartedAtRef.current > 0) return;\n const now = Date.now();\n currentSwapStartedAtRef.current = now;\n patchCurrentSwapHistoryEntry({ startedAt: now });\n };\n\n const enterSkippedSwapProgress = () => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n\n const shouldInitializeProgress = swapStepRef.current !== \"progress\";\n if (!currentSwapIdRef.current) {\n onStart?.();\n startSwapHistoryEntry();\n }\n\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setPreviewQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setSwapQuoteIssue(null);\n\n if (shouldInitializeProgress) {\n resetProgressEvents();\n swapStepsListRef.current = [];\n resetSteps();\n swapStepRef.current = \"progress\";\n setSwapStep(\"progress\");\n }\n };\n\n const cachePredictiveBaselineFromIntent = (intent: SwapIntentData) => {\n const destinationAmount = parseFiatNumber(intent.destination?.amount);\n const destinationValue = parseFiatNumber(intent.destination?.value);\n const sourceUsd = (intent.sources ?? []).reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0),\n );\n\n if (!destinationAmount || destinationAmount.lte(0)) return;\n\n const destinationUsdRate =\n destinationValue && destinationValue.gt(0)\n ? destinationValue.div(destinationAmount)\n : getUsdRateForSymbol(intent.destination?.token?.symbol);\n if (destinationUsdRate.lte(0)) return;\n\n cacheSymbolUsdRate(intent.destination?.token?.symbol, destinationUsdRate);\n\n const key = getPredictiveQuoteCacheKey();\n if (!key) return;\n\n const baseline: PredictiveQuoteBaseline = {\n destinationUsdRate: destinationUsdRate.toDecimalPlaces(18).toFixed(),\n updatedAt: Date.now(),\n };\n\n if (activeMode === \"swap\" && swapType === \"exactIn\" && sourceUsd.gt(0)) {\n baseline.exactInDestinationAmountPerSourceUsd = destinationAmount\n .div(sourceUsd)\n .toDecimalPlaces(18)\n .toFixed();\n }\n\n const resolvedDestinationValue =\n destinationValue && destinationValue.gt(0)\n ? destinationValue\n : destinationAmount.mul(destinationUsdRate);\n if (\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n resolvedDestinationValue.gt(0) &&\n sourceUsd.gt(0)\n ) {\n baseline.exactOutSourceUsdPerDestinationUsd = sourceUsd\n .div(resolvedDestinationValue)\n .toDecimalPlaces(18)\n .toFixed();\n }\n\n predictiveQuoteCacheRef.current[key] = baseline;\n };\n\n const applySwapIntent = useCallback(\n (intent: SwapIntentData) => {\n const sortedIntent = {\n ...intent,\n sources: sortIntentSourcesByUsdDesc(intent.sources ?? []),\n };\n const enrichedIntent = enrichSwapIntentTokenMetadata(sortedIntent);\n const sortedIntentSourceTokens = sortSwapTokensByUsdDesc(\n (enrichedIntent.sources ?? []).map(buildIntentSourceToken),\n );\n\n lastSwapIntentRefreshAtRef.current = Date.now();\n lastIntentSourceTokensRef.current = sortedIntentSourceTokens;\n if (\n !sourceSelectionTouched &&\n (activeMode === \"send\" ||\n (activeMode === \"deposit\" && swapType === \"exactOut\"))\n ) {\n lastAutoIntentSourceTokensRef.current = sortedIntentSourceTokens;\n }\n cacheDestinationUsdRateFromIntent(enrichedIntent);\n cachePredictiveBaselineFromIntent(enrichedIntent);\n setIntentData(enrichedIntent);\n setIntentToAmount(enrichedIntent.destination?.amount || undefined);\n setSwapQuoteIssue(null);\n\n if (\n !sourceSelectionTouched &&\n swapStepRef.current !== \"choose-swap-asset\" &&\n (activeMode === \"send\" ||\n (activeMode === \"deposit\" && swapType === \"exactOut\"))\n ) {\n syncingIntentSourcesRef.current = true;\n setFromTokens(sortedIntentSourceTokens);\n }\n\n try {\n const bridgeFees = enrichedIntent.feesAndBuffer?.bridge;\n const bridgeFeeData =\n bridgeFees && typeof bridgeFees === \"object\" ? bridgeFees : undefined;\n const collectionFee = parseFiatNumber(bridgeFeeData?.collection);\n const fulfilmentFee = parseFiatNumber(bridgeFeeData?.fulfilment);\n const executionGasFee =\n parseFiatNumber(bridgeFeeData?.caGas) ??\n (collectionFee !== undefined || fulfilmentFee !== undefined\n ? (collectionFee ?? new Decimal(0)).plus(\n fulfilmentFee ?? new Decimal(0),\n )\n : undefined);\n const bridgeGasSuppliedFee = parseFiatNumber(\n bridgeFeeData?.gasSupplied,\n );\n const destinationGasSuppliedFee =\n getIntentDestinationGasUsdValue(enrichedIntent);\n const gasSuppliedFee =\n bridgeGasSuppliedFee ?? destinationGasSuppliedFee;\n const bridgeComponentsTotal = bridgeFeeData\n ? [\n executionGasFee,\n parseFiatNumber(bridgeFeeData.protocol),\n parseFiatNumber(bridgeFeeData.solver),\n gasSuppliedFee,\n ].reduce(\n (sum, value) => sum.plus(value ?? new Decimal(0)),\n new Decimal(0),\n )\n : undefined;\n const rawBridgeTotal =\n typeof bridgeFees === \"string\"\n ? parseFiatNumber(bridgeFees)\n : parseFiatNumber(bridgeFeeData?.total);\n const bridgeTotal =\n rawBridgeTotal &&\n !bridgeGasSuppliedFee &&\n destinationGasSuppliedFee &&\n destinationGasSuppliedFee.gt(0)\n ? rawBridgeTotal.plus(destinationGasSuppliedFee)\n : (rawBridgeTotal ??\n (bridgeComponentsTotal && bridgeComponentsTotal.gt(0)\n ? bridgeComponentsTotal\n : destinationGasSuppliedFee));\n\n if (bridgeTotal !== undefined) {\n setIntentFeeUsd(\n bridgeTotal.gt(0) ? bridgeTotal.toDecimalPlaces(6).toFixed() : \"0\",\n );\n } else {\n setIntentFeeUsd(undefined);\n }\n } catch (err) {\n console.warn(\"Could not resolve bridge fee total\", err);\n setIntentFeeUsd(undefined);\n }\n },\n [\n activeMode,\n fromTokens,\n getFiatValue,\n sourceSelectionTouched,\n swapType,\n swapBalance,\n swapSupportedChainsAndTokens,\n supportedChainsAndTokens,\n toToken,\n ],\n );\n\n const handleSwapIntentCallback = useCallback(\n (data: any, runId: number, quoteInputKey: string) => {\n const { intent, allow, deny, refresh } = data;\n const bridgeProvider = normalizeBridgeProvider(\n data?.bridgeProvider ??\n intent?.bridgeProvider ??\n intent?.normalizedIntent?.bridgeProvider ??\n intent?.swap?.bridgeProvider,\n );\n const intentWithBridgeProvider = normalizeRenderableSwapIntentData(\n intent,\n bridgeProvider,\n );\n logSdkIntentEvent(\"onIntent\", data, {\n bridgeProvider,\n currentRunId: swapRunIdRef.current,\n normalizedIntent: intentWithBridgeProvider,\n isCurrentRun: swapRunIdRef.current === runId,\n quoteInputKey,\n runId,\n });\n if (swapRunIdRef.current !== runId) {\n logSdkIntentEvent(\"ignored stale onIntent\", data, {\n currentRunId: swapRunIdRef.current,\n quoteInputKey,\n runId,\n });\n deny();\n return;\n }\n if (quoteInputKey && activeQuoteInputKeyRef.current !== quoteInputKey) {\n logSdkIntentEvent(\"ignored stale quote input onIntent\", data, {\n currentQuoteInputKey: activeQuoteInputKeyRef.current,\n quoteInputKey,\n runId,\n });\n deny();\n return;\n }\n if (!intentWithBridgeProvider) {\n console.warn(\"[NexusWidget SDK][intent] Unsupported intent shape\", {\n intent,\n raw: data,\n });\n finishIntentFetchTiming(runId, \"failed\");\n deny();\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setTxError(\"Quote unavailable\");\n return;\n }\n const resolvedQuoteInputKey = quoteInputKey;\n const normalizedRefresh =\n typeof refresh === \"function\"\n ? async (...args: unknown[]) => {\n const refreshed = await refresh(...args);\n const refreshedBridgeProvider = normalizeBridgeProvider(\n refreshed?.bridgeProvider ??\n refreshed?.normalizedIntent?.bridgeProvider ??\n refreshed?.swap?.bridgeProvider ??\n bridgeProvider,\n );\n return (\n normalizeRenderableSwapIntentData(\n refreshed,\n refreshedBridgeProvider,\n ) ?? refreshed\n );\n }\n : refresh;\n finishIntentFetchTiming(runId);\n providerSwapIntent.current = {\n intent: intentWithBridgeProvider as any,\n allow,\n deny,\n refresh: normalizedRefresh,\n };\n swapIntentRef.current = {\n intent: intentWithBridgeProvider,\n allow,\n deny,\n refresh: normalizedRefresh,\n runId,\n quoteInputKey: resolvedQuoteInputKey,\n };\n flushSync(() => {\n applySwapIntent(intentWithBridgeProvider);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n });\n },\n [applySwapIntent, finishIntentFetchTiming, providerSwapIntent],\n );\n\n // Deposit-specific\n const [selectedOpportunity, setSelectedOpportunity] = useState<\n NexusWidgetDepositOpportunityConfig | undefined\n >(() => (activeMode === \"deposit\" ? configuredDeposit : undefined));\n const selectedOpportunityIdentity =\n getDepositConfigIdentity(selectedOpportunity);\n const [depositAmountMode, setDepositAmountMode] = useState<\"token\" | \"usd\">(\n \"token\",\n );\n const [depositSourceFilter, setDepositSourceFilter] =\n useState(\"all\");\n\n useEffect(() => {\n const immediateIssue = buildReceiveAmountIssue();\n applyReceiveAmountIssue(immediateIssue);\n\n if (!toToken || !getDestinationReceiveLimitUsd(toToken)) return;\n if (!parseFiatNumber(amount)?.gt(0)) return;\n if (getImmediateDestinationReceiveUsdRate(toToken)?.gt(0)) return;\n\n let cancelled = false;\n void resolveUsdRateForToken(toToken)\n .then((resolvedRate) => {\n if (cancelled) return;\n const issue = buildReceiveAmountIssue({\n destinationRate: resolvedRate.gt(0) ? resolvedRate : undefined,\n });\n applyReceiveAmountIssue(issue);\n })\n .catch(() => {\n if (!cancelled) {\n applyReceiveAmountIssue(buildReceiveAmountIssue());\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [\n activeMode,\n amount,\n depositAmountMode,\n fromTokens,\n swapType,\n toToken?.chainId,\n toToken?.chainName,\n toToken?.contractAddress,\n toToken?.priceUSD,\n toToken?.symbol,\n ]);\n\n const trackDeposit = useCallback(\n (event: string, props?: Record) => {\n const analytics = nexusSDK?.analytics;\n if (!analytics) return;\n analytics.track(event, {\n widgetSessionId: widgetSessionIdRef.current,\n widgetAttemptId: widgetAttemptIdRef.current,\n opportunityProtocol: selectedOpportunity?.protocol ?? null,\n destinationChainId: selectedOpportunity?.chainId ?? null,\n destinationToken: selectedOpportunity?.tokenSymbol ?? null,\n ...props,\n });\n },\n [nexusSDK, selectedOpportunity],\n );\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (!nexusSDK?.analytics) return;\n if (widgetOpenedFiredRef.current) return;\n widgetOpenedFiredRef.current = true;\n widgetOpenedTsRef.current = Date.now();\n rotateAttempt();\n trackDeposit(\"deposit_widget_opened\", {\n embed: Boolean(embed),\n depositConfigured: Boolean(configuredDeposit),\n prefillAmountPresent: Boolean(config.prefill?.amount),\n });\n }, [\n activeMode,\n nexusSDK,\n embed,\n configuredDeposit,\n config.prefill,\n rotateAttempt,\n trackDeposit,\n ]);\n\n useEffect(() => {\n analyticsRef.current = nexusSDK?.analytics ?? null;\n }, [nexusSDK]);\n\n useEffect(() => {\n selectedOpportunityRef.current = selectedOpportunity;\n }, [selectedOpportunity]);\n\n useEffect(() => {\n return () => {\n if (!widgetOpenedFiredRef.current) return;\n const analytics = analyticsRef.current;\n if (!analytics) return;\n const opp = selectedOpportunityRef.current;\n analytics.track(\"deposit_widget_closed\", {\n widgetSessionId: widgetSessionIdRef.current,\n widgetAttemptId: widgetAttemptIdRef.current,\n opportunityProtocol: opp?.protocol ?? null,\n lastStep: swapStepRef.current,\n reachedTerminal: reachedTerminalRef.current,\n hadSimulationSuccess: hadSimulationSuccessRef.current,\n hadPreviewViewed: hadPreviewViewedRef.current,\n timeInWidgetMs: Date.now() - widgetOpenedTsRef.current,\n });\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const toTokenFromOpportunity = (\n opp: NexusWidgetDepositOpportunityMetadata,\n ): SwapTokenOption => {\n const citreaToken = findCitreaReceiveToken({\n address: opp.tokenAddress,\n chainId: opp.chainId,\n symbol: opp.tokenSymbol,\n });\n const chainTokens = supportedChainsAndTokens?.find(\n (chain) => chain.id === opp.chainId,\n )?.tokens;\n const matchedToken = chainTokens?.find(\n (token) =>\n token.contractAddress.toLowerCase() ===\n opp.tokenAddress.toLowerCase() || token.symbol === opp.tokenSymbol,\n );\n const tokenSymbol =\n citreaToken?.symbol ?? matchedToken?.symbol ?? opp.tokenSymbol;\n const tokenMeta =\n TOKEN_METADATA[tokenSymbol as keyof typeof TOKEN_METADATA];\n\n return {\n chainId: opp.chainId,\n contractAddress: citreaToken?.contractAddress ?? opp.tokenAddress,\n symbol: tokenSymbol,\n name: matchedToken?.name || citreaToken?.name || tokenSymbol,\n balance: \"0\",\n balanceInFiat: \"$0.00\",\n decimals:\n matchedToken?.decimals ??\n citreaToken?.decimals ??\n opp.tokenDecimals ??\n tokenMeta?.decimals ??\n 18,\n logo:\n opp.tokenLogo ||\n matchedToken?.logo ||\n citreaToken?.logo ||\n tokenMeta?.logo,\n chainName: getShortChainName(\n opp.chainId,\n CHAIN_METADATA[opp.chainId]?.name ?? citreaToken?.chainName,\n ),\n chainLogo: CHAIN_METADATA[opp.chainId]?.logo ?? citreaToken?.chainLogo,\n };\n };\n\n const getDestinationBalanceFromSwapBalances = (token?: SwapTokenOption) => {\n if (!token?.chainId || !token.contractAddress) return null;\n\n const targetAddress = token.contractAddress.toLowerCase();\n const targetSymbol = token.symbol.toUpperCase();\n\n for (const asset of swapBalance ?? []) {\n for (const breakdown of asset.breakdown ?? []) {\n if (breakdown.chain?.id !== token.chainId) continue;\n\n const breakdownAddress = breakdown.contractAddress?.toLowerCase();\n const addressMatches =\n (breakdownAddress && breakdownAddress === targetAddress) ||\n (isNativeTokenAddress(breakdownAddress) &&\n isNativeTokenAddress(targetAddress));\n const symbolMatches =\n (breakdown.symbol ?? asset.symbol ?? \"\").toUpperCase() ===\n targetSymbol;\n\n if (!addressMatches && !symbolMatches) continue;\n\n const balance = parseFiatNumber(breakdown.balance);\n if (!balance) return null;\n\n return `${balance.toDecimalPlaces(6).toFixed()} ${token.symbol}`;\n }\n }\n\n return null;\n };\n\n const resolvePrefillToken = useCallback(\n (pair?: RuntimeDestinationPair) => {\n if (!pair?.token || !pair.chain) return undefined;\n\n const normalizeAddress = (address?: string) => {\n if (!address) return \"\";\n return isNativeTokenAddress(address)\n ? zeroAddress\n : address.toLowerCase();\n };\n const targetAddress = normalizeAddress(pair.token);\n\n const balanceToken = deriveTokenOptions(\n swapBalance ?? [],\n swapSupportedChainsAndTokens,\n ).find(\n (token) =>\n token.chainId === pair.chain &&\n normalizeAddress(token.contractAddress) === targetAddress,\n );\n if (balanceToken) return balanceToken;\n\n const chain = supportedChainsAndTokens?.find(\n (item) => item.id === pair.chain,\n );\n const matchedToken = chain?.tokens?.find(\n (token) => normalizeAddress(token.contractAddress) === targetAddress,\n );\n const citreaToken = findCitreaReceiveToken({\n address: pair.token,\n chainId: pair.chain,\n });\n const tokenAddressSymbol = Object.entries(\n TOKEN_CONTRACT_ADDRESSES as Record>,\n ).find(\n ([, addresses]) =>\n normalizeAddress(addresses[pair.chain]) === targetAddress,\n )?.[0];\n const chainMeta = CHAIN_METADATA[pair.chain];\n const isNativePrefill = isNativeTokenAddress(pair.token);\n const tokenSymbol =\n matchedToken?.symbol ??\n citreaToken?.symbol ??\n pair.symbol ??\n tokenAddressSymbol ??\n (isNativePrefill ? chainMeta?.nativeCurrency?.symbol : undefined) ??\n \"Token\";\n const tokenMeta =\n TOKEN_METADATA[tokenSymbol as keyof typeof TOKEN_METADATA];\n\n if (\n !chain &&\n !matchedToken &&\n !citreaToken &&\n !tokenAddressSymbol &&\n !pair.symbol &&\n !isNativePrefill\n ) {\n return undefined;\n }\n\n return {\n chainId: pair.chain,\n contractAddress: citreaToken?.contractAddress ?? pair.token,\n symbol: tokenSymbol,\n name: matchedToken?.name || citreaToken?.name || tokenSymbol,\n balance: `0 ${tokenSymbol}`,\n balanceInFiat: \"$0.00\",\n decimals:\n matchedToken?.decimals ??\n citreaToken?.decimals ??\n pair.decimals ??\n tokenMeta?.decimals ??\n (isNativePrefill ? chainMeta?.nativeCurrency?.decimals : undefined) ??\n 18,\n logo:\n matchedToken?.logo ||\n citreaToken?.logo ||\n pair.logo ||\n tokenMeta?.logo,\n chainName: getShortChainName(\n pair.chain,\n chain?.name ?? chainMeta?.name ?? citreaToken?.chainName,\n ),\n chainLogo: chainMeta?.logo ?? chain?.logo ?? citreaToken?.chainLogo,\n } satisfies SwapTokenOption;\n },\n [supportedChainsAndTokens, swapBalance],\n );\n\n useEffect(() => {\n if (activeMode !== \"swap\") return;\n\n const sourcePrefill = config.prefill?.source;\n const destinationPrefill = config.prefill?.destination;\n if (!sourcePrefill && !destinationPrefill) return;\n\n const prefillKey = [\n sourcePrefill\n ? `source:${sourcePrefill.chain}:${sourcePrefill.token.toLowerCase()}`\n : \"\",\n destinationPrefill\n ? `destination:${destinationPrefill.chain}:${destinationPrefill.token.toLowerCase()}`\n : \"\",\n config.prefill?.amount ? `amount:${config.prefill.amount}` : \"\",\n ].join(\"|\");\n\n if (appliedTokenPrefillRef.current === prefillKey) return;\n\n const sourceToken = resolvePrefillToken(sourcePrefill);\n const destinationToken = resolvePrefillToken(destinationPrefill);\n\n if (sourcePrefill && !sourceToken) return;\n if (destinationPrefill && !destinationToken) return;\n\n if (sourceToken) {\n setFromTokens((current) => {\n const nextSourceToken = {\n ...sourceToken,\n userAmount: config.prefill?.amount ?? \"\",\n };\n const currentSourceToken = current[0];\n if (\n current.length === 1 &&\n isSameTokenSelection(currentSourceToken, nextSourceToken) &&\n currentSourceToken.userAmount === nextSourceToken.userAmount\n ) {\n return current;\n }\n return [nextSourceToken];\n });\n setSourceSelectionTouched(true);\n }\n if (destinationToken) {\n setToToken((current) =>\n isSameTokenSelection(current, destinationToken)\n ? current\n : destinationToken,\n );\n }\n setSwapType(\"exactIn\");\n appliedTokenPrefillRef.current = prefillKey;\n }, [\n activeMode,\n config.prefill?.amount,\n config.prefill?.destination?.chain,\n config.prefill?.destination?.decimals,\n config.prefill?.destination?.logo,\n config.prefill?.destination?.symbol,\n config.prefill?.destination?.token,\n config.prefill?.source?.chain,\n config.prefill?.source?.decimals,\n config.prefill?.source?.logo,\n config.prefill?.source?.symbol,\n config.prefill?.source?.token,\n resolvePrefillToken,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"send\") return;\n\n const sendPrefill =\n config.prefill?.token && config.prefill?.chain\n ? {\n token: config.prefill.token,\n chain: config.prefill.chain,\n }\n : config.prefill?.destination;\n if (!sendPrefill) return;\n\n const prefillKey = `send:${sendPrefill.chain}:${sendPrefill.token.toLowerCase()}`;\n if (appliedTokenPrefillRef.current === prefillKey) return;\n\n const token = resolvePrefillToken(sendPrefill);\n if (!token) return;\n\n setToToken(token);\n setSwapType(\"exactOut\");\n appliedTokenPrefillRef.current = prefillKey;\n }, [\n activeMode,\n config.prefill?.chain,\n config.prefill?.destination?.chain,\n config.prefill?.destination?.decimals,\n config.prefill?.destination?.logo,\n config.prefill?.destination?.symbol,\n config.prefill?.destination?.token,\n config.prefill?.token,\n resolvePrefillToken,\n ]);\n\n useEffect(() => {\n if (config.prefill?.amount) setAmount(config.prefill.amount);\n if (config.prefill?.recipient)\n setRecipientAddress(config.prefill.recipient);\n }, [config.prefill?.amount, config.prefill?.recipient]);\n\n useEffect(() => {\n setDestinationBalance(null);\n\n const balanceToken =\n toToken ??\n (activeMode === \"deposit\" && selectedOpportunity\n ? toTokenFromOpportunity(selectedOpportunity)\n : undefined);\n\n if (!balanceToken?.chainId || !ownerAddress) return;\n\n const swapBalanceValue =\n getDestinationBalanceFromSwapBalances(balanceToken);\n if (swapBalanceValue) {\n setDestinationBalance(swapBalanceValue);\n }\n }, [\n activeMode,\n ownerAddress,\n selectedOpportunity?.chainId,\n selectedOpportunity?.tokenAddress,\n selectedOpportunity?.tokenLogo,\n selectedOpportunity?.tokenSymbol,\n swapBalance,\n toToken?.chainId,\n toToken?.chainName,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || !configuredDeposit) return;\n setSelectedOpportunity((current) =>\n isSameDepositConfig(current, configuredDeposit)\n ? current\n : configuredDeposit,\n );\n setSwapType(\"exactOut\");\n setToToken((current) => {\n const next = {\n ...toTokenFromOpportunity(configuredDeposit),\n balance: current?.balance ?? \"0\",\n balanceInFiat: current?.balanceInFiat ?? \"$0.00\",\n };\n if (\n current &&\n current.chainId === next.chainId &&\n current.contractAddress.toLowerCase() ===\n next.contractAddress.toLowerCase() &&\n current.symbol === next.symbol &&\n current.decimals === next.decimals &&\n current.logo === next.logo &&\n current.chainLogo === next.chainLogo &&\n current.chainName === next.chainName &&\n current.balance === next.balance &&\n current.balanceInFiat === next.balanceInFiat\n ) {\n return current;\n }\n return next;\n });\n }, [\n activeMode,\n configuredDepositIdentity,\n configuredDeposit?.chainId,\n configuredDeposit?.tokenAddress,\n configuredDeposit?.tokenLogo,\n configuredDeposit?.tokenSymbol,\n supportedChainsAndTokens,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || !selectedOpportunity) return;\n setToToken((current) => {\n const next = {\n ...toTokenFromOpportunity(selectedOpportunity),\n balance: current?.balance ?? \"0\",\n balanceInFiat: current?.balanceInFiat ?? \"$0.00\",\n };\n if (\n current &&\n current.chainId === next.chainId &&\n current.contractAddress.toLowerCase() ===\n next.contractAddress.toLowerCase() &&\n current.symbol === next.symbol &&\n current.decimals === next.decimals &&\n current.logo === next.logo &&\n current.chainLogo === next.chainLogo &&\n current.chainName === next.chainName &&\n current.balance === next.balance &&\n current.balanceInFiat === next.balanceInFiat\n ) {\n return current;\n }\n return next;\n });\n }, [activeMode, selectedOpportunity, supportedChainsAndTokens]);\n\n useEffect(() => {\n if (activeMode !== \"send\") return;\n setSwapType(\"exactOut\");\n }, [activeMode]);\n\n useEffect(() => {\n if (activeMode === \"swap\" && swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n }, [activeMode, swapType]);\n\n useEffect(() => {\n if (!toToken?.symbol) return;\n if (getFiatValue(1, toToken.symbol) > 0) return;\n\n let cancelled = false;\n void resolveTokenUsdRate(toToken.symbol).catch((error) => {\n if (!cancelled) {\n console.warn(\"Unable to resolve Nexus Widget token USD rate\", {\n symbol: toToken.symbol,\n error,\n });\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [activeMode, getFiatValue, resolveTokenUsdRate, toToken?.symbol]);\n\n // Balance helpers\n const activeBalanceArray = swapBalance;\n const selectedToken = config.prefill?.token ?? \"USDC\";\n const currentAsset =\n activeBalanceArray?.find((a) => a.symbol === selectedToken) ||\n activeBalanceArray?.[0];\n const maxBalance = currentAsset?.balance\n ? String(currentAsset.balance)\n : undefined;\n const usdValue = getFiatValue(\n Number(amount) || 0,\n currentAsset?.symbol || \"USDC\",\n );\n const getDepositTokenUsdRate = () => {\n if (selectedOpportunity?.tokenSymbol) {\n const fiat = getFiatValue(1, selectedOpportunity.tokenSymbol);\n if (Number.isFinite(fiat) && fiat > 0) {\n return new Decimal(fiat);\n }\n }\n\n if (toToken) {\n const tokenRate = getTokenUsdRate(toToken);\n if (tokenRate.gt(0)) return tokenRate;\n }\n\n return getCachedDestinationUsdRate(toToken) ?? new Decimal(0);\n };\n const getDepositTokenAmountForQuote = () => {\n const parsedAmount = parseFiatNumber(amount) ?? new Decimal(0);\n if (parsedAmount.lte(0)) return undefined;\n if (depositAmountMode === \"token\") return parsedAmount;\n\n const rate = getDepositTokenUsdRate();\n if (rate.lte(0)) return undefined;\n return parsedAmount.div(rate);\n };\n const depositTokenAmountForQuote = getDepositTokenAmountForQuote();\n const depositQuoteAmountKey = depositTokenAmountForQuote?.toFixed() ?? \"\";\n const depositUsdDecimal =\n depositAmountMode === \"usd\"\n ? (parseFiatNumber(amount) ?? new Decimal(0))\n : depositTokenAmountForQuote\n ? depositTokenAmountForQuote.mul(getDepositTokenUsdRate())\n : new Decimal(0);\n const depositUsdDisplay = depositUsdDecimal.toDecimalPlaces(2).toFixed();\n const depositTokenDisplay =\n depositTokenAmountForQuote\n ?.toDecimalPlaces(\n getCappedTokenDisplayDecimals(toToken?.decimals),\n Decimal.ROUND_DOWN,\n )\n .toFixed() ?? \"0\";\n const isExactOutMode = activeMode === \"deposit\" || activeMode === \"send\";\n const getActiveTotalBalanceUsd = () =>\n isExactOutMode\n ? getExactOutTotalSourceBalanceUsd()\n : getSwapBalanceTotalUsd();\n const getTotalBalancePercentUsdAmount = (pct: number) => {\n const ratio =\n isExactOutMode && pct === 100\n ? receiveMaxSafetyMultiplier\n : new Decimal(pct).div(100);\n return getActiveTotalBalanceUsd().mul(ratio);\n };\n const getExactOutDestinationUsdRate = (token: SwapTokenOption) => {\n if (activeMode === \"deposit\") {\n return getDepositTokenUsdRate();\n }\n return getTokenUsdRate(token);\n };\n const getExactOutPercentAmountFromBalance = (\n token: SwapTokenOption,\n pct: number,\n preferUsd: boolean,\n ) => {\n const usdAmount = getTotalBalancePercentUsdAmount(pct);\n if (usdAmount.lte(0)) return undefined;\n\n if (preferUsd) {\n return {\n amount: usdAmount.toDecimalPlaces(2, Decimal.ROUND_DOWN).toFixed(),\n mode: \"usd\" as const,\n };\n }\n\n const rate = getExactOutDestinationUsdRate(token);\n if (rate.lte(0)) return undefined;\n\n return {\n amount: usdAmount\n .div(rate)\n .toDecimalPlaces(\n getCappedTokenDisplayDecimals(token.decimals),\n Decimal.ROUND_DOWN,\n )\n .toFixed(),\n mode: \"token\" as const,\n };\n };\n const depositSourceTargetUsdKey =\n activeMode === \"deposit\"\n ? (getDepositSourceTargetUsd()?.toFixed() ?? \"\")\n : \"\";\n const normalizedQuoteAmountKey = parseFiatNumber(amount)?.toFixed() ?? \"\";\n const quoteRecipientKey =\n activeMode === \"swap\"\n ? effectiveRecipientAddress\n : activeMode === \"send\"\n ? recipientAddress\n : \"\";\n const activeQuoteInputKey = [\n activeMode,\n swapType,\n normalizedQuoteAmountKey,\n toTokenQuoteKey,\n quoteRecipientKey.toLowerCase(),\n activeMode === \"swap\" ? fromTokensQuoteKey : \"\",\n activeMode === \"deposit\"\n ? [\n depositAmountMode,\n depositQuoteAmountKey,\n selectedOpportunityIdentity,\n depositSourceTargetUsdKey,\n depositSourceFilter,\n sourceSelectionTouched ? \"manual\" : \"auto\",\n sourceSelectionRevision,\n exactOutQuoteSourceMode,\n ].join(\":\")\n : \"\",\n activeMode === \"send\"\n ? [\n sourceSelectionTouched ? \"manual\" : \"auto\",\n sourceSelectionRevision,\n exactOutQuoteSourceMode,\n ].join(\":\")\n : \"\",\n ].join(\"|\");\n\n const exactOutAutoSourceInputKey = [\n activeMode,\n normalizedQuoteAmountKey,\n toTokenQuoteKey,\n quoteRecipientKey.toLowerCase(),\n activeMode === \"deposit\"\n ? [\n depositAmountMode,\n depositQuoteAmountKey,\n selectedOpportunityIdentity,\n depositSourceTargetUsdKey,\n ].join(\":\")\n : \"\",\n ].join(\"|\");\n\n activeQuoteInputKeyRef.current = activeQuoteInputKey;\n\n useEffect(() => {\n lastAutoIntentSourceTokensRef.current = [];\n }, [exactOutAutoSourceInputKey]);\n\n useEffect(() => {\n setTxError(null);\n }, [activeQuoteInputKey]);\n const hasCurrentQuoteIntent = Boolean(\n intentData &&\n swapIntentRef.current &&\n swapIntentRef.current.runId === swapRunIdRef.current &&\n swapIntentRef.current.quoteInputKey === activeQuoteInputKey,\n );\n\n useEffect(() => {\n if (swapStep !== \"idle\") return;\n if (!hasCurrentQuoteIntent) return;\n if (sourceSelectionTouched) return;\n if (\n activeMode !== \"send\" &&\n !(activeMode === \"deposit\" && swapType === \"exactOut\")\n ) {\n return;\n }\n\n const nextTokens = lastIntentSourceTokensRef.current;\n if (nextTokens.length === 0) return;\n\n setFromTokens((current) => {\n if (\n getSourceTokenListSyncKey(current) ===\n getSourceTokenListSyncKey(nextTokens)\n ) {\n return current;\n }\n\n syncingIntentSourcesRef.current = true;\n return nextTokens;\n });\n }, [\n activeMode,\n hasCurrentQuoteIntent,\n sourceSelectionTouched,\n swapStep,\n swapType,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (!nexusSDK?.analytics) return;\n const parsed = parseFiatNumber(amount);\n if (!parsed || parsed.lte(0)) return;\n if (amount === amountEnteredLastValueRef.current) return;\n const timeout = setTimeout(() => {\n amountEnteredLastValueRef.current = amount;\n trackDeposit(\"deposit_amount_entered\", {\n amountToken: depositTokenDisplay,\n amountUsd: Number(depositUsdDisplay) || 0,\n inputMethod: lastInputMethodRef.current,\n });\n lastInputMethodRef.current = \"typed\";\n }, 500);\n return () => clearTimeout(timeout);\n }, [\n amount,\n activeMode,\n nexusSDK,\n depositTokenDisplay,\n depositUsdDisplay,\n trackDeposit,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (intentData) hadSimulationSuccessRef.current = true;\n }, [intentData, activeMode]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (sourceSelectionTouched) return;\n previousAutoSourceCountRef.current = (intentData?.sources ?? []).length;\n }, [intentData, activeMode, sourceSelectionTouched]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n const prev = prevSourceTouchedRef.current;\n const curr = sourceSelectionTouched;\n if (prev === curr) return;\n prevSourceTouchedRef.current = curr;\n if (!prev && curr) {\n trackDeposit(\"deposit_source_selection_changed\", {\n sourceCount: fromTokens.length,\n sourceChainIds: fromTokens.map((t) => t.chainId).filter(Boolean),\n sourceTokenSymbols: fromTokens.map((t) => t.symbol).filter(Boolean),\n previousSourceCount: previousAutoSourceCountRef.current,\n });\n } else if (prev && !curr) {\n trackDeposit(\"deposit_source_selection_reverted_to_auto\", {\n previousSourceCount: fromTokens.length,\n });\n }\n }, [sourceSelectionTouched, activeMode, fromTokens, trackDeposit]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (swapStep !== \"preview-intent\") return;\n if (intentLoading) return;\n if (!intentData) return;\n if (hadPreviewViewedRef.current) return;\n hadPreviewViewedRef.current = true;\n previewViewedTsRef.current = Date.now();\n trackDeposit(\"deposit_preview_viewed\", {\n totalFeeUsd: Number(intentFeeUsd) || 0,\n toAmountUsd: Number(depositUsdDisplay) || 0,\n sourceCount: (intentData?.sources ?? []).length,\n });\n }, [\n swapStep,\n intentLoading,\n intentData,\n activeMode,\n intentFeeUsd,\n depositUsdDisplay,\n trackDeposit,\n ]);\n const requiredDestinationTokenAmount =\n activeMode === \"deposit\"\n ? depositTokenAmountForQuote\n : activeMode === \"send\"\n ? parseFiatNumber(amount)\n : undefined;\n const canRefreshExactOutQuote = () =>\n activeMode === \"deposit\"\n ? Boolean(\n hasPositiveDecimalInput(amount) &&\n toToken &&\n selectedOpportunity &&\n depositTokenAmountForQuote &&\n depositTokenAmountForQuote.gt(0),\n )\n : activeMode === \"send\"\n ? Boolean(hasPositiveDecimalInput(amount) && toToken)\n : false;\n const invalidateExactOutQuoteForRefresh = (options?: {\n sourceTokens?: SwapTokenOption[];\n }) => {\n immediateQuoteAfterSourceEditRef.current = true;\n const receiveIssue = buildReceiveAmountIssue({\n sourceTokens: options?.sourceTokens,\n });\n applyReceiveAmountIssue(receiveIssue);\n const configuredIssue = buildConfiguredAmountIssue();\n if (configuredIssue) {\n clearPreviewForBlockingAmountIssue();\n }\n const sourceBalanceIssue = buildExactOutSourceBalanceIssue(\n options?.sourceTokens,\n );\n if (sourceBalanceIssue) {\n clearPreviewForBlockingAmountIssue();\n setSwapQuoteIssue(sourceBalanceIssue);\n }\n const shouldLoadQuote = Boolean(\n !receiveIssue &&\n !configuredIssue &&\n !sourceBalanceIssue &&\n nexusSDK &&\n canRefreshExactOutQuote(),\n );\n if (!receiveIssue && !configuredIssue && !sourceBalanceIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n setTxError(null);\n setSwapQuoteIssue(null);\n }\n return shouldLoadQuote;\n };\n\n useEffect(() => {\n if (\n activeMode !== \"swap\" ||\n swapStep !== \"idle\" ||\n swapType !== \"exactIn\"\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current,\n );\n return;\n }\n\n const sources = getPredictiveExactInSourceTokens();\n const key = getPredictiveQuoteCacheKey();\n if (!toToken || sources.length === 0 || !key) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current,\n );\n return;\n }\n\n const runId = ++predictiveQuoteRunRef.current;\n let cancelled = false;\n\n void (async () => {\n const baseline = predictiveQuoteCacheRef.current[key];\n const cachedDestinationRate = parseFiatNumber(\n baseline?.destinationUsdRate,\n );\n const destinationRate =\n cachedDestinationRate && cachedDestinationRate.gt(0)\n ? cachedDestinationRate\n : await resolveUsdRateForToken(toToken);\n\n if (cancelled || runId !== predictiveQuoteRunRef.current) return;\n if (destinationRate.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current,\n );\n return;\n }\n\n let sourceUsd = new Decimal(0);\n for (const source of sources) {\n const sourceAmount =\n parseFiatNumber(source.userAmount) ?? new Decimal(0);\n if (sourceAmount.lte(0)) continue;\n\n if (source.userAmountMode === \"usd\") {\n sourceUsd = sourceUsd.plus(sourceAmount);\n continue;\n }\n\n const sourceRate = await resolveUsdRateForToken(source);\n if (cancelled || runId !== predictiveQuoteRunRef.current) return;\n if (sourceRate.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current,\n );\n return;\n }\n sourceUsd = sourceUsd.plus(sourceAmount.mul(sourceRate));\n }\n\n if (sourceUsd.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactIn\" ? null : current,\n );\n return;\n }\n\n const cachedAmountPerSourceUsd = parseFiatNumber(\n baseline?.exactInDestinationAmountPerSourceUsd,\n );\n const predictedDestinationAmount =\n cachedAmountPerSourceUsd && cachedAmountPerSourceUsd.gt(0)\n ? sourceUsd.mul(cachedAmountPerSourceUsd)\n : sourceUsd\n .mul(BASIS_POINTS - PREDICTIVE_EXACT_IN_DISCOUNT_BPS)\n .div(BASIS_POINTS)\n .div(destinationRate);\n const predictedDestinationUsd =\n cachedAmountPerSourceUsd && cachedAmountPerSourceUsd.gt(0)\n ? predictedDestinationAmount.mul(destinationRate)\n : sourceUsd\n .mul(BASIS_POINTS - PREDICTIVE_EXACT_IN_DISCOUNT_BPS)\n .div(BASIS_POINTS);\n\n if (\n cancelled ||\n runId !== predictiveQuoteRunRef.current ||\n predictedDestinationAmount.lte(0)\n ) {\n return;\n }\n\n setPredictiveQuote({\n key,\n mode: \"exactIn\",\n toAmount: getPredictiveDisplayAmount(\n predictedDestinationAmount,\n toToken,\n ),\n toUsd: predictedDestinationUsd.toDecimalPlaces(6).toFixed(),\n });\n })();\n\n return () => {\n cancelled = true;\n };\n }, [\n activeMode,\n amount,\n fromTokens,\n swapStep,\n swapType,\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n useEffect(() => {\n if (\n (activeMode !== \"deposit\" && activeMode !== \"send\") ||\n swapStep !== \"idle\" ||\n swapType !== \"exactOut\" ||\n !nexusSDK\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current,\n );\n return;\n }\n\n const parsedAmount = parseFiatNumber(amount);\n const key = getPredictiveQuoteCacheKey();\n if (\n !toToken ||\n !parsedAmount ||\n parsedAmount.lte(0) ||\n !key ||\n (activeMode === \"deposit\" && !selectedOpportunity)\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current,\n );\n return;\n }\n\n const runId = ++predictiveQuoteRunRef.current;\n let cancelled = false;\n\n void (async () => {\n const baseline = predictiveQuoteCacheRef.current[key];\n const cachedDestinationRate = parseFiatNumber(\n baseline?.destinationUsdRate,\n );\n const destinationRate =\n cachedDestinationRate && cachedDestinationRate.gt(0)\n ? cachedDestinationRate\n : await resolveUsdRateForToken(toToken);\n\n if (cancelled || runId !== predictiveQuoteRunRef.current) return;\n if (destinationRate.lte(0)) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current,\n );\n return;\n }\n\n const destinationAmount =\n activeMode === \"deposit\" && depositAmountMode === \"usd\"\n ? parsedAmount.div(destinationRate)\n : parsedAmount;\n const destinationUsd =\n activeMode === \"deposit\" && depositAmountMode === \"usd\"\n ? parsedAmount\n : destinationAmount.mul(destinationRate);\n const destinationCoverage = getExactOutDestinationBalanceCoverage({\n requestedAmount: destinationAmount,\n requestedUsd: destinationUsd,\n token: toToken,\n });\n const destinationUsdNeedingSources = Decimal.max(\n destinationUsd.minus(destinationCoverage?.usd ?? new Decimal(0)),\n new Decimal(0),\n );\n const cachedSourceUsdRatio = parseFiatNumber(\n baseline?.exactOutSourceUsdPerDestinationUsd,\n );\n const requiredSourceUsd = getPredictiveExactOutSourceTargetUsd(\n destinationUsdNeedingSources,\n cachedSourceUsdRatio,\n );\n const sources = requiredSourceUsd.gt(0)\n ? await buildPredictiveExactOutSources(requiredSourceUsd)\n : [];\n\n if (\n cancelled ||\n runId !== predictiveQuoteRunRef.current ||\n (requiredSourceUsd.gt(0) && sources.length === 0)\n ) {\n setPredictiveQuote((current) =>\n current?.mode === \"exactOut\" ? null : current,\n );\n return;\n }\n\n setPredictiveQuote({\n key,\n mode: \"exactOut\",\n sources,\n toAmount: getPredictiveDisplayAmount(destinationAmount, toToken),\n toUsd: destinationUsd.toDecimalPlaces(6).toFixed(),\n });\n })();\n\n return () => {\n cancelled = true;\n };\n }, [\n activeMode,\n amount,\n depositAmountMode,\n destinationBalance,\n fromTokensQuoteKey,\n nexusSDK,\n selectedOpportunityIdentity,\n sourceSelectionRevision,\n swapBalance,\n swapStep,\n swapType,\n toToken?.balance,\n toToken?.balanceInFiat,\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.symbol,\n ]);\n\n const resolvedDepositSourceTokens = useMemo(() => {\n if (activeMode !== \"deposit\" || !swapBalance) return [];\n const selection = getResolvedDepositSourceSelection();\n return getDepositSourceTokensForIds(selection.selectedSourceIds);\n }, [\n activeMode,\n depositSourceFilter,\n depositQuoteAmountKey,\n depositSourceTargetUsdKey,\n depositUsdDecimal.toFixed(),\n fromTokensQuoteKey,\n selectedOpportunity?.chainId,\n selectedOpportunity?.tokenAddress,\n selectedOpportunity?.tokenSymbol,\n sourceSelectionRevision,\n sourceSelectionTouched,\n swapBalance,\n toToken?.chainId,\n toToken?.contractAddress,\n toToken?.symbol,\n ]);\n const lockedDestinationSourceTokens = useMemo(() => {\n if (\n (activeMode !== \"deposit\" && activeMode !== \"send\") ||\n !toToken?.chainId ||\n !requiredDestinationTokenAmount ||\n requiredDestinationTokenAmount.lte(0)\n ) {\n return [];\n }\n\n const heldDestinationToken = getHeldDestinationTokenOption();\n return heldDestinationToken ? [heldDestinationToken] : [];\n }, [\n activeMode,\n requiredDestinationTokenAmount?.toFixed(),\n swapBalance,\n toToken?.chainId,\n toToken?.chainLogo,\n toToken?.chainName,\n toToken?.contractAddress,\n toToken?.decimals,\n toToken?.logo,\n toToken?.symbol,\n ]);\n\n const setSourcePickerDraftSelection = useCallback(\n (tokens: SwapTokenOption[]) => {\n const nextTokens = tokens.map((token) => ({\n ...token,\n userAmount: \"\",\n }));\n sourcePickerDraftTokensRef.current = nextTokens;\n setSourcePickerDraftTokens(nextTokens);\n },\n [],\n );\n\n const getAutoExactOutSourceTokensForPicker = useCallback(() => {\n const autoIntentTokens = lastAutoIntentSourceTokensRef.current;\n if (autoIntentTokens.length > 0) {\n return autoIntentTokens;\n }\n\n return sourceSelectionTouched ? [] : fromTokens;\n }, [fromTokens, sourceSelectionTouched]);\n\n const resetSourcePickerDraft = useCallback(() => {\n sourcePickerDraftTokensRef.current = null;\n setSourcePickerDraftTokens(null);\n }, []);\n\n const beginSourcePickerEdit = useCallback(() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n resetSourcePickerDraft();\n return;\n }\n\n sourcePickerDraftDepositFilterRef.current = depositSourceFilter;\n sourcePickerDraftTouchedRef.current = sourceSelectionTouched;\n sourcePickerDraftModeRef.current = exactOutQuoteSourceModeRef.current;\n setSourcePickerDraftSelection(fromTokens);\n }, [\n activeMode,\n depositSourceFilter,\n fromTokens,\n resetSourcePickerDraft,\n setSourcePickerDraftSelection,\n sourceSelectionTouched,\n ]);\n\n const handleSourcePickerCancel = useCallback(() => {\n resetSourcePickerDraft();\n closeDrawerToIdle();\n }, [closeDrawerToIdle, resetSourcePickerDraft]);\n\n const handleSourcePickerDraftSelectionChange = useCallback(\n (tokens: SwapTokenOption[]) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n\n setSourcePickerDraftSelection(tokens);\n sourcePickerDraftTouchedRef.current = true;\n sourcePickerDraftModeRef.current = \"selected\";\n if (activeMode === \"deposit\") {\n sourcePickerDraftDepositFilterRef.current = \"custom\";\n }\n },\n [activeMode, setSourcePickerDraftSelection],\n );\n\n const handleSourcePickerFilterTabSelect = useCallback(\n (tab: Exclude) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n\n const nextFilter: DepositSourceFilter =\n tab === \"stables\" ? \"stablecoins\" : tab;\n\n if (tab === \"all\") {\n sourcePickerDraftDepositFilterRef.current = nextFilter;\n sourcePickerDraftTouchedRef.current = false;\n sourcePickerDraftModeRef.current = \"all\";\n setSourcePickerDraftSelection(getAutoExactOutSourceTokensForPicker());\n return;\n }\n\n if (activeMode !== \"deposit\") return;\n\n const selection = getResolvedDepositSourceSelection({\n filter: nextFilter,\n isManualSelection: false,\n });\n const sourcePoolTokens = getDepositSourceTokensForIds(\n selection.sourcePoolIds,\n );\n\n sourcePickerDraftDepositFilterRef.current = nextFilter;\n sourcePickerDraftTouchedRef.current = false;\n sourcePickerDraftModeRef.current = \"all\";\n setSourcePickerDraftSelection(sourcePoolTokens);\n },\n [\n activeMode,\n getAutoExactOutSourceTokensForPicker,\n getDepositSourceTokensForIds,\n getResolvedDepositSourceSelection,\n setSourcePickerDraftSelection,\n ],\n );\n\n const commitSourcePickerDraft = useCallback(\n (tokens?: SwapTokenOption[]) => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n resetSourcePickerDraft();\n closeDrawerToIdle();\n return;\n }\n\n const nextTokens =\n tokens ?? sourcePickerDraftTokensRef.current ?? fromTokens;\n const normalizedTokens = nextTokens.map((token) => ({\n ...token,\n userAmount: \"\",\n }));\n\n setSourceSelectionTouched(sourcePickerDraftTouchedRef.current);\n setExactOutQuoteSourceModeValue(sourcePickerDraftModeRef.current);\n if (activeMode === \"deposit\") {\n setDepositSourceFilter(sourcePickerDraftDepositFilterRef.current);\n }\n invalidateExactOutQuoteForRefresh({\n sourceTokens: normalizedTokens,\n });\n setSourceSelectionRevision((current) => current + 1);\n setFromTokens(normalizedTokens);\n resetSourcePickerDraft();\n closeDrawerToIdle();\n },\n [\n activeMode,\n closeDrawerToIdle,\n fromTokens,\n invalidateExactOutQuoteForRefresh,\n resetSourcePickerDraft,\n setExactOutQuoteSourceModeValue,\n ],\n );\n\n const sourcePickerSelectedTokens =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n swapStep === \"choose-swap-asset\"\n ? (sourcePickerDraftTokens ?? fromTokens)\n : fromTokens;\n\n useEffect(() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n if (lockedDestinationSourceTokens.length === 0) return;\n if (activeMode === \"deposit\" && !sourceSelectionTouched) return;\n\n setFromTokens((current) => {\n const missing = lockedDestinationSourceTokens.filter(\n (locked) =>\n !current.some(\n (token) =>\n getTokenSelectionKey(token) === getTokenSelectionKey(locked),\n ),\n );\n if (missing.length === 0) return current;\n return [\n ...current,\n ...missing.map((token) => ({ ...token, userAmount: \"\" })),\n ];\n });\n }, [activeMode, lockedDestinationSourceTokens, sourceSelectionTouched]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (sourceSelectionTouched) return;\n if (\n !toToken ||\n !depositTokenAmountForQuote ||\n depositTokenAmountForQuote.lte(0)\n ) {\n return;\n }\n if (resolvedDepositSourceTokens.length === 0) {\n return;\n }\n\n setFromTokens((current) => {\n const canInitialize = current.length === 0;\n if (!canInitialize) return current;\n\n const next: SwapTokenOption[] = [];\n const seen = new Set();\n for (const token of resolvedDepositSourceTokens) {\n const key = getTokenSelectionKey(token);\n if (!key || seen.has(key)) continue;\n seen.add(key);\n next.push({ ...token, userAmount: \"\" });\n }\n\n const currentKeys = current.map(getTokenSelectionKey).sort().join(\"|\");\n const nextKeys = next.map(getTokenSelectionKey).sort().join(\"|\");\n if (currentKeys === nextKeys) return current;\n return next;\n });\n }, [\n activeMode,\n depositQuoteAmountKey,\n resolvedDepositSourceTokens,\n sourceSelectionTouched,\n toTokenQuoteKey,\n ]);\n\n // ---------------------------------------------------------------------------\n // Handlers\n // ---------------------------------------------------------------------------\n\n const applyConfiguredPrefillsAfterReset = () => {\n const prefillAmount = config.prefill?.amount ?? \"\";\n const prefillRecipient = config.prefill?.recipient ?? \"\";\n\n appliedTokenPrefillRef.current = null;\n syncingIntentSourcesRef.current = false;\n maxPercentRunRef.current += 1;\n setAmount(prefillAmount);\n setRecipientAddress(prefillRecipient);\n setTxError(null);\n setSwapQuoteIssue(null);\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n setPredictiveQuote(null);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n setMaxCalculationPercent(null);\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setExactOutQuoteSourceModeValue(\"all\");\n setSourceSelectionRevision((current) => current + 1);\n setDepositAmountMode(\"token\");\n\n if (activeMode === \"swap\") {\n const sourcePrefill = config.prefill?.source;\n const destinationPrefill = config.prefill?.destination;\n const sourceToken = resolvePrefillToken(sourcePrefill);\n const destinationToken = resolvePrefillToken(destinationPrefill);\n\n setFromTokens(\n sourceToken\n ? [\n {\n ...sourceToken,\n userAmount: prefillAmount,\n },\n ]\n : [],\n );\n setSourceSelectionTouched(Boolean(sourceToken));\n setToToken(destinationToken);\n setSelectedOpportunity(undefined);\n setSwapType(\"exactIn\");\n\n if (\n (!sourcePrefill || sourceToken) &&\n (!destinationPrefill || destinationToken) &&\n (sourcePrefill || destinationPrefill)\n ) {\n appliedTokenPrefillRef.current = [\n sourcePrefill\n ? `source:${sourcePrefill.chain}:${sourcePrefill.token.toLowerCase()}`\n : \"\",\n destinationPrefill\n ? `destination:${destinationPrefill.chain}:${destinationPrefill.token.toLowerCase()}`\n : \"\",\n config.prefill?.amount ? `amount:${config.prefill.amount}` : \"\",\n ].join(\"|\");\n }\n return;\n }\n\n if (activeMode === \"send\") {\n const sendPrefill =\n config.prefill?.token && config.prefill?.chain\n ? {\n token: config.prefill.token,\n chain: config.prefill.chain,\n }\n : config.prefill?.destination;\n const token = resolvePrefillToken(sendPrefill);\n\n setFromTokens([]);\n setSelectedOpportunity(undefined);\n setToToken(token);\n setSwapType(\"exactOut\");\n\n if (sendPrefill && token) {\n appliedTokenPrefillRef.current = `send:${sendPrefill.chain}:${sendPrefill.token.toLowerCase()}`;\n }\n return;\n }\n\n const destinationPrefill = config.prefill?.destination;\n const configuredPrefillDeposit = destinationPrefill\n ? configuredDepositOptions.find(\n (deposit) =>\n deposit.chainId === destinationPrefill.chain &&\n deposit.tokenAddress.toLowerCase() ===\n destinationPrefill.token.toLowerCase(),\n )\n : undefined;\n const nextDeposit = configuredPrefillDeposit ?? configuredDeposit;\n\n setFromTokens([]);\n setSelectedOpportunity(nextDeposit);\n setToToken(nextDeposit ? toTokenFromOpportunity(nextDeposit) : undefined);\n setSwapType(\"exactOut\");\n };\n\n const handleReset = () => {\n clearPendingSwapIntent();\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n amountEnteredLastValueRef.current = config.prefill?.amount ?? \"\";\n applyConfiguredPrefillsAfterReset();\n rotateAttempt();\n };\n\n const resetSourceSelectionAfterFailure = () => {\n lastAutoIntentSourceTokensRef.current = [];\n lastIntentSourceTokensRef.current = [];\n resetSourcePickerDraft();\n resetExactOutSourcesToAuto();\n };\n\n const handleFailureBack = () => {\n clearPendingSwapIntent();\n resetSourceSelectionAfterFailure();\n setTxError(null);\n void refreshSelectedSourceBalances();\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n rotateAttempt();\n };\n\n const clearTerminalInputState = () => {\n setAmount(\"\");\n setRecipientAddress(\"\");\n setTxError(null);\n setSwapQuoteIssue(null);\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n setFromTokens((current) => (current.length === 0 ? current : []));\n setSourceSelectionTouched(false);\n setDepositSourceFilter(\"all\");\n setDepositAmountMode(\"token\");\n };\n\n const resetInputsAfterSuccessfulExecution = () => {\n clearTerminalInputState();\n if (activeMode === \"deposit\") {\n setSelectedOpportunity(configuredDeposit);\n setToToken(\n configuredDeposit\n ? toTokenFromOpportunity(configuredDeposit)\n : undefined,\n );\n } else {\n setToToken(undefined);\n }\n };\n\n const handleTerminalReceiptDone = () => {\n clearPendingSwapIntent();\n clearTerminalInputState();\n void refreshSelectedSourceBalances();\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n amountEnteredLastValueRef.current = \"\";\n rotateAttempt();\n };\n\n const handleModalOpenChange = useCallback(\n (open: boolean) => {\n if (!open && swapStepRef.current === \"progress\") return;\n if (!isControlledOpen) {\n setInternalOpen(open);\n }\n onOpenChange?.(open);\n if (!open) {\n clearPendingSwapIntent();\n onClose?.();\n }\n },\n [clearPendingSwapIntent, isControlledOpen, onClose, onOpenChange],\n );\n\n const handleClose = () => {\n if (!embed) {\n handleModalOpenChange(false);\n return;\n }\n clearPendingSwapIntent();\n onClose?.();\n };\n\n const handleConnectWallet = async () => {\n if (walletActionPending || nexusLoading) return;\n\n const clickHandler = onConnectClick || onConnectWallet;\n if (clickHandler) {\n setWalletActionPending(true);\n setTxError(null);\n try {\n await clickHandler();\n } catch (error: any) {\n setTxError(error?.message || \"Unable to connect wallet.\");\n } finally {\n setWalletActionPending(false);\n }\n return;\n }\n\n if (isWalletConnectPending) return;\n\n setWalletActionPending(true);\n setTxError(null);\n try {\n let activeConnector = connector;\n\n if (walletStatus !== \"connected\") {\n const nextConnector = connectors[0];\n if (!nextConnector) {\n throw new Error(\"No wallet connector available.\");\n }\n await connectAsync({ connector: nextConnector });\n return;\n }\n\n const effectiveProvider =\n await getEffectiveWalletProvider(activeConnector);\n\n if (\n !effectiveProvider ||\n typeof effectiveProvider.request !== \"function\"\n ) {\n throw new Error(\"Wallet provider is not ready yet.\");\n }\n\n await handleInit(effectiveProvider as EthereumProvider, ownerAddress);\n } catch (error: any) {\n setTxError(error?.message || \"Unable to connect wallet.\");\n } finally {\n setWalletActionPending(false);\n }\n };\n\n const handleOpenRecipientEditor = () => {\n if (activeMode === \"swap\" && !recipientAddress && defaultRecipientAddress) {\n setRecipientAddress(defaultRecipientAddress);\n }\n setTxError(null);\n openDrawerStep(\"enter-recipient\");\n };\n\n const handleResetRecipientToDefault = () => {\n setRecipientAddress(defaultRecipientAddress);\n setTxError(null);\n };\n\n const handleSaveRecipient = () => {\n const next = recipientAddress.trim();\n if (!next) {\n setTxError(\"Recipient address is required\");\n return;\n }\n if (!next.endsWith(\".eth\") && !isAddress(next)) {\n setTxError(\"Incorrect address\");\n return;\n }\n if (\n activeMode === \"send\" &&\n ownerAddress &&\n isAddress(next) &&\n next.toLowerCase() === ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\n setRecipientAddress(next);\n setTxError(null);\n closeDrawerToIdle();\n };\n\n /** Start swap flow — v2 SDK per-operation onIntent hooks populate preview. */\n const handleEnterPreview = async (options: { background?: boolean } = {}) => {\n const { background = false } = options;\n const isExactOutFlow = activeMode === \"deposit\" || activeMode === \"send\";\n const quoteInputKey = activeQuoteInputKeyRef.current;\n const isCurrentQuoteInput = () =>\n activeQuoteInputKeyRef.current === quoteInputKey;\n\n if (!toToken) {\n return;\n }\n\n if (isExactOutFlow) {\n if (!hasPositiveDecimalInput(amount)) {\n return;\n }\n } else if (!hasReadyExactInSwapInput(fromTokens, toToken)) {\n if (!background) {\n setTxError(null);\n setSwapQuoteIssue(null);\n }\n return;\n }\n\n const receiveIssue = buildReceiveAmountIssue();\n if (receiveIssue) {\n applyReceiveAmountIssue(receiveIssue);\n if (!background && swapStepRef.current !== \"idle\") {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n }\n return;\n }\n\n const configuredIssue = buildConfiguredAmountIssue();\n if (configuredIssue) {\n clearPreviewForBlockingAmountIssue();\n if (!background && swapStepRef.current !== \"idle\") {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n }\n return;\n }\n\n const sourceBalanceIssue = isExactOutFlow\n ? buildExactOutSourceBalanceIssue()\n : null;\n if (sourceBalanceIssue) {\n clearPreviewForBlockingAmountIssue();\n setSwapQuoteIssue(sourceBalanceIssue);\n if (!background && swapStepRef.current !== \"idle\") {\n swapStepRef.current = \"idle\";\n setSwapStep(\"idle\");\n }\n return;\n }\n\n if (!background && activeMode === \"deposit\") {\n trackDeposit(\"deposit_confirm_clicked\", {\n amountToken: depositTokenDisplay,\n amountUsd: Number(depositUsdDisplay) || 0,\n selectionMode: sourceSelectionTouched ? \"manual\" : \"auto\",\n sourceCount: (intentData?.sources ?? []).length,\n });\n }\n\n setTxError(null);\n setSwapQuoteIssue(null);\n\n if (\n !background &&\n swapIntentRef.current?.runId === swapRunIdRef.current &&\n swapIntentRef.current?.quoteInputKey === quoteInputKey &&\n intentData &&\n (activeMode !== \"send\" || Boolean(recipientAddress)) &&\n ((activeMode !== \"deposit\" && activeMode !== \"send\") ||\n (intentData.sources ?? []).length > 0 ||\n Boolean(intentData.destination))\n ) {\n swapStepRef.current = \"preview-intent\";\n setSwapStep(\"preview-intent\");\n return;\n }\n\n let resolvedRecipientAddress =\n activeMode === \"swap\" ? effectiveRecipientAddress : recipientAddress;\n\n if (!background && activeMode === \"send\" && !resolvedRecipientAddress) {\n setTxError(\"Recipient address is required\");\n return;\n }\n\n if ((!background && activeMode === \"send\") || hasCustomSwapRecipient) {\n if (!resolvedRecipientAddress) {\n setTxError(\"Recipient address is required\");\n return;\n }\n\n if (\n activeMode === \"send\" &&\n ownerAddress &&\n isAddress(resolvedRecipientAddress) &&\n resolvedRecipientAddress.toLowerCase() === ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\n\n if (resolvedRecipientAddress.endsWith(\".eth\")) {\n try {\n const mainnetClient =\n publicClient?.chain?.id === 1\n ? publicClient\n : createPublicClient({\n chain: mainnet,\n transport: http(),\n });\n const ensAddr = await mainnetClient.getEnsAddress({\n name: normalize(resolvedRecipientAddress),\n });\n if (!ensAddr) {\n setTxError(\"Could not resolve ENS name to an address.\");\n return;\n }\n resolvedRecipientAddress = ensAddr;\n } catch (e: any) {\n setTxError(e.message || \"Failed to resolve ENS name.\");\n return;\n }\n } else {\n if (!isAddress(resolvedRecipientAddress)) {\n setTxError(\"Invalid recipient address.\");\n return;\n }\n }\n\n if (\n activeMode === \"send\" &&\n ownerAddress &&\n isAddress(resolvedRecipientAddress) &&\n resolvedRecipientAddress.toLowerCase() === ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\n }\n\n if (!isCurrentQuoteInput()) {\n return;\n }\n\n if (!background) {\n swapStepRef.current = \"preview-intent\";\n setSwapStep(\"preview-intent\");\n }\n setIntentLoading(true);\n setQuoteRefreshing(background);\n setIntentToAmount(undefined);\n setIntentFeeUsd(undefined);\n setIntentData(null);\n swapIntentRef.current?.deny();\n swapIntentRef.current = null;\n if (!background) {\n resetProgressEvents();\n swapStepsListRef.current = [];\n resetSteps();\n }\n\n if (!nexusSDK) {\n setTxError(\"SDK not initialized\");\n if (!background) {\n setSwapStep(\"idle\");\n }\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n swapRunIdRef.current += 1;\n const runId = swapRunIdRef.current;\n\n const isActionPlanStep = (step: SwapStepType | BridgeStepType) => {\n const type = getProgressStepType(step);\n return (\n type === \"APPROVAL\" ||\n type === \"TRANSACTION_SENT\" ||\n type === \"TRANSACTION_CONFIRMED\"\n );\n };\n\n const hasSwapPlanSteps = (stepList: Array) =>\n stepList.some((step) => !isActionPlanStep(step));\n\n const handleProgressStepSideEffects = (\n event: any,\n step: SwapStepType | BridgeStepType,\n completed: boolean,\n ) => {\n const type = getProgressStepType(step);\n const rawStepType = String(\n event?.stepType ?? (step as any)?.type ?? (step as any)?.typeID ?? \"\",\n ).toLowerCase();\n const rawState = String(event?.state ?? \"\").toLowerCase();\n const explorerUrl = getPlanStepExplorerUrl(event, step);\n const sourceMovementStep = isSourceMovementProgressStep(\n type,\n rawStepType,\n );\n const hasSourceMovementTransaction = Boolean(\n explorerUrl || getPlanStepTransactionHash(event, step),\n );\n const intentExplorerUrl = getEventIntentExplorerUrl(\n appConfig.nexusNetwork,\n event,\n step,\n );\n\n patchCurrentIntentExplorerUrl(intentExplorerUrl);\n\n if (\n type === \"TRANSACTION_SENT\" ||\n type === \"TRANSACTION_CONFIRMED\" ||\n type === \"SOURCE_SWAP\" ||\n type === \"BRIDGE_DEPOSIT\" ||\n type === \"BRIDGE_INTENT_SUBMISSION\" ||\n type === \"BRIDGE_FILL\" ||\n type === \"DESTINATION_SWAP\" ||\n type === \"SWAP_COMPLETE\" ||\n type === \"SWAP_SKIPPED\"\n ) {\n markSwapExecutionStarted();\n }\n\n if (\n sourceMovementStep &&\n hasSourceMovementTransaction &&\n PLAN_STEP_FUNDS_MAY_HAVE_MOVED_STATES.has(rawState)\n ) {\n fundsMovedRef.current = true;\n }\n\n if (explorerUrl) {\n if (\n rawStepType === \"destination_swap\" ||\n rawStepType === \"execute_transaction\" ||\n type.includes(\"DESTINATION_SWAP\") ||\n type === \"TRANSACTION_SENT\" ||\n type === \"TRANSACTION_CONFIRMED\"\n ) {\n mergeExplorerUrls({ destinationExplorerUrl: explorerUrl });\n } else if (sourceMovementStep) {\n mergeExplorerUrls({ sourceExplorerUrl: explorerUrl });\n }\n\n if (\n !intentUrlRef.current &&\n (rawStepType === \"bridge_intent_submission\" ||\n rawStepType === \"request_submission\" ||\n type === \"BRIDGE_INTENT_SUBMISSION\")\n ) {\n patchCurrentIntentExplorerUrl(explorerUrl);\n }\n }\n\n if (completed) {\n onStepComplete(step as SwapStepType);\n }\n };\n\n const handlePlanEvent = (event: any) => {\n if (event.type === \"plan_preview\" || event.type === \"plan_confirmed\") {\n const stepList = Array.isArray(event.plan?.steps)\n ? event.plan.steps.map((step: any) =>\n normalizePlanStep(step, step?.type, undefined, false),\n )\n : [];\n logSwapPlanSteps(event.type, stepList, event.plan?.steps);\n if (stepList.length === 0) return;\n\n if (hasSwapPlanSteps(stepList)) {\n swapStepsListRef.current = stepList as SwapStepType[];\n appendProgressListEvent(\n PROGRESS_EVENT_NAMES.SWAP_PLAN_LIST,\n stepList,\n );\n } else {\n appendProgressListEvent(\n PROGRESS_EVENT_NAMES.BRIDGE_PLAN_LIST,\n stepList,\n );\n }\n onStepsList(stepList as SwapStepType[]);\n return;\n }\n\n if (event.type !== \"plan_progress\") {\n logSdkSwapEvent(\"unhandled typed event\", event);\n return;\n }\n\n const state = String(event.state ?? \"\").toLowerCase();\n const completed = PLAN_FINAL_STATES.has(state);\n const step = normalizePlanStep(\n event.step,\n event.stepType,\n event.state,\n completed,\n );\n const eventName = isActionPlanStep(step)\n ? PROGRESS_EVENT_NAMES.BRIDGE_PLAN_PROGRESS\n : PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS;\n\n logSwapPlanProgress(event, step, eventName, completed);\n appendProgressEvent(eventName, step, completed, event);\n handleProgressStepSideEffects(event, step, completed);\n };\n\n const appendSkippedSwapProgress = () => {\n const step = {\n completed: true,\n type: \"SWAP_SKIPPED\",\n typeID: \"SWAP_SKIPPED\",\n } as SwapStepType;\n enterSkippedSwapProgress();\n appendProgressEvent(PROGRESS_EVENT_NAMES.SWAP_PLAN_PROGRESS, step, true);\n onStepComplete(step);\n };\n\n const handleSwapEvent = (event: any) => {\n if (!event || typeof event !== \"object\") return;\n if (typeof event.type === \"string\") {\n handlePlanEvent(event);\n return;\n }\n logSdkSwapEvent(\"ignored event without string type\", event);\n };\n\n const onEvent = (event: any) => {\n const isCurrentRun = swapRunIdRef.current === runId;\n const isCurrentQuote = isCurrentQuoteInput();\n logSdkSwapEvent(\"onEvent\", event, {\n currentRunId: swapRunIdRef.current,\n isCurrentQuote,\n isCurrentRun,\n quoteInputKey,\n runId,\n });\n if (!isCurrentRun || !isCurrentQuote) {\n logSdkSwapEvent(\"ignored stale onEvent\", event, {\n currentRunId: swapRunIdRef.current,\n isCurrentQuote,\n quoteInputKey,\n runId,\n });\n return;\n }\n patchCurrentIntentExplorerUrl(\n getEventIntentExplorerUrl(appConfig.nexusNetwork, event),\n );\n handleSwapEvent(event);\n };\n\n const buildRecipientTransferExecuteConfig = (transferAmount: bigint) => {\n if (!resolvedRecipientAddress) {\n throw new Error(\"Recipient address is required\");\n }\n\n const isNative =\n !toToken.contractAddress ||\n toToken.contractAddress.toLowerCase() ===\n \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\" ||\n toToken.contractAddress ===\n \"0x0000000000000000000000000000000000000000\";\n\n if (isNative) {\n return {\n to: resolvedRecipientAddress as `0x${string}`,\n value: transferAmount,\n gas: BigInt(100000),\n };\n }\n\n return {\n to: toToken.contractAddress as `0x${string}`,\n data: encodeFunctionData({\n abi: erc20Abi,\n functionName: \"transfer\",\n args: [resolvedRecipientAddress as `0x${string}`, transferAmount],\n }),\n gas: BigInt(100000),\n };\n };\n\n const executeRecipientTransfer = async (transferAmount: bigint) => {\n const result = await nexusSDK.execute(\n {\n toChainId: toToken.chainId!,\n ...buildRecipientTransferExecuteConfig(transferAmount),\n },\n { onEvent },\n );\n const finalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result,\n );\n if (finalExplorerUrl) {\n setTransferExplorerUrl(finalExplorerUrl);\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n return finalExplorerUrl;\n };\n\n try {\n if (!isExactOutFlow) {\n const fromPayload: {\n chainId: number;\n tokenAddress: `0x${string}`;\n amountRaw: bigint;\n }[] = [];\n\n const exactInSourceTokens = getReadyExactInSourceTokens(fromTokens);\n\n for (const token of exactInSourceTokens) {\n // Determine the amount to use for this specific token\n let rawAmountStr = token.userAmount;\n if (!rawAmountStr && exactInSourceTokens.length === 1) {\n rawAmountStr = amount; // fallback for single-token case\n }\n\n let cleanAmount = parseFiatNumber(rawAmountStr) ?? new Decimal(0);\n if (cleanAmount.lte(0)) continue;\n\n if (token.userAmountMode === \"usd\") {\n const tokenBalance =\n parseFiatNumber(token.balance) ?? new Decimal(0);\n const fiatBalance =\n parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n const price = tokenBalance.gt(0)\n ? fiatBalance.div(tokenBalance)\n : new Decimal(0);\n if (price.gt(0)) {\n cleanAmount = cleanAmount.div(price);\n } else {\n cleanAmount = new Decimal(0);\n }\n }\n\n if (cleanAmount.lte(0)) continue;\n\n const safeTokenAmountStr = toViemDecimalString(\n cleanAmount,\n token.decimals || 18,\n );\n\n fromPayload.push({\n chainId: token.chainId!,\n tokenAddress: token.contractAddress as `0x${string}`,\n amountRaw: parseUnits(safeTokenAmountStr, token.decimals || 18),\n });\n }\n\n if (fromPayload.length === 0) {\n throw new Error(\"No source amount available for swap.\");\n }\n\n resetExplorerUrls();\n const exactInSwapPayload = {\n sources: fromPayload,\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n };\n let intentExplorerUrl: string | null = null;\n let intentId = currentSwapEntry?.intentId;\n let finalExplorerUrl: string | null =\n explorerUrlsRef.current.destinationExplorerUrl ||\n explorerUrlsRef.current.sourceExplorerUrl;\n\n if (hasCustomSwapRecipient && resolvedRecipientAddress) {\n const sdkWithOptionalTransfer = nexusSDK as any;\n\n if (typeof sdkWithOptionalTransfer.swapAndTransfer === \"function\") {\n const swapAndTransferExactInInput = {\n mode: \"exactIn\",\n recipient: resolvedRecipientAddress as `0x${string}`,\n ...exactInSwapPayload,\n };\n logSdkIntentInput(\n \"swapAndTransfer exactIn\",\n swapAndTransferExactInInput,\n {\n activeMode,\n quoteInputKey,\n runId,\n },\n );\n const result = await sdkWithOptionalTransfer.swapAndTransfer(\n swapAndTransferExactInInput,\n { onEvent },\n );\n if (result?.success === false) {\n throw new Error(result?.error || \"Swap and transfer failed\");\n }\n\n const swapResult = getSdkSwapResult(result);\n intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result,\n swapResult,\n );\n intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const resultFinalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result,\n swapResult,\n );\n finalExplorerUrl = resultFinalExplorerUrl || finalExplorerUrl;\n if (resultFinalExplorerUrl) {\n setTransferExplorerUrl(resultFinalExplorerUrl);\n mergeExplorerUrls({\n destinationExplorerUrl: resultFinalExplorerUrl,\n });\n }\n } else {\n logSdkIntentInput(\"swapWithExactIn\", exactInSwapPayload, {\n activeMode,\n quoteInputKey,\n runId,\n });\n startIntentFetchTiming({\n background,\n operation: \"swapWithExactIn\",\n quoteInputKey,\n runId,\n });\n const result = await nexusSDK.swapWithExactIn(exactInSwapPayload, {\n hooks: {\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n },\n onEvent,\n });\n\n intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result,\n );\n intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n\n const latestSwapIntent = (\n swapIntentRef.current as unknown as {\n intent?: SwapIntentData;\n } | null\n )?.intent;\n const transferAmount = latestSwapIntent?.destination?.amount;\n if (!transferAmount) {\n throw new Error(\n \"Unable to determine received amount to transfer.\",\n );\n }\n\n const transferAmountBigInt = parseUnits(\n toViemDecimalString(transferAmount, toToken.decimals || 18),\n toToken.decimals || 18,\n );\n finalExplorerUrl =\n (await executeRecipientTransfer(transferAmountBigInt)) ||\n finalExplorerUrl;\n }\n } else {\n // Start exact-in swap — the intent hook will fire and populate preview\n logSdkIntentInput(\"swapWithExactIn\", exactInSwapPayload, {\n activeMode,\n quoteInputKey,\n runId,\n });\n startIntentFetchTiming({\n background,\n operation: \"swapWithExactIn\",\n quoteInputKey,\n runId,\n });\n const result = await nexusSDK.swapWithExactIn(exactInSwapPayload, {\n hooks: {\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n },\n onEvent,\n });\n intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result,\n );\n intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const swapResult = getSdkSwapResult(result);\n const resultFinalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result,\n swapResult,\n );\n finalExplorerUrl = resultFinalExplorerUrl || finalExplorerUrl;\n if (resultFinalExplorerUrl) {\n setTransferExplorerUrl(resultFinalExplorerUrl);\n mergeExplorerUrls({\n destinationExplorerUrl: resultFinalExplorerUrl,\n });\n }\n }\n\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n const resolvedFinalExplorerUrl =\n finalExplorerUrl ||\n explorerUrlsRef.current.destinationExplorerUrl ||\n explorerUrlsRef.current.sourceExplorerUrl;\n const resolvedIntentExplorerUrl =\n intentExplorerUrl || intentUrlRef.current;\n finishCurrentSwapHistoryEntry(\"fulfilled\", {\n finalExplorerUrl: resolvedFinalExplorerUrl,\n ...(resolvedIntentExplorerUrl\n ? { intentExplorerUrl: resolvedIntentExplorerUrl }\n : {}),\n ...(intentId ? { intentId } : {}),\n });\n resetInputsAfterSuccessfulExecution();\n onComplete?.();\n setSwapStep(\"success\");\n }\n } else {\n const exactOutAmountString =\n activeMode === \"deposit\"\n ? depositTokenAmountForQuote\n ?.toDecimalPlaces(toToken.decimals || 18, Decimal.ROUND_DOWN)\n .toFixed()\n : amount;\n if (!exactOutAmountString || new Decimal(exactOutAmountString).lte(0)) {\n setTxError(\n depositAmountMode === \"usd\"\n ? \"Unable to convert USD amount into the destination token amount.\"\n : \"Enter a valid amount.\",\n );\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n const amountBigInt = parseUnits(\n toViemDecimalString(exactOutAmountString, toToken.decimals || 18),\n toToken.decimals || 18,\n );\n\n resetExplorerUrls();\n\n const fromSourcesPayload = buildExactOutSourcesPayload(\n getExactOutSourceTokens(),\n );\n\n let executeConfig: any;\n if (activeMode === \"deposit\" && !selectedOpportunity?.executeDeposit) {\n throw new Error(\"Deposit config is missing executeDeposit.\");\n }\n\n if (activeMode === \"deposit\" && selectedOpportunity) {\n const user = (ownerAddress ?? connectedAddress) as `0x${string}`;\n\n const executeParams = selectedOpportunity.executeDeposit(\n selectedOpportunity.tokenSymbol,\n selectedOpportunity.tokenAddress,\n amountBigInt,\n selectedOpportunity.chainId,\n user,\n );\n if (!isPositiveGasLimit(executeParams.gas)) {\n throw new Error(\n \"Deposit config executeDeposit must return a positive gas limit.\",\n );\n }\n executeConfig = {\n to: executeParams.to,\n value: executeParams.value,\n data: executeParams.data,\n tokenApproval: executeParams.tokenApproval,\n gas: executeParams.gas,\n };\n } else if (\n (activeMode === \"send\" || hasCustomSwapRecipient) &&\n resolvedRecipientAddress\n ) {\n executeConfig = buildRecipientTransferExecuteConfig(amountBigInt);\n }\n\n if (executeConfig?.tokenApproval) {\n executeConfig = {\n ...executeConfig,\n tokenApproval: {\n toTokenAddress:\n executeConfig.tokenApproval.toTokenAddress ||\n toToken.contractAddress,\n amount: executeConfig.tokenApproval.amount,\n spender: executeConfig.tokenApproval.spender,\n },\n };\n }\n\n if (executeConfig) {\n const sdkWithOptionalTransfer = nexusSDK as any;\n const isTransferExactOut =\n (activeMode === \"send\" || hasCustomSwapRecipient) &&\n typeof sdkWithOptionalTransfer.swapAndTransfer === \"function\";\n const exactOutOperationInput = isTransferExactOut\n ? {\n mode: \"exactOut\",\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmountRaw: amountBigInt,\n recipient: resolvedRecipientAddress as `0x${string}`,\n ...fromSourcesPayload,\n }\n : {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmountRaw: amountBigInt,\n execute: executeConfig,\n ...fromSourcesPayload,\n };\n logSdkIntentInput(\n isTransferExactOut\n ? \"swapAndTransfer exactOut\"\n : \"swapAndExecute exactOut\",\n exactOutOperationInput,\n {\n activeMode,\n quoteInputKey,\n runId,\n },\n );\n startIntentFetchTiming({\n background,\n operation: isTransferExactOut\n ? \"swapAndTransfer exactOut\"\n : \"swapAndExecute exactOut\",\n quoteInputKey,\n runId,\n });\n const result = isTransferExactOut\n ? await sdkWithOptionalTransfer.swapAndTransfer(\n exactOutOperationInput,\n {\n onEvent,\n onIntent: (data: any) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n },\n )\n : await nexusSDK.swapAndExecute(exactOutOperationInput as any, {\n onEvent,\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n });\n const swapResult = result?.swapResult ?? result?.result ?? null;\n const swapSkipped = Boolean((result as any)?.swapSkipped);\n if (swapSkipped) {\n appendSkippedSwapProgress();\n }\n if (\n !swapResult &&\n !swapSkipped &&\n activeMode !== \"send\" &&\n !hasCustomSwapRecipient\n ) {\n throw new Error(\"Swap failed\");\n }\n const executeTxHash = getSdkTransactionHash(result);\n const intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result,\n swapResult,\n );\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const finalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n executeTxHash,\n result,\n swapResult,\n );\n if (finalExplorerUrl) {\n if (activeMode === \"send\" || hasCustomSwapRecipient) {\n setTransferExplorerUrl(finalExplorerUrl);\n }\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n patchCurrentSwapHistoryEntry({\n ...(finalExplorerUrl ? { finalExplorerUrl } : {}),\n ...(intentExplorerUrl ? { intentExplorerUrl } : {}),\n ...(intentId ? { intentId } : {}),\n });\n } else {\n const exactOutSwapInput = {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmountRaw: amountBigInt,\n ...fromSourcesPayload,\n };\n logSdkIntentInput(\"swapWithExactOut\", exactOutSwapInput, {\n activeMode,\n quoteInputKey,\n runId,\n });\n startIntentFetchTiming({\n background,\n operation: \"swapWithExactOut\",\n quoteInputKey,\n runId,\n });\n const result = await nexusSDK.swapWithExactOut(exactOutSwapInput, {\n hooks: {\n onIntent: (data) =>\n handleSwapIntentCallback(data, runId, quoteInputKey),\n },\n onEvent,\n });\n const intentExplorerUrl = getSdkIntentExplorerUrlForNetwork(\n appConfig.nexusNetwork,\n result,\n );\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ??\n currentSwapEntry?.intentId;\n const swapResult = getSdkSwapResult(result);\n const finalExplorerUrl =\n getSdkExplorerUrl(result) ||\n getExplorerTxUrl(\n toToken.chainId,\n getSdkTransactionHash(result),\n result,\n swapResult,\n );\n if (finalExplorerUrl) {\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n patchCurrentSwapHistoryEntry({\n ...(finalExplorerUrl ? { finalExplorerUrl } : {}),\n ...(intentExplorerUrl ? { intentExplorerUrl } : {}),\n ...(intentId ? { intentId } : {}),\n });\n }\n\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n finishCurrentSwapHistoryEntry(\"fulfilled\");\n resetInputsAfterSuccessfulExecution();\n onComplete?.();\n if (activeMode === \"deposit\") {\n reachedTerminalRef.current = true;\n const now = Date.now();\n trackDeposit(\"deposit_completed\", {\n postConfirmDurationMs: previewConfirmedTsRef.current\n ? now - previewConfirmedTsRef.current\n : 0,\n totalDurationMs: now - widgetOpenedTsRef.current,\n attemptCount: attemptCountRef.current,\n amountToken: depositTokenDisplay,\n amountUsd: Number(depositUsdDisplay) || 0,\n });\n }\n setSwapStep(\"success\");\n }\n }\n } catch (err: any) {\n const caughtTimeout = isTimeoutLikeError(err);\n if (caughtTimeout) {\n console.warn(\"Timeout in handleEnterPreview:\", err);\n } else {\n console.error(\"Error in handleEnterPreview:\", err);\n }\n if (swapRunIdRef.current !== runId || !isCurrentQuoteInput()) {\n return;\n }\n finishIntentFetchTiming(runId, \"failed\");\n if (activeMode === \"deposit\" && err?.code !== \"USER_DENIED_INTENT\") {\n const hasActiveExecution =\n swapStepRef.current === \"progress\" &&\n Boolean(currentSwapIdRef.current);\n const isInsufficient = isInsufficientSourcesError(err);\n const errMessage =\n (typeof err?.message === \"string\" ? err.message : \"\") ||\n (typeof err === \"string\" ? err : \"\");\n const errName = typeof err?.name === \"string\" ? err.name : \"\";\n const isTimeout = isTimeoutLikeError(err);\n const isUserRejected =\n err?.code === 4001 ||\n err?.code === \"ACTION_REJECTED\" ||\n errName === \"UserRejectedRequestError\" ||\n /user rejected|user denied/i.test(errMessage);\n const failedAtStep:\n | \"simulation\"\n | \"nexus_operation\"\n | \"execute_leg\"\n | \"unknown\" = !hasActiveExecution ? \"simulation\" : \"nexus_operation\";\n const errorCategory: string = isUserRejected\n ? \"user_rejected\"\n : isTimeout\n ? \"timeout\"\n : isInsufficient\n ? \"no_eligible_sources\"\n : !hasActiveExecution\n ? \"quote_failed\"\n : fundsMovedRef.current\n ? \"pending_after_funds_moved\"\n : \"execution_failed\";\n reachedTerminalRef.current = true;\n if (fundsMovedRef.current) {\n trackDeposit(\"deposit_pending_after_funds_moved\", {\n errorCode: err?.code ?? \"UNKNOWN\",\n errorMessage: errMessage || \"Transaction pending.\",\n intentUrl: intentUrlRef.current,\n failedAtStep,\n });\n } else {\n trackDeposit(\"deposit_failed\", {\n errorCode: err?.code ?? \"UNKNOWN\",\n errorCategory,\n errorMessage: errMessage || \"Transaction failed.\",\n failedAtStep,\n });\n }\n }\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n const hasActiveExecution =\n swapStepRef.current === \"progress\" && Boolean(currentSwapIdRef.current);\n const isTimeout = caughtTimeout;\n const showFailedProgressThenReceipt = (\n error: string,\n patch: Partial = {},\n ) => {\n const failedProgressEvent = progressEventsRef.current.at(-1);\n const isTransferExecution =\n activeMode === \"send\" || hasCustomSwapRecipient;\n const fallbackFailedStep =\n activeMode === \"deposit\" || isTransferExecution\n ? ({ type: \"APPROVAL\", typeID: \"AP\" } as BridgeStepType)\n : ({\n type: \"DETERMINING_SWAP\",\n typeID: \"DETERMINING_SWAP\",\n } as unknown as SwapStepType);\n const failedStep = failedProgressEvent?.step ?? fallbackFailedStep;\n const autoRefundAvailable =\n isAutoRefundAvailableProgressEvent(failedProgressEvent);\n setFailedProgressStep(failedStep);\n finishCurrentSwapHistoryEntry(\"failed\", {\n error,\n autoRefundAvailable,\n failureDescription: getFailureDescriptionForProgressStep(\n failedStep,\n autoRefundAvailable,\n ),\n failureMessage: getFailureMessageForProgressStep(\n failedStep,\n hasCustomSwapRecipient ? \"send\" : activeMode,\n autoRefundAvailable,\n ),\n failedStepType: getProgressStepType(failedStep),\n ...patch,\n });\n window.setTimeout(() => {\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n setSwapStep(\"failed\");\n }\n }, 700);\n };\n const showTimeoutReceipt = (\n message = \"Transaction timed out\",\n patch: Partial = {},\n ) => {\n finishCurrentSwapHistoryEntry(\"timeout\", {\n error: message,\n failureDescription:\n \"This transaction is still pending. Check the intent explorer for the latest status.\",\n failureMessage: TIMEOUT_LABEL,\n ...patch,\n });\n window.setTimeout(() => {\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n setSwapStep(\"failed\");\n }\n }, 700);\n };\n if (err?.code === \"USER_DENIED_INTENT\") {\n if (hasActiveExecution) {\n showFailedProgressThenReceipt(\"Transaction cancelled by user\");\n } else if (!background && swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n return;\n }\n if (isInsufficientSourcesError(err) && !hasActiveExecution) {\n const issue = buildInsufficientSourcesIssue(err);\n if (!background || swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n setTxError(null);\n setSwapQuoteIssue(issue);\n onError?.(issue.message);\n return;\n }\n if (\n isExactOutFlow &&\n isViemInvalidDecimalError(err) &&\n !hasActiveExecution\n ) {\n const issue = buildExactOutSourceBalanceIssue();\n if (issue) {\n if (!background || swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n setTxError(null);\n setSwapQuoteIssue(issue);\n onError?.(issue.message);\n return;\n }\n }\n const errorMessage =\n err?.message ||\n (typeof err === \"string\"\n ? err\n : \"Transaction failed. Please try again or check console.\");\n if (isTimeout && hasActiveExecution) {\n showTimeoutReceipt(errorMessage);\n setTxError(null);\n return;\n }\n if (hasActiveExecution && fundsMovedRef.current) {\n showTimeoutReceipt(errorMessage, {\n failureDescription:\n \"Funds have already moved and the transaction is still pending. Check the intent explorer for the latest status.\",\n });\n setTxError(null);\n return;\n }\n if (hasActiveExecution) {\n showFailedProgressThenReceipt(errorMessage);\n } else if (!background || swapStepRef.current === \"preview-intent\") {\n setSwapStep(\"idle\");\n }\n setTxError(errorMessage);\n onError?.(errorMessage);\n }\n };\n\n const hasInsufficientSourcesQuoteIssue =\n swapQuoteIssue?.type === \"insufficientSources\";\n const hasReceiveAmountQuoteIssue = Boolean(receiveAmountIssue);\n\n useEffect(() => {\n if (activeMode !== \"swap\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = false;\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n }\n\n if (hasReceiveAmountQuoteIssue) {\n clearPendingSwapIntent(true);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n if (hasInsufficientSourcesQuoteIssue) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n const hasEnoughForQuote = hasReadyExactInSwapInput(fromTokens, toToken);\n\n if (!hasEnoughForQuote) {\n clearPendingSwapIntent();\n setSwapQuoteIssue(null);\n setTxError(null);\n return;\n }\n\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n return;\n }\n\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n let quoteStarted = false;\n const quoteDelay = getQuoteRequestDelay();\n const timer = window.setTimeout(() => {\n quoteStarted = true;\n void handleEnterPreview({ background: true });\n }, quoteDelay);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (!quoteStarted && swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n activeQuoteInputKey,\n amount,\n defaultRecipientAddress,\n fromTokensQuoteKey,\n hasCurrentQuoteIntent,\n hasInsufficientSourcesQuoteIssue,\n hasReceiveAmountQuoteIssue,\n getQuoteRequestDelay,\n nexusSDK,\n recipientAddress,\n swapStep,\n toTokenQuoteKey,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = false;\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n }\n\n if (hasReceiveAmountQuoteIssue) {\n clearPendingSwapIntent(true);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n if (hasInsufficientSourcesQuoteIssue) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n const parsedAmount = parseFiatNumber(amount);\n const hasEnoughForQuote = Boolean(\n parsedAmount?.gt(0) &&\n toToken &&\n selectedOpportunity &&\n depositTokenAmountForQuote,\n );\n\n if (!hasEnoughForQuote) {\n clearPendingSwapIntent();\n clearSelectedSources();\n return;\n }\n\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n return;\n }\n\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n let quoteStarted = false;\n const quoteDelay = getQuoteRequestDelay();\n const timer = window.setTimeout(() => {\n quoteStarted = true;\n void handleEnterPreview({ background: true });\n }, quoteDelay);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (!quoteStarted && swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n amount,\n activeQuoteInputKey,\n depositAmountMode,\n depositQuoteAmountKey,\n hasCurrentQuoteIntent,\n hasInsufficientSourcesQuoteIssue,\n hasReceiveAmountQuoteIssue,\n getQuoteRequestDelay,\n nexusSDK,\n sourceSelectionRevision,\n selectedOpportunityIdentity,\n swapStep,\n toTokenQuoteKey,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"send\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = false;\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n }\n\n if (hasReceiveAmountQuoteIssue) {\n clearPendingSwapIntent(true);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n if (hasInsufficientSourcesQuoteIssue) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n return;\n }\n\n const parsedAmount = parseFiatNumber(amount);\n const hasEnoughForQuote = Boolean(parsedAmount?.gt(0) && toToken);\n\n if (!hasEnoughForQuote) {\n clearPendingSwapIntent();\n clearSelectedSources();\n return;\n }\n\n if (hasCurrentQuoteIntent) {\n setIntentLoading(false);\n setQuoteRefreshing(false);\n return;\n }\n\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n let quoteStarted = false;\n const quoteDelay = getQuoteRequestDelay();\n const timer = window.setTimeout(() => {\n quoteStarted = true;\n void handleEnterPreview({ background: true });\n }, quoteDelay);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (!quoteStarted && swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n amount,\n activeQuoteInputKey,\n hasCurrentQuoteIntent,\n hasInsufficientSourcesQuoteIssue,\n hasReceiveAmountQuoteIssue,\n getQuoteRequestDelay,\n nexusSDK,\n sourceSelectionRevision,\n swapStep,\n toTokenQuoteKey,\n ]);\n\n const refreshActiveSwapIntent = useCallback(async () => {\n if (receiveAmountIssueRef.current) return;\n\n const activeIntent = swapIntentRef.current;\n if (\n !activeIntent ||\n intentLoading ||\n quoteRefreshing ||\n receiveMaxCalculating ||\n previewQuoteRefreshing\n ) {\n return;\n }\n\n const runId = activeIntent.runId;\n const quoteInputKey = activeIntent.quoteInputKey;\n if (!quoteInputKey || activeQuoteInputKeyRef.current !== quoteInputKey) {\n return;\n }\n const isPreviewRefresh = swapStepRef.current === \"preview-intent\";\n if (isPreviewRefresh) {\n setPreviewQuoteRefreshing(true);\n } else {\n setQuoteRefreshing(true);\n }\n try {\n const updatedRaw = await activeIntent.refresh();\n const updatedBridgeProvider = normalizeBridgeProvider(\n (updatedRaw as any)?.bridgeProvider ??\n (updatedRaw as any)?.normalizedIntent?.bridgeProvider ??\n (updatedRaw as any)?.swap?.bridgeProvider ??\n activeIntent.intent?.bridgeProvider,\n );\n const updated = normalizeRenderableSwapIntentData(\n updatedRaw,\n updatedBridgeProvider,\n );\n if (\n !updated ||\n swapRunIdRef.current !== runId ||\n activeQuoteInputKeyRef.current !== quoteInputKey\n ) {\n return;\n }\n\n if (swapIntentRef.current) {\n swapIntentRef.current.intent = updated;\n }\n applySwapIntent(updated);\n } catch (err) {\n console.error(\"Unable to refresh swap intent\", err);\n } finally {\n if (\n swapRunIdRef.current === runId &&\n activeQuoteInputKeyRef.current === quoteInputKey\n ) {\n if (isPreviewRefresh) {\n setPreviewQuoteRefreshing(false);\n } else {\n setQuoteRefreshing(false);\n }\n }\n }\n }, [\n applySwapIntent,\n intentLoading,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n ]);\n\n useEffect(() => {\n const hasRefreshableIntent =\n (activeMode === \"swap\" ||\n activeMode === \"deposit\" ||\n activeMode === \"send\") &&\n Boolean(\n intentData &&\n swapIntentRef.current &&\n swapIntentRef.current.quoteInputKey === activeQuoteInputKey,\n ) &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n\n if (!hasRefreshableIntent || receiveAmountIssue) {\n setQuoteRefreshProgress(0);\n setQuoteRefreshSecondsRemaining(0);\n return;\n }\n\n let cancelled = false;\n let timeout: number | undefined;\n\n const scheduleRefresh = () => {\n const quoteAge = Date.now() - lastSwapIntentRefreshAtRef.current;\n const delay = Math.max(0, QUOTE_REFRESH_INTERVAL_MS - quoteAge);\n timeout = window.setTimeout(() => {\n if (receiveAmountIssueRef.current) {\n clearPendingSwapIntent(true);\n setQuoteRefreshProgress(0);\n setQuoteRefreshSecondsRemaining(0);\n return;\n }\n\n if (\n intentLoading ||\n quoteRefreshing ||\n receiveMaxCalculating ||\n previewQuoteRefreshing\n ) {\n if (!cancelled) {\n timeout = window.setTimeout(scheduleRefresh, 1000);\n }\n return;\n }\n\n void refreshActiveSwapIntent().finally(() => {\n if (!cancelled) {\n scheduleRefresh();\n }\n });\n }, delay);\n };\n\n scheduleRefresh();\n\n return () => {\n cancelled = true;\n if (timeout !== undefined) {\n window.clearTimeout(timeout);\n }\n };\n }, [\n activeMode,\n activeQuoteInputKey,\n intentData,\n intentLoading,\n receiveAmountIssue,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n refreshActiveSwapIntent,\n swapStep,\n ]);\n\n useEffect(() => {\n const hasRefreshableIntent =\n (activeMode === \"swap\" ||\n activeMode === \"deposit\" ||\n activeMode === \"send\") &&\n Boolean(\n intentData &&\n swapIntentRef.current &&\n swapIntentRef.current.quoteInputKey === activeQuoteInputKey,\n ) &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n\n if (!hasRefreshableIntent || receiveAmountIssue) {\n setQuoteRefreshProgress(0);\n setQuoteRefreshSecondsRemaining(0);\n return;\n }\n\n const updateProgress = () => {\n const quoteAge = Date.now() - lastSwapIntentRefreshAtRef.current;\n const remaining = Math.max(0, QUOTE_REFRESH_INTERVAL_MS - quoteAge);\n setQuoteRefreshProgress(remaining / QUOTE_REFRESH_INTERVAL_MS);\n setQuoteRefreshSecondsRemaining(Math.ceil(remaining / 1000));\n };\n\n updateProgress();\n const interval = window.setInterval(updateProgress, 250);\n\n return () => window.clearInterval(interval);\n }, [\n activeMode,\n activeQuoteInputKey,\n intentData,\n receiveAmountIssue,\n swapStep,\n ]);\n\n /** User accepted swap from the preview — call allow() from the intent hook */\n const handleSwapAccept = () => {\n const activeIntent = swapIntentRef.current;\n if (activeIntent) {\n if (\n activeIntent.quoteInputKey &&\n activeQuoteInputKeyRef.current !== activeIntent.quoteInputKey\n ) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n setSwapStep(\"idle\");\n return;\n }\n if (activeMode === \"deposit\") {\n previewConfirmedTsRef.current = Date.now();\n attemptCountRef.current += 1;\n const timeInPreviewMs = previewViewedTsRef.current\n ? previewConfirmedTsRef.current - previewViewedTsRef.current\n : 0;\n trackDeposit(\"deposit_preview_confirmed\", {\n timeInPreviewMs,\n totalFeeUsd: Number(intentFeeUsd) || 0,\n sourceCount: (intentData?.sources ?? []).length,\n });\n }\n onStart?.();\n startSwapHistoryEntry();\n setSwapStep(\"progress\");\n setQuoteRefreshing(false);\n resetProgressEvents();\n if (swapStepsListRef.current.length > 0) {\n seed(swapStepsListRef.current);\n } else {\n resetSteps();\n }\n activeIntent.allow();\n }\n };\n\n // ---------------------------------------------------------------------------\n // Header title\n // ---------------------------------------------------------------------------\n const isDepositMethodScreen =\n activeMode === \"deposit\" &&\n swapStep === \"idle\" &&\n depositFundingStep === \"method\";\n const isDepositWalletScreen =\n activeMode === \"deposit\" &&\n swapStep === \"idle\" &&\n depositFundingStep === \"wallet\";\n const isDepositOnrampScreen =\n activeMode === \"deposit\" &&\n swapStep === \"idle\" &&\n depositFundingStep === \"onramp\";\n const normalizedDepositOnrampSessionState =\n depositOnrampSessionState?.trim().toUpperCase() ?? \"\";\n const hasDepositOnrampSession =\n isDepositOnrampScreen && Boolean(depositOnrampSessionState);\n const getDepositOnrampSessionTitle = () => {\n if (\n normalizedDepositOnrampSessionState === \"SETTLED\" ||\n [\n \"COMPLETED\",\n \"DEPOSIT_COMPLETE\",\n \"DEPOSIT_SUCCESS\",\n \"DEPOSITED\",\n ].includes(normalizedDepositOnrampSessionState)\n ) {\n return \"Success\";\n }\n if (normalizedDepositOnrampSessionState === \"FAILED\") {\n return \"Payment failed\";\n }\n if (\n [\n \"DEPOSIT_ATTENTION\",\n \"DEPOSIT_FAILED\",\n \"DEPOSIT_REQUIRES_ATTENTION\",\n ].includes(normalizedDepositOnrampSessionState)\n ) {\n return \"Deposit needs your attention\";\n }\n if (normalizedDepositOnrampSessionState === \"CANCELLED\") {\n return \"Payment cancelled\";\n }\n if (normalizedDepositOnrampSessionState === \"REFUNDED\") {\n return \"Payment refunded\";\n }\n if (normalizedDepositOnrampSessionState === \"EXPIRED\") {\n return \"Payment expired\";\n }\n if (\n [\"COMPLETING_DEPOSIT\", \"DEPOSIT_PROCESSING\", \"DEPOSITING\"].includes(\n normalizedDepositOnrampSessionState,\n )\n ) {\n return \"Completing your deposit\";\n }\n if (\n normalizedDepositOnrampSessionState === \"PROCESSING\" ||\n normalizedDepositOnrampSessionState === \"SETTLING\"\n ) {\n return \"Processing your payment\";\n }\n return \"Continue on Other Window\";\n };\n\n const getTitle = () => {\n const configuredWidgetHeading = normalizeConfiguredString(\n appearanceConfig?.widgetHeading,\n );\n if (swapStep === \"history\") return \"Transaction History\";\n // Drawer panels overlay the main page,\n // so the header should still show the main page title.\n\n if (swapStep === \"preview-intent\") {\n return activeMode === \"deposit\"\n ? \"Confirm Deposit\"\n : activeMode === \"send\"\n ? \"Confirm Send\"\n : \"Confirm Swap\";\n }\n\n if (activeMode === \"swap\") {\n if (swapStep === \"progress\") return \"Swapping…\";\n if (swapStep === \"success\") return \"Swap Complete\";\n if (swapStep === \"failed\" && currentSwapEntry?.status === \"timeout\") {\n return TIMEOUT_LABEL;\n }\n if (swapStep === \"failed\") return \"Swap Failed\";\n return configuredWidgetHeading ?? \"Swap and Bridge\";\n }\n if (activeMode === \"deposit\") {\n if (isDepositOnrampScreen) {\n return hasDepositOnrampSession\n ? getDepositOnrampSessionTitle()\n : \"Payment Amount\";\n }\n if (swapStep === \"progress\") return \"Depositing…\";\n if (swapStep === \"success\") return \"Deposit Complete\";\n if (swapStep === \"failed\" && currentSwapEntry?.status === \"timeout\") {\n return TIMEOUT_LABEL;\n }\n if (swapStep === \"failed\") return \"Deposit Failed\";\n return configuredWidgetHeading ?? \"Deposit\";\n }\n if (activeMode === \"send\") {\n if (swapStep === \"progress\") return \"Sending…\";\n if (swapStep === \"success\") return \"Send Complete\";\n if (swapStep === \"failed\" && currentSwapEntry?.status === \"timeout\") {\n return TIMEOUT_LABEL;\n }\n if (swapStep === \"failed\") return \"Send Failed\";\n return configuredWidgetHeading ?? \"Send\";\n }\n return configuredWidgetHeading ?? \"Nexus Widget\";\n };\n\n // Titles that should be center-aligned (main screens / confirm screens)\n // Left-aligned: choose-swap-asset, choose-receive-asset (sub-screens with subtitles)\n const isTitleCentered = () => {\n if (swapStep === \"history\") return false;\n return true; // idle, drawer panels, preview-intent, progress, etc.\n };\n\n const canGoBack =\n swapStep === \"preview-intent\" ||\n swapStep === \"history\" ||\n isDepositWalletScreen ||\n (isDepositOnrampScreen && !hasDepositOnrampSession);\n const showHistoryButton = !(isDepositMethodScreen || isDepositOnrampScreen);\n const handleBack = () => {\n if (\n activeMode === \"deposit\" &&\n swapStep === \"idle\" &&\n depositFundingStep !== \"method\"\n ) {\n setDepositFundingStep(\"method\");\n return;\n }\n if (swapStep === \"history\") {\n setSwapStep(\"idle\");\n return;\n }\n if (swapStep === \"choose-swap-asset\") {\n handleSourcePickerCancel();\n return;\n }\n if (swapStep === \"choose-receive-asset\") {\n closeDrawerToIdle();\n return;\n }\n if (swapStep === \"enter-recipient\") {\n closeDrawerToIdle();\n return;\n }\n if (swapStep === \"preview-intent\") {\n const canRequoteAfterPreviewBack =\n activeMode === \"swap\"\n ? hasReadyExactInSwapInput(fromTokens, toToken)\n : canRefreshExactOutQuote();\n\n if (\n canRequoteAfterPreviewBack &&\n (activeMode === \"deposit\" || activeMode === \"send\")\n ) {\n setExactOutQuoteSourceModeValue(\"all\");\n }\n if (activeMode === \"deposit\" || activeMode === \"send\") {\n invalidateExactOutQuoteForRefresh();\n } else {\n clearPendingSwapIntent(true, {\n keepQuoteRefreshing: canRequoteAfterPreviewBack,\n });\n }\n if (canRequoteAfterPreviewBack && activeMode === \"swap\") {\n setQuoteRefreshing(true);\n setTxError(null);\n setSwapQuoteIssue(null);\n }\n setSwapStep(\"idle\");\n return;\n }\n if (swapStep === \"progress\") {\n return;\n } // can't go back during tx\n setSwapStep(\"idle\");\n };\n\n const handleSwapAmountChange = (val: string, panel: \"send\" | \"receive\") => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n setSwapQuoteIssue(null);\n setTxError(null);\n const nextAmount = parseFiatNumber(val);\n const receiveIssue = buildReceiveAmountIssue({ inputAmount: val });\n applyReceiveAmountIssue(receiveIssue);\n const hasSelectedSourceToken = fromTokens.some(\n (token) => token.chainId && token.contractAddress,\n );\n const shouldLoadQuote = Boolean(\n !receiveIssue &&\n nexusSDK &&\n nextAmount?.gt(0) &&\n toToken &&\n hasSelectedSourceToken,\n );\n if (!receiveIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n }\n setAmount(val);\n if (panel === \"receive\") {\n setFromTokens((prev) =>\n prev.map((token) => ({ ...token, userAmount: \"\" })),\n );\n }\n // Nexus Widget swaps are exact-in only. Exact-out is reserved for Deposit and Send.\n if (swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n };\n\n const handleSwapTokensUpdate = (tokens: SwapTokenOption[]) => {\n setSwapQuoteIssue(null);\n setTxError(null);\n applyReceiveAmountIssue(buildReceiveAmountIssue({ sourceTokens: tokens }));\n setFromTokens(tokens);\n };\n\n const handleDepositAmountChange = (val: string) => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n resetExactOutSourcesToAuto();\n maxPercentRunRef.current += 1;\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setSwapQuoteIssue(null);\n const nextAmount = parseFiatNumber(val);\n const receiveIssue = buildReceiveAmountIssue({ inputAmount: val });\n applyReceiveAmountIssue(receiveIssue);\n const shouldLoadQuote = Boolean(\n !receiveIssue &&\n nexusSDK &&\n nextAmount?.gt(0) &&\n toToken &&\n selectedOpportunity,\n );\n if (!receiveIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n } else {\n clearSelectedSources();\n }\n setAmount(val);\n };\n\n const handleSendAmountChange = (val: string) => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n resetExactOutSourcesToAuto();\n maxPercentRunRef.current += 1;\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setSwapQuoteIssue(null);\n setSwapType(\"exactOut\");\n const nextAmount = parseFiatNumber(val);\n const receiveIssue = buildReceiveAmountIssue({\n inputAmount: val,\n type: \"exactOut\",\n });\n applyReceiveAmountIssue(receiveIssue);\n const shouldLoadQuote = Boolean(\n !receiveIssue && nexusSDK && nextAmount?.gt(0) && toToken,\n );\n if (!receiveIssue) {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n }\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n } else {\n clearSelectedSources();\n }\n setAmount(val);\n };\n\n const handleDepositAmountModeToggle = () => {\n if (isConfiguredAmountFixed) return;\n syncingIntentSourcesRef.current = false;\n resetExactOutSourcesToAuto();\n const rate = getDepositTokenUsdRate();\n const parsedAmount = parseFiatNumber(amount) ?? new Decimal(0);\n if (parsedAmount.gt(0) && rate.gt(0)) {\n const converted =\n depositAmountMode === \"token\"\n ? parsedAmount.mul(rate).toDecimalPlaces(2)\n : parsedAmount\n .div(rate)\n .toDecimalPlaces(\n getCappedTokenDisplayDecimals(toToken?.decimals),\n Decimal.ROUND_DOWN,\n );\n setAmount(converted.toFixed());\n }\n clearPendingSwapIntent();\n setDepositAmountMode((current) => (current === \"token\" ? \"usd\" : \"token\"));\n };\n\n const handleDepositPercentSelect = async (pct: number) => {\n if (isConfiguredAmountFixed) return;\n if (!toToken) return;\n\n syncingIntentSourcesRef.current = false;\n setTxError(null);\n setSwapQuoteIssue(null);\n const runId = ++maxPercentRunRef.current;\n lastInputMethodRef.current =\n pct === 20\n ? \"percent_20\"\n : pct === 25\n ? \"percent_25\"\n : pct === 50\n ? \"percent_50\"\n : pct === 75\n ? \"percent_75\"\n : \"percent_max\";\n\n if (pct !== 100) {\n const nextAmount = getExactOutPercentAmountFromBalance(\n toToken,\n pct,\n depositAmountMode === \"usd\",\n );\n\n if (nextAmount) {\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setDepositAmountMode(nextAmount.mode);\n handleDepositAmountChange(nextAmount.amount);\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setTxError(\"Unable to calculate this percentage for the deposit asset.\");\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n const maxAmount = getExactOutPercentAmountFromBalance(\n toToken,\n 100,\n depositAmountMode === \"usd\",\n );\n if (runId !== maxPercentRunRef.current) return;\n if (!maxAmount) {\n setTxError(\"No depositable amount is available for this deposit.\");\n return;\n }\n\n setDepositAmountMode(maxAmount.mode);\n handleDepositAmountChange(maxAmount.amount);\n };\n\n const handleSendPercentSelect = async (pct: number) => {\n if (isConfiguredAmountFixed) return;\n if (!toToken) return;\n\n syncingIntentSourcesRef.current = false;\n setTxError(null);\n setSwapQuoteIssue(null);\n const runId = ++maxPercentRunRef.current;\n lastInputMethodRef.current =\n pct === 20\n ? \"percent_20\"\n : pct === 25\n ? \"percent_25\"\n : pct === 50\n ? \"percent_50\"\n : pct === 75\n ? \"percent_75\"\n : \"percent_max\";\n\n if (pct !== 100) {\n const nextAmount = getExactOutPercentAmountFromBalance(\n toToken,\n pct,\n false,\n );\n\n if (nextAmount) {\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleSendAmountChange(nextAmount.amount);\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setTxError(\"Unable to calculate this percentage for the send asset.\");\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n const maxAmount = getExactOutPercentAmountFromBalance(toToken, 100, false);\n if (runId !== maxPercentRunRef.current) return;\n if (!maxAmount) {\n setTxError(\"No transferable amount is available for this asset.\");\n return;\n }\n\n handleSendAmountChange(maxAmount.amount);\n };\n\n // ---------------------------------------------------------------------------\n // Render\n // ---------------------------------------------------------------------------\n const insufficientSourceIssue =\n swapQuoteIssue?.type === \"insufficientSources\" ? swapQuoteIssue : null;\n const configuredAmountIssue = buildConfiguredAmountIssue();\n const blockingQuoteIssue =\n insufficientSourceIssue ?? receiveAmountIssue ?? configuredAmountIssue;\n const isExactOutPaymentFlow =\n activeMode === \"deposit\" || activeMode === \"send\";\n const hasCurrentRunnableIntent = hasCurrentQuoteIntent;\n const hasIntentSources = Boolean((intentData?.sources ?? []).length > 0);\n const hasCurrentExactOutPaymentIntent =\n hasCurrentRunnableIntent &&\n (hasIntentSources ||\n (isExactOutPaymentFlow && Boolean(intentData?.destination)));\n const isExactOutRouteLoading =\n isExactOutPaymentFlow &&\n swapStep === \"idle\" &&\n swapType === \"exactOut\" &&\n Boolean(\n toToken && (receiveMaxCalculating || (amount && Number(amount) > 0)),\n ) &&\n !blockingQuoteIssue &&\n !hasCurrentExactOutPaymentIntent &&\n (quoteRefreshing || intentLoading || receiveMaxCalculating);\n const isQuoteUnavailableForAutoSourceFlow =\n isExactOutPaymentFlow &&\n Boolean(hasPositiveDecimalInput(amount) && toToken) &&\n !quoteRefreshing &&\n !receiveMaxCalculating &&\n !intentLoading &&\n !blockingQuoteIssue &&\n !hasCurrentExactOutPaymentIntent;\n const hasPositiveRootAmount = hasPositiveDecimalInput(amount);\n const hasReadySwapQuoteInput = hasReadyExactInSwapInput(fromTokens, toToken);\n const needsWalletConnection = !ownerAddress || !nexusSDK;\n const isExactOutPaymentQuotePending =\n isExactOutPaymentFlow && (quoteRefreshing || intentLoading);\n const walletConnectBusy =\n walletActionPending ||\n nexusLoading ||\n isWalletConnectPending ||\n walletStatus === \"connecting\";\n const hasConnectWalletHandler = Boolean(\n onConnectClick || onConnectWallet || connectors.length > 0,\n );\n const walletCtaLabel = hasConnectWalletHandler\n ? walletConnectBusy\n ? \"Connecting...\"\n : \"Connect Wallet\"\n : \"Connect your wallet to proceed\";\n const isSwapCtaDisabled = needsWalletConnection\n ? !hasConnectWalletHandler || walletConnectBusy\n : !hasReadySwapQuoteInput ||\n receiveMaxCalculating ||\n quoteRefreshing ||\n Boolean(blockingQuoteIssue);\n const isDepositCtaDisabled = needsWalletConnection\n ? !hasConnectWalletHandler || walletConnectBusy\n : !hasPositiveRootAmount ||\n !toToken ||\n receiveMaxCalculating ||\n isExactOutPaymentQuotePending ||\n (!hasCurrentExactOutPaymentIntent &&\n isQuoteUnavailableForAutoSourceFlow) ||\n Boolean(blockingQuoteIssue);\n const sendNeedsRecipient = activeMode === \"send\" && !recipientAddress;\n const isSendCtaDisabled = needsWalletConnection\n ? !hasConnectWalletHandler || walletConnectBusy\n : !hasPositiveRootAmount ||\n !toToken ||\n hasSameOwnerSendRecipient ||\n receiveMaxCalculating ||\n (!sendNeedsRecipient &&\n (isExactOutPaymentQuotePending ||\n (!hasCurrentExactOutPaymentIntent &&\n isQuoteUnavailableForAutoSourceFlow))) ||\n Boolean(blockingQuoteIssue);\n const quoteCtaLabel = (fallback: string) => {\n if (needsWalletConnection) return walletCtaLabel;\n if (insufficientSourceIssue) return \"Insufficient balance\";\n if (receiveAmountIssue) return receiveAmountIssue.ctaLabel;\n if (configuredAmountIssue) return configuredAmountIssue.ctaLabel;\n if (receiveMaxCalculating) return \"Calculating...\";\n if (\n isExactOutPaymentQuotePending ||\n (!hasCurrentExactOutPaymentIntent && (quoteRefreshing || intentLoading))\n ) {\n return \"Fetching quotes...\";\n }\n if (isQuoteUnavailableForAutoSourceFlow) return \"Quote unavailable\";\n if (!hasPositiveRootAmount) return \"Enter amount\";\n return fallback;\n };\n const sendCtaLabel = (() => {\n if (needsWalletConnection) return walletCtaLabel;\n if (insufficientSourceIssue) return \"Insufficient balance\";\n if (receiveAmountIssue) return receiveAmountIssue.ctaLabel;\n if (configuredAmountIssue) return configuredAmountIssue.ctaLabel;\n if (!hasPositiveRootAmount) return \"Enter amount\";\n if (!toToken) return \"Select token\";\n if (hasSameOwnerSendRecipient) return \"Change recipient\";\n if (sendNeedsRecipient) return \"Add recipient\";\n return quoteCtaLabel(\"Review send\");\n })();\n const previewIntentSourceUsdNumber = (intentData?.sources ?? []).reduce(\n (sum, source) =>\n sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0),\n );\n const previewSourceUsdNumber = previewIntentSourceUsdNumber.gt(0)\n ? previewIntentSourceUsdNumber\n : fromTokens.length > 0\n ? fromTokens.reduce(\n (sum, token) =>\n sum.plus(\n getTokenUsdValue(\n token,\n swapType === \"exactIn\" && fromTokens.length === 1\n ? amount\n : undefined,\n ),\n ),\n new Decimal(0),\n )\n : undefined;\n const previewExactOutDestinationAmount =\n activeMode === \"deposit\"\n ? depositTokenAmountForQuote\n : activeMode === \"send\"\n ? parseFiatNumber(amount)\n : undefined;\n const previewExactOutDestinationUsdNumber =\n activeMode === \"deposit\"\n ? depositUsdDecimal\n : activeMode === \"send\" && amount && toToken\n ? getTokenUsdValue(\n {\n ...toToken,\n userAmount: amount,\n userAmountMode: \"token\",\n },\n amount,\n )\n : undefined;\n const previewDestinationUsdNumber =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n previewExactOutDestinationUsdNumber?.gt(0)\n ? previewExactOutDestinationUsdNumber\n : parseFiatNumber((intentData?.destination as any)?.value);\n const previewDestinationAmount =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n previewExactOutDestinationAmount?.gt(0)\n ? previewExactOutDestinationAmount\n .toDecimalPlaces(toToken?.decimals ?? 18, Decimal.ROUND_DOWN)\n .toFixed()\n : intentToAmount;\n const previewFromAmountUsd =\n previewSourceUsdNumber && previewSourceUsdNumber.gt(0)\n ? previewSourceUsdNumber.toDecimalPlaces(6).toFixed()\n : undefined;\n const previewToAmountUsd =\n previewDestinationUsdNumber && previewDestinationUsdNumber.gt(0)\n ? previewDestinationUsdNumber.toDecimalPlaces(6).toFixed()\n : undefined;\n const previewDestinationGasFeeUsd = (() => {\n const value = getIntentDestinationGasUsdValue(intentData);\n return value && value.gt(0)\n ? value.toDecimalPlaces(6, Decimal.ROUND_DOWN).toFixed()\n : undefined;\n })();\n const predictiveExactInQuote =\n predictiveQuote?.mode === \"exactIn\" &&\n predictiveQuote.key === getPredictiveQuoteCacheKey(\"swap\", \"exactIn\")\n ? predictiveQuote\n : null;\n const predictiveExactOutQuote =\n predictiveQuote?.mode === \"exactOut\" &&\n predictiveQuote.key === getPredictiveQuoteCacheKey(activeMode, \"exactOut\")\n ? predictiveQuote\n : null;\n const shouldUseCurrentExactOutIntentSources =\n isExactOutPaymentFlow && hasCurrentQuoteIntent && hasIntentSources;\n const currentExactOutIntentSourceTokens =\n shouldUseCurrentExactOutIntentSources\n ? sortSwapTokensByUsdDesc(\n (intentData?.sources ?? [])\n .map(buildIntentSourceToken)\n .filter(hasPositiveSourceSpend),\n )\n : [];\n const resolvedToToken =\n toToken ??\n (activeMode === \"deposit\" && selectedOpportunity\n ? toTokenFromOpportunity(selectedOpportunity)\n : undefined);\n const enrichedResolvedToToken = enrichTokenOptionLogo(resolvedToToken);\n const toTokenWithFetchedBalance =\n enrichedResolvedToToken && destinationBalance\n ? { ...enrichedResolvedToToken, balance: destinationBalance }\n : enrichedResolvedToToken;\n const idleReceiveQuoteAmount =\n activeMode === \"swap\" && swapType === \"exactIn\"\n ? (intentToAmount ?? predictiveExactInQuote?.toAmount)\n : undefined;\n const idleReceiveQuoteUsd =\n activeMode === \"swap\" && swapType === \"exactIn\"\n ? (previewToAmountUsd ?? predictiveExactInQuote?.toUsd)\n : previewToAmountUsd;\n const exactOutDestinationCoverage = getExactOutDestinationBalanceCoverage({\n requestedAmount: previewExactOutDestinationAmount,\n requestedUsd: previewExactOutDestinationUsdNumber,\n producedAmount: hasIntentSources\n ? parseFiatNumber(intentData?.destination?.amount)\n : undefined,\n producedUsd: hasIntentSources\n ? parseFiatNumber(intentData?.destination?.value)\n : undefined,\n token: toTokenWithFetchedBalance,\n });\n const destinationBalanceDisplayToken = buildDestinationBalanceDisplayToken(\n exactOutDestinationCoverage,\n toTokenWithFetchedBalance,\n );\n const shouldShowPredictiveExactOutDisplay =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (quoteRefreshing || intentLoading) &&\n !hasIntentSources &&\n Boolean(\n predictiveExactOutQuote &&\n ((predictiveExactOutQuote.sources?.length ?? 0) > 0 ||\n destinationBalanceDisplayToken),\n );\n const baseDisplayFromTokens = shouldUseCurrentExactOutIntentSources\n ? currentExactOutIntentSourceTokens\n : shouldShowPredictiveExactOutDisplay\n ? (predictiveExactOutQuote?.sources ?? fromTokens)\n : fromTokens;\n const displayFromTokens = (() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") {\n return baseDisplayFromTokens;\n }\n if (!destinationBalanceDisplayToken) {\n return mergeDisplaySourceTokens(\n sortDisplaySourcesByBalanceUsdDesc(baseDisplayFromTokens),\n );\n }\n\n const destinationKey = getTokenSelectionKey(destinationBalanceDisplayToken);\n let replacedEmptyDestinationToken = false;\n const tokens = baseDisplayFromTokens.map((token) => {\n const isDestinationToken = getTokenSelectionKey(token) === destinationKey;\n if (\n isDestinationToken &&\n !hasPositiveDecimalInput(token.userAmount) &&\n !hasPositiveDecimalInput(token.userAmountUsd)\n ) {\n replacedEmptyDestinationToken = true;\n return destinationBalanceDisplayToken;\n }\n return token;\n });\n\n const displayTokens = replacedEmptyDestinationToken\n ? tokens\n : [...tokens, destinationBalanceDisplayToken];\n return mergeDisplaySourceTokens(\n sortDisplaySourcesByBalanceUsdDesc(displayTokens),\n );\n })();\n const displayExactOutRouteLoading =\n isExactOutRouteLoading && !shouldShowPredictiveExactOutDisplay;\n const isSourcePickerRefreshDisabled =\n quoteRefreshing || intentLoading || previewQuoteRefreshing;\n const isIntentSourceUiActionable =\n activeMode === \"deposit\"\n ? !isDepositCtaDisabled\n : activeMode === \"send\"\n ? !isSendCtaDisabled\n : !isSwapCtaDisabled;\n\n useEffect(() => {\n if (!intentFetchTiming?.endTimeMs) {\n return;\n }\n if (latestIntentFetchRunIdRef.current !== intentFetchTiming.runId) {\n return;\n }\n if (intentUiRenderLoggedRunIdRef.current === intentFetchTiming.runId) {\n return;\n }\n const hasRenderedSources =\n displayFromTokens.length > 0 ||\n hasIntentSources ||\n Boolean(intentData?.destination);\n if (!hasRenderedSources) return;\n if (\n isSwapBalancePending ||\n isSourcePickerRefreshDisabled ||\n quoteRefreshing ||\n intentLoading ||\n previewQuoteRefreshing ||\n receiveMaxCalculating ||\n displayExactOutRouteLoading ||\n !isIntentSourceUiActionable\n ) {\n return;\n }\n\n const renderTimeMs = Date.now();\n const renderDurationMs = renderTimeMs - intentFetchTiming.startTimeMs;\n const fetchDurationMs =\n intentFetchTiming.fetchDurationMs ??\n intentFetchTiming.endTimeMs - intentFetchTiming.startTimeMs;\n const differenceMs = renderDurationMs - fetchDurationMs;\n\n console.log(\"[NexusWidget] intent sources UI render complete\", {\n runId: intentFetchTiming.runId,\n walletAddress: intentFetchTiming.walletAddress,\n operation: intentFetchTiming.operation,\n quoteInputKey: intentFetchTiming.quoteInputKey,\n sourceCount: displayFromTokens.length,\n dateNow: renderTimeMs,\n date: new Date(renderTimeMs),\n totalRenderTimeSeconds: renderDurationMs / 1000,\n elapsedMs: renderDurationMs,\n });\n console.log(\"[NexusWidget] intent render/fetch difference\", {\n runId: intentFetchTiming.runId,\n walletAddress: intentFetchTiming.walletAddress,\n operation: intentFetchTiming.operation,\n quoteInputKey: intentFetchTiming.quoteInputKey,\n differenceMs,\n dateNow: renderTimeMs,\n date: new Date(renderTimeMs),\n });\n intentUiRenderLoggedRunIdRef.current = intentFetchTiming.runId;\n }, [\n displayExactOutRouteLoading,\n displayFromTokens.length,\n hasIntentSources,\n intentLoading,\n intentData?.destination,\n intentFetchTiming,\n isDepositCtaDisabled,\n isIntentSourceUiActionable,\n isSendCtaDisabled,\n isSourcePickerRefreshDisabled,\n isSwapCtaDisabled,\n isSwapBalancePending,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n ]);\n\n const totalSwapBalanceUsd = getActiveTotalBalanceUsd()\n .toDecimalPlaces(2)\n .toFixed();\n const requestedOutputUsd =\n amount && toToken\n ? getTokenUsdValue(\n {\n ...toToken,\n userAmount: amount,\n userAmountMode: \"token\",\n },\n amount,\n )\n : new Decimal(0);\n const sendAmountUsd = requestedOutputUsd.toNumber();\n const exactOutRequiredUsdAmount = (() => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return undefined;\n\n const issueMissingUsd = parseFiatNumber(\n insufficientSourceIssue?.missingUsd,\n );\n if (issueMissingUsd && issueMissingUsd.gt(0)) {\n return getExactOutAvailableSourceUsd().plus(issueMissingUsd);\n }\n\n const requiredFundingUsd = getExactOutRequiredFundingUsd();\n if (requiredFundingUsd?.gt(0)) return requiredFundingUsd;\n\n if (requestedOutputUsd.gt(0)) return requestedOutputUsd;\n\n return undefined;\n })();\n const exactOutRequiredUsdDisplay = exactOutRequiredUsdAmount\n ?.toDecimalPlaces(2)\n .toFixed();\n const assetSelectorRequiredUsdDisplay =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (swapStep === \"choose-swap-asset\" ||\n closingDrawerStep === \"choose-swap-asset\")\n ? (sourceSelectionRequiredUsdDisplay ?? exactOutRequiredUsdDisplay)\n : exactOutRequiredUsdDisplay;\n const captureSourceSelectionRequiredUsd = () => {\n if (activeMode !== \"deposit\" && activeMode !== \"send\") return;\n setSourceSelectionRequiredUsdDisplay(exactOutRequiredUsdDisplay);\n };\n const isIdleSwapQuoteLoading =\n activeMode === \"swap\" &&\n swapStep === \"idle\" &&\n (quoteRefreshing || intentLoading);\n const isReceiveAmountLoading =\n receiveMaxCalculating ||\n (isIdleSwapQuoteLoading &&\n swapType === \"exactIn\" &&\n !idleReceiveQuoteAmount);\n const isReceiveUsdLoading =\n receiveMaxCalculating ||\n (isIdleSwapQuoteLoading && swapType === \"exactIn\" && !idleReceiveQuoteUsd);\n const hasQuoteRefreshCountdown =\n (activeMode === \"swap\" ||\n activeMode === \"deposit\" ||\n activeMode === \"send\") &&\n hasCurrentQuoteIntent &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n const isRecipientDrawerClosing = closingDrawerStep === \"enter-recipient\";\n const isSwapAssetDrawerClosing = closingDrawerStep === \"choose-swap-asset\";\n const isReceiveAssetDrawerClosing =\n closingDrawerStep === \"choose-receive-asset\";\n const isReceiveAssetPickerLocked =\n activeMode === \"deposit\"\n ? configuredDepositOptions.length <= 1\n : (config.allowedDestinationPairs?.length ?? 0) === 1;\n const hideDestinationTokenDropdownIcon =\n activeMode === \"deposit\"\n ? configuredDepositOptions.length === 1\n : (config.allowedDestinationPairs?.length ?? 0) === 1;\n const isTokenAssetDrawerActive =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n closingDrawerStep === \"choose-swap-asset\" ||\n closingDrawerStep === \"choose-receive-asset\";\n const configuredReceiveTokenCount =\n activeMode === \"deposit\" || activeMode === \"send\"\n ? Math.max(\n configuredDestinationTokenOptions.length,\n config.allowedDestinationPairs?.length ?? 0,\n )\n : 0;\n const isConfiguredReceiveAssetDrawerActive =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (swapStep === \"choose-receive-asset\" ||\n closingDrawerStep === \"choose-receive-asset\") &&\n Boolean(config.allowedDestinationPairs?.length);\n const tokenAssetDrawerMinRootContentHeight =\n isConfiguredReceiveAssetDrawerActive\n ? getConfiguredReceiveSelectorRootHeight(configuredReceiveTokenCount)\n : isTokenAssetDrawerActive\n ? TOKEN_SELECTOR_MIN_ROOT_CONTENT_HEIGHT\n : 0;\n const isDrawerOverlayActive =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n swapStep === \"enter-recipient\" ||\n closingDrawerStep !== null;\n const isPreviewIntentScreen = swapStep === \"preview-intent\";\n const displayedRootContentHeight =\n hasMeasuredRootContent && rootContentHeight\n ? Math.max(rootContentHeight, tokenAssetDrawerMinRootContentHeight)\n : null;\n\n const widgetContent = (\n {\n const transitions = [];\n if (hasMeasuredRootContent && shouldAnimateRootHeight) {\n transitions.push(`height ${ROOT_HEIGHT_TRANSITION_MS}ms ease-out`);\n }\n return transitions.join(\", \");\n })(),\n willChange: \"height\",\n width: \"450px\",\n maxWidth: \"100%\",\n minWidth: \"320px\",\n WebkitFontSmoothing: \"antialiased\",\n MozOsxFontSmoothing: \"grayscale\",\n }}\n >\n \n \n \n {canGoBack && (\n \n \n \n )}\n \n {getTitle()}\n \n\n {/* Sub-screen asset counts */}\n {!isTitleCentered() &&\n activeMode === \"swap\" &&\n swapStep === \"choose-swap-asset\" &&\n swapType === \"exactIn\" && (\n \n {fromTokens.length} asset(s) selected\n \n )}\n \n\n {/* Right side icons */}\n \n {hasQuoteRefreshCountdown && (\n \n )}\n {showHistoryButton && (\n setSwapStep(\"history\")}\n style={{\n alignItems: \"center\",\n backgroundColor: theme.primitives.iconButton.backgroundColor,\n borderColor: theme.primitives.iconButton.borderColor,\n borderRadius: theme.radius.iconButton,\n borderStyle: \"solid\",\n borderWidth: \"1px\",\n boxShadow: theme.primitives.iconButton.boxShadow,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n height: \"28px\",\n justifyContent: \"center\",\n width: \"28px\",\n cursor: \"pointer\",\n padding: 0,\n }}\n >\n \n \n \n \n \n \n )}\n {showCloseButton && (\n \n \n \n \n \n )}\n \n \n\n {/* ------------------------------------------------------------------ */}\n {/* Main content area */}\n {/* ------------------------------------------------------------------ */}\n \n {/* =============================================================== */}\n {/* SHARED SUB-SCREENS (non-drawer panels) */}\n {/* =============================================================== */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep !== \"idle\" &&\n swapStep !== \"choose-swap-asset\" &&\n swapStep !== \"choose-receive-asset\" &&\n swapStep !== \"enter-recipient\" && (\n <>\n {/* Panel: preview. */}\n {swapStep === \"preview-intent\" && (\n \n {\n clearPendingSwapIntent();\n setSwapStep(\"idle\");\n }}\n onTransitionChange={setIsPreviewTransitioning}\n opportunity={selectedOpportunity}\n recipientAddress={transferRecipientAddress}\n steps={steps}\n supportedTokenAssets={supportedChainsAndTokens}\n swapBalances={swapBalance}\n swapType={swapType}\n toAmount={previewDestinationAmount}\n toAmountTokens={\n previewDestinationAmount\n ? `${previewDestinationAmount}`\n : undefined\n }\n toAmountUsd={previewToAmountUsd}\n toToken={toTokenWithFetchedBalance}\n totalFeeUsd={intentFeeUsd}\n />\n \n )}\n\n {swapStep === \"progress\" && (\n \n )}\n\n {(swapStep === \"success\" || swapStep === \"failed\") &&\n currentSwapEntry && (\n
\n \n
\n )}\n \n )}\n\n {/* =============================================================== */}\n {/* HISTORY SCREEN */}\n {/* =============================================================== */}\n {swapStep === \"history\" && (\n \n )}\n\n {/* =============================================================== */}\n {/* SWAP IDLE SCREEN */}\n {/* =============================================================== */}\n {activeMode === \"swap\" &&\n [\n \"idle\",\n \"choose-swap-asset\",\n \"choose-receive-asset\",\n \"enter-recipient\",\n ].includes(swapStep) && (\n <>\n {\n handleSwapAmountChange(val, panel);\n }}\n onOpenDestPicker={() =>\n openDrawerStep(\"choose-receive-asset\")\n }\n onOpenRecipientPicker={handleOpenRecipientEditor}\n onOpenSourcePicker={(index) => {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n setEditingAssetIndex(index ?? null);\n beginSourcePickerEdit();\n openDrawerStep(\"choose-swap-asset\");\n }}\n onUpdateTokens={handleSwapTokensUpdate}\n receiveQuoteAmount={\n swapType === \"exactIn\" ? idleReceiveQuoteAmount : undefined\n }\n receiveQuoteUsd={idleReceiveQuoteUsd}\n recipientAddress={effectiveRecipientAddress}\n sourceRouteMessage={insufficientSourceIssue?.message}\n sourceRouteStatus={\n insufficientSourceIssue\n ? \"insufficient\"\n : isExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n swapType={swapType}\n toToken={toTokenWithFetchedBalance}\n totalBalance={totalSwapBalanceUsd}\n usdValue={amount && usdValue > 0 ? usdValue.toFixed(2) : \"\"}\n />\n\n {receiveAmountIssue && (\n \n )}\n\n {configuredAmountIssue && (\n \n )}\n\n {txError && !blockingQuoteIssue && (\n \n )}\n\n {/* CTA Button */}\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n void handleEnterPreview();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: blockingQuoteIssue\n ? \"#FCEEED\"\n : isSwapCtaDisabled\n ? theme.colors.surfaceCool\n : primaryButtonBackground,\n border: blockingQuoteIssue ? \"1px solid #F7C4C1\" : \"none\",\n borderRadius: theme.radius.primaryButton,\n boxShadow:\n blockingQuoteIssue || isSwapCtaDisabled\n ? \"none\"\n : theme.shadows.primaryButton,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"7px\",\n height: \"42px\",\n justifyContent: \"center\",\n marginTop: \"8px\",\n paddingInline: \"16px\",\n cursor: isSwapCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {blockingQuoteIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n quoteRefreshing ||\n receiveMaxCalculating ? (\n \n ) : null}\n \n {needsWalletConnection\n ? walletCtaLabel\n : fromTokens.length === 0\n ? \"Add Assets to Bridge\"\n : quoteCtaLabel(\"Review swap\")}\n \n \n \n \n )}\n\n {/* =============================================================== */}\n {/* DEPOSIT MODE LAYOUT */}\n {/* =============================================================== */}\n {activeMode === \"deposit\" &&\n [\n \"idle\",\n \"choose-swap-asset\",\n \"choose-receive-asset\",\n \"enter-recipient\",\n ].includes(swapStep) && (\n <>\n {swapStep === \"idle\" && depositFundingStep === \"method\" && (\n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n setDepositFundingStep(\"onramp\");\n }}\n onSelectWallet={() => setDepositFundingStep(\"wallet\")}\n primaryButtonForeground={primaryButtonForeground}\n totalBalance={totalSwapBalanceUsd}\n />\n )}\n\n {swapStep === \"idle\" && depositFundingStep === \"onramp\" && (\n {\n setTxError(message);\n onError?.(message);\n }}\n onSelectDestinationToken={(token) => {\n const tokenChanged = !isSameTokenSelection(\n toToken,\n token,\n );\n if (tokenChanged) {\n onReceiveAssetChange?.({\n chainId: token.chainId,\n chainName: token.chainName,\n contractAddress: token.contractAddress,\n symbol: token.symbol,\n });\n }\n const nextDeposit = getDepositForTokenSelection(\n configuredDepositOptions,\n token,\n );\n if (nextDeposit) {\n setSelectedOpportunity(nextDeposit);\n }\n setToToken(token);\n }}\n onSessionStateChange={setDepositOnrampSessionState}\n nexusSDK={nexusSDK}\n ownerAddress={ownerAddress}\n opportunity={selectedOpportunity}\n primaryButtonForeground={primaryButtonForeground}\n publicClient={publicClient}\n toToken={toTokenWithFetchedBalance}\n />\n )}\n\n {selectedOpportunity && depositFundingStep === \"wallet\" && (\n <>\n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n captureSourceSelectionRequiredUsd();\n beginSourcePickerEdit();\n openDrawerStep(\"choose-swap-asset\");\n }}\n onOpenTokenPicker={() =>\n openDrawerStep(\"choose-receive-asset\")\n }\n onSetPercent={handleDepositPercentSelect}\n routeMessage={insufficientSourceIssue?.message}\n routeStatus={\n insufficientSourceIssue\n ? \"insufficient\"\n : displayExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n showAutoBadge={!sourceSelectionTouched}\n isTokenPickerDisabled={isReceiveAssetPickerLocked}\n hideDestinationTokenDropdownIcon={\n hideDestinationTokenDropdownIcon\n }\n tokenValue={depositTokenDisplay}\n toToken={toTokenWithFetchedBalance}\n totalBalance={totalSwapBalanceUsd}\n usdValue={depositUsdDisplay}\n />\n\n {receiveAmountIssue && (\n \n )}\n\n {configuredAmountIssue && (\n \n )}\n\n {txError && !blockingQuoteIssue && (\n \n )}\n\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n void handleEnterPreview();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: blockingQuoteIssue\n ? \"#FCEEED\"\n : isDepositCtaDisabled\n ? theme.colors.surfaceCool\n : primaryButtonBackground,\n border: blockingQuoteIssue\n ? \"1px solid #F7C4C1\"\n : \"none\",\n borderRadius: blockingQuoteIssue\n ? \"4px\"\n : theme.radius.primaryButton,\n boxShadow:\n blockingQuoteIssue || isDepositCtaDisabled\n ? \"none\"\n : theme.shadows.primaryButton,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"7px\",\n height: \"40px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n cursor: isDepositCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {blockingQuoteIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n isExactOutPaymentQuotePending ||\n receiveMaxCalculating ? (\n \n ) : null}\n \n {quoteCtaLabel(\"Review deposit\")}\n \n \n \n \n )}\n \n )}\n\n {/* =============================================================== */}\n {/* SEND MODE — recipient first, then amount, then asset */}\n {/* =============================================================== */}\n {activeMode === \"send\" &&\n [\n \"idle\",\n \"choose-swap-asset\",\n \"choose-receive-asset\",\n \"enter-recipient\",\n ].includes(swapStep) && (\n <>\n \n openDrawerStep(\"choose-receive-asset\")\n }\n isRecipientLocked={isConfiguredRecipientLocked}\n onOpenRecipientPicker={\n isConfiguredRecipientLocked\n ? () => undefined\n : handleOpenRecipientEditor\n }\n onOpenSourcePicker={() => {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n setEditingAssetIndex(null);\n captureSourceSelectionRequiredUsd();\n beginSourcePickerEdit();\n openDrawerStep(\"choose-swap-asset\");\n }}\n onSetPercent={handleSendPercentSelect}\n recipientAddress={recipientAddress || \"\"}\n routeMessage={insufficientSourceIssue?.message}\n routeStatus={\n insufficientSourceIssue\n ? \"insufficient\"\n : displayExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n showAutoBadge={!sourceSelectionTouched}\n toToken={toTokenWithFetchedBalance}\n totalBalance={totalSwapBalanceUsd}\n usdValue={\n amount && sendAmountUsd > 0 ? sendAmountUsd.toFixed(2) : \"\"\n }\n />\n\n {receiveAmountIssue && (\n \n )}\n\n {configuredAmountIssue && (\n \n )}\n\n {txError && !blockingQuoteIssue && (\n \n )}\n\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n if (sendNeedsRecipient) {\n handleOpenRecipientEditor();\n return;\n }\n void handleEnterPreview();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: blockingQuoteIssue\n ? \"#FCEEED\"\n : isSendCtaDisabled\n ? theme.colors.surfaceCool\n : primaryButtonBackground,\n border: blockingQuoteIssue ? \"1px solid #F7C4C1\" : \"none\",\n borderRadius: blockingQuoteIssue\n ? \"4px\"\n : theme.radius.primaryButton,\n boxShadow:\n blockingQuoteIssue || isSendCtaDisabled\n ? \"none\"\n : theme.shadows.primaryButton,\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"7px\",\n height: \"40px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n cursor: isSendCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {blockingQuoteIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n (!sendNeedsRecipient &&\n (isExactOutPaymentQuotePending ||\n receiveMaxCalculating)) ? (\n \n ) : null}\n \n {sendCtaLabel}\n \n \n \n \n )}\n \n \n\n {/* ================================================================== */}\n {/* DRAWER PANELS — rendered as direct children of root widget */}\n {/* so they overlay the main page as bottom drawers */}\n {/* ================================================================== */}\n\n {/* Drawer: enter-recipient */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep === \"enter-recipient\" && (\n \n {\n setTxError(null);\n closeDrawerToIdle();\n }}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n backgroundColor: \"rgba(0,0,0,0.35)\",\n pointerEvents: \"auto\",\n opacity: isRecipientDrawerClosing ? 0 : 1,\n transition: `opacity ${DRAWER_CLOSE_MS}ms ease`,\n }}\n />\n \n \n \n \n \n {\n setTxError(null);\n closeDrawerToIdle();\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: theme.colors.surface,\n border: `1px solid ${theme.colors.border}`,\n borderRadius: \"8px\",\n cursor: \"pointer\",\n display: \"flex\",\n flexShrink: 0,\n height: \"32px\",\n justifyContent: \"center\",\n padding: 0,\n width: \"32px\",\n }}\n type=\"button\"\n >\n \n \n \n Recipient\n \n \n \n \n \n Wallet Address\n \n {activeMode === \"swap\" && defaultRecipientAddress && (\n \n Reset to default\n \n )}\n \n {\n setRecipientAddress(next);\n if (txError) setTxError(null);\n }}\n onClear={() => setRecipientAddress(\"\")}\n placeholder=\"Wallet address\"\n value={recipientAddress}\n />\n {txError && (\n \n {txError}\n \n )}\n {activeMode === \"send\" && (\n \n Recipient must be different from the connected wallet.\n \n )}\n \n Save\n \n \n \n )}\n\n {/* Drawer: choose-swap-asset */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep === \"choose-swap-asset\" && (\n \n \n \n {\n if (activeMode === \"swap\") {\n const next = [...fromTokens];\n const targetIndex =\n editingAssetIndex !== null &&\n editingAssetIndex < next.length\n ? editingAssetIndex\n : null;\n const existingToken =\n targetIndex !== null ? next[targetIndex] : undefined;\n const tokenChanged = !isSameTokenSelection(\n existingToken,\n token,\n );\n const preservedAmount = tokenChanged\n ? \"\"\n : existingToken?.userAmount ||\n (targetIndex === 0 ? amount : \"\");\n const newToken = {\n ...token,\n userAmount: preservedAmount,\n };\n\n if (targetIndex !== null) {\n next[targetIndex] = newToken;\n } else {\n next.push(newToken);\n }\n\n if (tokenChanged) {\n clearPendingSwapIntent();\n setAmount(getSourceAmountInput(next));\n }\n if (swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n setFromTokens(next);\n closeDrawerToIdle();\n } else if (\n activeMode === \"deposit\" ||\n activeMode === \"send\"\n ) {\n handleSourcePickerDraftSelectionChange([\n { ...token, userAmount: \"\" },\n ]);\n }\n }}\n onSelectionChange={\n activeMode === \"deposit\" || activeMode === \"send\"\n ? handleSourcePickerDraftSelectionChange\n : undefined\n }\n onToggle={(token) => {\n const isDepositOrSendSourcePicker =\n activeMode === \"deposit\" || activeMode === \"send\";\n const prevTokens = isDepositOrSendSourcePicker\n ? sourcePickerSelectedTokens\n : fromTokens;\n const isSameSelection = (\n a: SwapTokenOption,\n b: SwapTokenOption,\n ) => {\n if (a.isUnified || b.isUnified) {\n return Boolean(\n a.isUnified &&\n b.isUnified &&\n a.unifiedSymbol === b.unifiedSymbol,\n );\n }\n return (\n a.contractAddress.toLowerCase() ===\n b.contractAddress.toLowerCase() &&\n a.chainId === b.chainId\n );\n };\n const sourceTokens = token.sourceTokens ?? [];\n const isSameUnifiedGroup = (item: SwapTokenOption) =>\n Boolean(\n item.isUnified &&\n token.isUnified &&\n item.unifiedSymbol === token.unifiedSymbol,\n );\n const withDefaultAmount = (item: SwapTokenOption) => ({\n ...item,\n userAmount:\n activeMode === \"swap\" && prevTokens.length === 0\n ? amount\n : \"\",\n });\n const getNextTokens = () => {\n if (\n isDepositOrSendSourcePicker &&\n token.isUnified &&\n sourceTokens.length > 0\n ) {\n const hasUnifiedSelection =\n prevTokens.some(isSameUnifiedGroup);\n const areAllChildrenSelected = sourceTokens.every(\n (source) =>\n prevTokens.some((item) =>\n isSameSelection(item, source),\n ),\n );\n const withoutGroup = prevTokens.filter(\n (item) =>\n !isSameUnifiedGroup(item) &&\n !sourceTokens.some((source) =>\n isSameSelection(item, source),\n ),\n );\n\n if (hasUnifiedSelection || areAllChildrenSelected) {\n return withoutGroup;\n }\n\n return [\n ...withoutGroup,\n ...sourceTokens.map((source) =>\n withDefaultAmount(source),\n ),\n ];\n }\n\n if (isDepositOrSendSourcePicker && !token.isUnified) {\n const unifiedSelection = prevTokens.find(\n (item) =>\n item.isUnified &&\n item.sourceTokens?.some((source) =>\n isSameSelection(source, token),\n ),\n );\n\n if (unifiedSelection?.sourceTokens?.length) {\n const withoutUnified = prevTokens.filter(\n (item) => !isSameSelection(item, unifiedSelection),\n );\n return [\n ...withoutUnified,\n ...unifiedSelection.sourceTokens\n .filter((source) => !isSameSelection(source, token))\n .map((source) => withDefaultAmount(source)),\n ];\n }\n }\n\n const exists = prevTokens.find((item) =>\n isSameSelection(item, token),\n );\n if (exists) {\n return prevTokens.filter(\n (item) => !isSameSelection(item, token),\n );\n }\n const tokenSourceKeys = new Set(\n (token.sourceTokens ?? []).map(\n (source) =>\n `${source.chainId}-${source.contractAddress.toLowerCase()}`,\n ),\n );\n const next = prevTokens.filter((existing) => {\n if (\n token.isUnified &&\n tokenSourceKeys.has(\n `${existing.chainId}-${existing.contractAddress.toLowerCase()}`,\n )\n ) {\n return false;\n }\n if (\n existing.isUnified &&\n existing.sourceTokens?.some(\n (source) =>\n source.chainId === token.chainId &&\n source.contractAddress.toLowerCase() ===\n token.contractAddress.toLowerCase(),\n )\n ) {\n return false;\n }\n return true;\n });\n return [...next, withDefaultAmount(token)];\n };\n const nextTokens = getNextTokens();\n\n if (isDepositOrSendSourcePicker) {\n handleSourcePickerDraftSelectionChange(nextTokens);\n } else {\n clearPendingSwapIntent();\n setFromTokens(nextTokens);\n }\n }}\n preserveSelectedBelowMinimum={false}\n requiredUsd={assetSelectorRequiredUsdDisplay}\n selectedTokens={sourcePickerSelectedTokens}\n showBelowMinimumInline={\n activeMode === \"swap\" && swapType === \"exactIn\"\n }\n swapBalance={swapBalance}\n swapSupportedChains={swapSupportedChainsAndTokens}\n title={\n activeMode === \"deposit\" || activeMode === \"send\"\n ? \"Choose Assets to Pay with\"\n : \"Choose assets to send\"\n }\n />\n \n \n )}\n\n {/* Drawer: choose-receive-asset */}\n {(activeMode === \"swap\" ||\n activeMode === \"send\" ||\n activeMode === \"deposit\") &&\n swapStep === \"choose-receive-asset\" && (\n \n \n \n {\n const tokenChanged = !isSameTokenSelection(toToken, token);\n if (tokenChanged) {\n onReceiveAssetChange?.({\n chainId: token.chainId,\n chainName: token.chainName,\n contractAddress: token.contractAddress,\n symbol: token.symbol,\n });\n }\n if (activeMode === \"send\" || activeMode === \"deposit\") {\n setExactOutQuoteSourceModeValue(\"all\");\n if (tokenChanged) {\n clearPendingSwapIntent();\n if (!isConfiguredAmountFixed) {\n setAmount(\"\");\n }\n }\n setSwapType(\"exactOut\");\n if (activeMode === \"deposit\") {\n const nextDeposit = getDepositForTokenSelection(\n configuredDepositOptions,\n token,\n );\n if (nextDeposit) {\n setSelectedOpportunity(nextDeposit);\n }\n }\n setToToken(token);\n closeDrawerToIdle();\n return;\n }\n if (tokenChanged) {\n clearPendingSwapIntent();\n }\n if (swapType !== \"exactIn\") {\n setSwapType(\"exactIn\");\n }\n setToToken(token);\n closeDrawerToIdle();\n }}\n />\n \n \n )}\n \n );\n\n if (embed) return widgetContent;\n\n return (\n \n \n \n \n \n {widgetContent}\n \n \n );\n}\n\nexport default NexusWidget;\n", "type": "registry:component", "target": "components/nexus/nexus.tsx" }, diff --git a/registry/avail-widgets/nexus-widget/components/deposit-funding-method.tsx b/registry/avail-widgets/nexus-widget/components/deposit-funding-method.tsx index cca249bc..79d9729d 100644 --- a/registry/avail-widgets/nexus-widget/components/deposit-funding-method.tsx +++ b/registry/avail-widgets/nexus-widget/components/deposit-funding-method.tsx @@ -7,6 +7,7 @@ import { nexusWidgetTheme } from "../theme"; type FundingMethod = "wallet" | "local-currency"; interface DepositFundingMethodProps { + isBalanceLoading?: boolean; onSelectLocalCurrency: () => void; onSelectWallet: () => void; primaryButtonForeground: string; @@ -87,9 +88,36 @@ function RadioMark({ selected }: { selected: boolean }) { ); } +function AmountSkeleton({ + height = "16px", + width = "64px", +}: { + height?: string; + width?: string; +}) { + return ( + - {amount && ( + {(amount || amountLoading) && ( - ${amount} + {amountLoading ? : `$${amount}`} )} @@ -193,6 +222,7 @@ function FundingOption({ } export function DepositFundingMethod({ + isBalanceLoading = false, onSelectLocalCurrency, onSelectWallet, primaryButtonForeground, @@ -240,6 +270,7 @@ export function DepositFundingMethod({ = { + expiresAt: number; + value: T; +}; + +type OnrampBlockedRequest = { + key: string; + message: string; +}; + +type OnrampDepositExecutionState = { + amount?: string; + error?: string; + explorerUrl?: string; + skipped?: boolean; + status: "idle" | "running" | "success" | "failed"; + txHash?: Hex; +}; + +type OnrampNexusSDK = { + execute: (params: any, options?: any) => Promise; +}; + type OnrampSheet = | "currency" | "destination" @@ -124,18 +204,33 @@ interface DepositOnrampFlowProps { onError?: (message: string) => void; onSelectDestinationToken?: (token: SwapTokenOption) => void; onSessionStateChange?: (state: string | null) => void; + nexusSDK?: OnrampNexusSDK | null; ownerAddress?: string; opportunity?: NexusWidgetDepositOpportunityConfig; primaryButtonForeground: string; + publicClient?: PublicClient | null; toToken?: SwapTokenOption; } -const ONRAMP_CLIENT_HEADER = "nexus-widget"; -const ONRAMP_DEFAULT_COUNTRY = "IN"; -const ONRAMP_DEFAULT_BASE_URL = "http://localhost:8000"; -const QUOTE_REFRESH_SECONDS = 15; +const ONRAMP_CLIENT_HEADER = "nexus-widgets"; +const ONRAMP_DEFAULT_BASE_URL = "https://nexus-v2.canary.avail.so/middleware"; +const ONRAMP_RETURN_PATH = "/onramp/complete"; +const ONRAMP_IP_COUNTRY_URL = "https://api.country.is/"; +const ONRAMP_OPTIONS_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; +const ONRAMP_COUNTRY_CACHE_TTL_MS = 24 * 60 * 60 * 1000; +const ONRAMP_COUNTRY_CACHE_KEY = "nexus-widgets:onramp:country:v1"; +const ONRAMP_OPTIONS_CACHE_KEY_PREFIX = "nexus-widgets:onramp:options:v1"; +const ONRAMP_SANDBOX_FALLBACK_COUNTRY = "FR"; +const ONRAMP_PRODUCTION_FALLBACK_COUNTRY = "US"; +const QUOTE_REFRESH_SECONDS = 60; const ONRAMP_SESSION_POLL_MS = 3000; -const TEST_FIAT_CURRENCIES = ["INR", "USD", "EUR", "JPY", "GBP"] as const; +const ONRAMP_CALLBACK_MESSAGE_TYPE = "nexus-widgets:onramp:session"; +const ONRAMP_CALLBACK_SUCCESS_MESSAGE = "nexus-onramp-success"; +const ONRAMP_CALLBACK_CHANNEL = "nexus-widgets:onramp"; +const ONRAMP_CALLBACK_STORAGE_KEY = "nexus-widgets:onramp:session"; +const ONRAMP_PROGRESS_ARTWORK_URL = + "https://files.availproject.org/nexus-elements/nexus-one/progress-grid.gif"; +const ONRAMP_SHEET_EDGE_OFFSET = "-16px"; const theme = nexusWidgetTheme; const brand = "var(--foreground-brand)"; @@ -221,21 +316,272 @@ const formatUsdDisplay = (value: unknown) => { return `$${formatNumberDisplay(parsed, 2)}`; }; -const getFiatCurrencyName = (currencyCode?: string) => { - switch ((currencyCode ?? "").toUpperCase()) { - case "EUR": - return "Euro"; - case "GBP": - return "British Pound"; - case "INR": - return "Indian Rupee"; - case "JPY": - return "Japanese Yen"; - case "USD": - return "US Dollar"; - default: - return currencyCode ?? ""; +const getIntlCurrencyName = (currencyCode?: string) => { + if (!currencyCode) return ""; + try { + return ( + new Intl.DisplayNames(["en"], { type: "currency" }).of( + currencyCode.toUpperCase(), + ) ?? currencyCode + ); + } catch { + return currencyCode; + } +}; + +const getIntlCurrencySymbol = (currencyCode?: string) => { + if (!currencyCode) return undefined; + try { + return new Intl.NumberFormat("en", { + currency: currencyCode.toUpperCase(), + currencyDisplay: "narrowSymbol", + style: "currency", + }) + .formatToParts(0) + .find((part) => part.type === "currency")?.value; + } catch { + return undefined; + } +}; + +const getFiatCurrencyCode = (currency: OnrampFiatCurrency) => + (typeof currency === "string" + ? currency + : (currency.currencyCode ?? currency.code ?? "") + ).toUpperCase(); + +const getFiatCurrencyName = ( + currencyCode?: string, + currency?: OnrampFiatCurrencyOption, +) => currency?.name ?? getIntlCurrencyName(currencyCode); + +const getCountryByCode = ( + countries: OnrampCountry[] | undefined, + countryCode?: string, +) => + countries?.find( + (country) => + country.countryCode.toUpperCase() === countryCode?.toUpperCase(), + ); + +const isCountryInOptionsList = ( + options: OnrampOptionsResponse, + countryCode: string, +) => { + if (!options.countries?.length) return true; + return Boolean(getCountryByCode(options.countries, countryCode)); +}; + +const getCountryFlagUrl = ( + countries: OnrampCountry[] | undefined, + countryCode?: string, +) => { + const normalizedCountryCode = countryCode?.toUpperCase(); + if (!normalizedCountryCode) return undefined; + return ( + getCountryByCode(countries, normalizedCountryCode)?.flagUrl ?? + `https://flagsapi.com/${normalizedCountryCode}/flat/64.png` + ); +}; + +const getFiatCurrencyOptions = ( + options: OnrampOptionsResponse | null, +): OnrampFiatCurrencyOption[] => { + const selection = options?.selection; + const selectedCountryFlagUrl = getCountryFlagUrl( + options?.countries, + selection?.countryCode, + ); + const byCode = new Map(); + + for (const currency of selection?.fiatCurrencies ?? []) { + const currencyCode = getFiatCurrencyCode(currency); + if (!currencyCode || byCode.has(currencyCode)) continue; + const isDefaultFiat = + currencyCode === selection?.defaultFiat?.toUpperCase(); + byCode.set(currencyCode, { + currencyCode, + flagUrl: + typeof currency === "string" + ? isDefaultFiat + ? selectedCountryFlagUrl + : undefined + : (currency.flagUrl ?? + currency.symbolUrl ?? + (isDefaultFiat ? selectedCountryFlagUrl : undefined)), + name: + typeof currency === "string" + ? getIntlCurrencyName(currencyCode) + : (currency.name ?? getIntlCurrencyName(currencyCode)), + symbol: + typeof currency === "string" + ? getIntlCurrencySymbol(currencyCode) + : (currency.symbol ?? getIntlCurrencySymbol(currencyCode)), + symbolUrl: typeof currency === "string" ? undefined : currency.symbolUrl, + }); + } + + const defaultFiat = selection?.defaultFiat?.toUpperCase(); + if (defaultFiat && !byCode.has(defaultFiat)) { + byCode.set(defaultFiat, { + currencyCode: defaultFiat, + flagUrl: selectedCountryFlagUrl, + name: getIntlCurrencyName(defaultFiat), + symbol: getIntlCurrencySymbol(defaultFiat), + }); + } + + return Array.from(byCode.values()); +}; + +const readOnrampCache = (key: string) => { + if (typeof window === "undefined") return null; + try { + const raw = window.localStorage.getItem(key); + if (!raw) return null; + const cached = JSON.parse(raw) as OnrampCacheRecord; + if (!cached?.expiresAt || cached.expiresAt <= Date.now()) { + window.localStorage.removeItem(key); + return null; + } + return cached.value; + } catch { + return null; + } +}; + +const writeOnrampCache = (key: string, value: T, ttlMs: number) => { + if (typeof window === "undefined") return; + try { + window.localStorage.setItem( + key, + JSON.stringify({ + expiresAt: Date.now() + ttlMs, + value, + } satisfies OnrampCacheRecord), + ); + } catch { + // Cache failures should not affect the onramp flow. + } +}; + +const getOnrampOptionsCacheKey = (baseUrl: string, countryCode: string) => + `${ONRAMP_OPTIONS_CACHE_KEY_PREFIX}:${baseUrl}:${countryCode.toUpperCase()}`; + +const readCachedOnrampOptions = (baseUrl: string, countryCode: string) => + readOnrampCache( + getOnrampOptionsCacheKey(baseUrl, countryCode), + ); + +const writeCachedOnrampOptions = ( + baseUrl: string, + countryCode: string, + options: OnrampOptionsResponse, +) => { + writeOnrampCache( + getOnrampOptionsCacheKey(baseUrl, countryCode), + options, + ONRAMP_OPTIONS_CACHE_TTL_MS, + ); +}; + +const getOnrampEnvironmentValue = (key: string) => { + if (typeof process === "undefined") return ""; + return process.env?.[key]?.trim() ?? ""; +}; + +const normalizeOnrampBaseUrl = (baseUrl: string) => baseUrl.replace(/\/+$/, ""); + +const getOnrampBaseUrl = () => + normalizeOnrampBaseUrl( + getOnrampEnvironmentValue("NEXT_PUBLIC_NEXUS_ONRAMP_BASE_URL") || + ONRAMP_DEFAULT_BASE_URL, + ); + +const getOnrampReturnUrl = () => + getOnrampEnvironmentValue("NEXT_PUBLIC_NEXUS_ONRAMP_RETURN_URL") || + (typeof window !== "undefined" && window.location.origin + ? `${window.location.origin}${ONRAMP_RETURN_PATH}` + : ONRAMP_RETURN_PATH); + +const getOnrampRuntimeEnvironment = (baseUrl: string) => { + const explicitEnvironment = + getOnrampEnvironmentValue("NEXT_PUBLIC_NEXUS_ONRAMP_ENV") || + getOnrampEnvironmentValue("NEXT_PUBLIC_ONRAMP_ENV") || + getOnrampEnvironmentValue("NEXT_PUBLIC_NEXUS_ENV") || + getOnrampEnvironmentValue("NEXT_PUBLIC_VERCEL_ENV") || + getOnrampEnvironmentValue("VERCEL_ENV"); + + if (explicitEnvironment) return explicitEnvironment.toLowerCase(); + + const normalizedBaseUrl = baseUrl.toLowerCase(); + if ( + normalizedBaseUrl.includes("canary") || + normalizedBaseUrl.includes("localhost") || + normalizedBaseUrl.includes("127.0.0.1") + ) { + return "sandbox"; + } + + return "production"; +}; + +const getUnsupportedCountryFallbackCode = (baseUrl: string) => { + const environment = getOnrampRuntimeEnvironment(baseUrl); + return environment === "production" || environment === "prod" + ? ONRAMP_PRODUCTION_FALLBACK_COUNTRY + : ONRAMP_SANDBOX_FALLBACK_COUNTRY; +}; + +const getIpCountryCode = async () => { + try { + const response = await fetch(ONRAMP_IP_COUNTRY_URL, { + headers: { Accept: "application/json" }, + method: "GET", + }); + if (!response.ok) return ""; + const data = (await response.json()) as IpCountryResponse; + return data.country?.toUpperCase() ?? ""; + } catch { + return ""; + } +}; + +const getLocalCountryCode = () => { + if (typeof navigator === "undefined") return ""; + const locales = [...(navigator.languages ?? []), navigator.language].filter( + Boolean, + ); + for (const locale of locales) { + const countryCode = locale.match(/[-_]([A-Za-z]{2})$/)?.[1]?.toUpperCase(); + if (countryCode) return countryCode; + } + return ""; +}; + +const resolveOnrampCountryCode = async () => { + const cached = readOnrampCache(ONRAMP_COUNTRY_CACHE_KEY); + if (cached) return cached; + + const resolved = (await getIpCountryCode()) || getLocalCountryCode() || "US"; + writeOnrampCache( + ONRAMP_COUNTRY_CACHE_KEY, + resolved, + ONRAMP_COUNTRY_CACHE_TTL_MS, + ); + return resolved; +}; + +const getDefaultFiatCurrencyCode = (options: OnrampOptionsResponse | null) => { + const currencies = getFiatCurrencyOptions(options); + const defaultFiat = options?.selection?.defaultFiat?.toUpperCase(); + if ( + defaultFiat && + currencies.some((currency) => currency.currencyCode === defaultFiat) + ) { + return defaultFiat; } + return currencies[0]?.currencyCode ?? ""; }; const getMethodLabel = (method?: string) => { @@ -261,31 +607,23 @@ const getMethodLabel = (method?: string) => { .filter(Boolean) .map( (part) => - part.charAt(0).toUpperCase() + part.slice(1).toLowerCase() + part.charAt(0).toUpperCase() + part.slice(1).toLowerCase(), ) .join(" ") : "Payment method"; } }; -const getMethodSubtitle = (method?: string) => { - switch ((method ?? "").toUpperCase()) { - case "UPI": - return "~1 min"; - case "APPLE_PAY": - case "GOOGLE_PAY": - return "~2 min"; - case "BANK_TRANSFER": - case "IMPS": - case "NEFT": - case "RTGS": - return "~3 min"; - case "CREDIT_DEBIT_CARD": - case "CARD": - return "~3 min"; - default: - return "Available route"; - } +const getMethodSubtitle = (method?: OnrampPaymentMethod) => { + const subtitle = + method?.subtitle ?? + method?.description ?? + method?.estimatedTime ?? + method?.estimatedDuration ?? + method?.duration; + return typeof subtitle === "string" && subtitle.trim() + ? subtitle.trim() + : undefined; }; const getProviderLabel = (provider?: string) => @@ -293,7 +631,9 @@ const getProviderLabel = (provider?: string) => ? provider .split(/[_\s-]+/) .filter(Boolean) - .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) + .map( + (part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase(), + ) .join(" ") : "Payment partner"; @@ -304,12 +644,127 @@ const getProviderInitials = (provider?: string) => { return `${parts[0]?.[0] ?? ""}${parts[1]?.[0] ?? ""}`.toUpperCase(); }; +class OnrampRequestError extends Error { + code?: string; + errorId?: string; + rawMessage?: string; + status: number; + subcode?: string; + + constructor({ + code, + errorId, + message, + rawMessage, + status, + subcode, + }: { + code?: string; + errorId?: string; + message: string; + rawMessage?: string; + status: number; + subcode?: string; + }) { + super(message); + this.name = "OnrampRequestError"; + this.code = code; + this.errorId = errorId; + this.rawMessage = rawMessage; + this.status = status; + this.subcode = subcode; + } +} + const getErrorMessage = (error: unknown) => { if (error instanceof Error) return error.message; if (typeof error === "string") return error; return "Unable to continue with local currency."; }; +const getOnrampErrorSignature = (error: OnrampRequestError) => + [error.code, error.subcode, error.rawMessage, error.message] + .filter(Boolean) + .join(" ") + .toUpperCase(); + +const isUnsupportedOnrampDestinationError = (error: unknown) => { + if (!(error instanceof OnrampRequestError)) return false; + const signature = getOnrampErrorSignature(error); + return ( + signature.includes("ASSET_NOT_SUPPORTED") || + signature.includes("CURRENCY_NOT_SUPPORTED") || + signature.includes("TOKEN_NOT_SUPPORTED") || + signature.includes("TOKEN UNSUPPORTED") || + signature.includes("UNSUPPORTED_DESTINATION") || + signature.includes("UNSUPPORTED TOKEN") || + signature.includes("DESTINATION ASSET IS NOT SUPPORTED") || + signature.includes("DESTINATIONCHAINID IS INVALID") || + signature.includes("DESTINATIONTOKEN IS INVALID") + ); +}; + +const isOnrampConfigError = (error: unknown) => { + if (!(error instanceof OnrampRequestError)) return false; + const signature = getOnrampErrorSignature(error); + return ( + signature.includes("CONFIG") || + signature.includes("CONFIGURATION") || + signature.includes("NOT CONFIGURED") || + signature.includes("NO ROUTE") || + (signature.includes("ROUTE") && signature.includes("UNAVAILABLE")) + ); +}; + +const isTerminalOnrampRateError = (error: unknown) => + isUnsupportedOnrampDestinationError(error) || isOnrampConfigError(error); + +const getOnrampRequestErrorMessage = ( + error: unknown, + context?: { + countryCode?: string; + sourceCurrencyCode?: string; + token?: SwapTokenOption; + }, +) => { + if (isTerminalOnrampRateError(error)) { + const tokenLabel = context?.token?.symbol + ? `${context.token.symbol}${ + context.token.chainName ? ` on ${context.token.chainName}` : "" + }` + : "this token"; + const localeLabel = [context?.countryCode, context?.sourceCurrencyCode] + .filter(Boolean) + .join(" / "); + return `Local currency deposits are not available for ${tokenLabel}${ + localeLabel ? ` with ${localeLabel}` : "" + }. Choose another deposit token or pay with wallet.`; + } + + return getErrorMessage(error); +}; + +const getOnrampRateRequestKey = ({ + countryCode, + destinationChainId, + destinationCurrencyCode, + destinationToken, + sourceCurrencyCode, +}: { + countryCode?: string; + destinationChainId?: string; + destinationCurrencyCode?: string; + destinationToken?: string; + sourceCurrencyCode?: string; +}) => + [ + countryCode?.toUpperCase() ?? "", + sourceCurrencyCode?.toUpperCase() ?? "", + destinationCurrencyCode?.toUpperCase() ?? "", + destinationChainId ?? "", + destinationToken?.toLowerCase() ?? "", + ].join("|"); + const ONRAMP_TERMINAL_STATES = new Set([ "CANCELLED", "EXPIRED", @@ -362,61 +817,117 @@ const isOnrampDepositFailedState = (state?: string | null) => const isNativeAddress = (address?: string) => { const lower = address?.toLowerCase(); return ( - lower === "0x0000000000000000000000000000000000000000" || + lower === zeroAddress || lower === "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" ); }; -const getChainCurrencySuffix = (chainId?: number, chainName?: string) => { - switch (chainId) { - case 1: - return "ETHEREUM"; - case 10: - return "OPTIMISM"; - case 56: - return "BNB"; - case 137: - return "POLYGON"; - case 8453: - return "BASE"; - case 42161: - return "ARBITRUM"; - case 43114: - return "AVALANCHE"; - default: - return (chainName ?? "CHAIN").replaceAll(/[^a-zA-Z0-9]+/g, "_").toUpperCase(); +const isPositiveGasLimit = (value: unknown): value is bigint => { + try { + return BigInt(value as bigint) > BigInt(0); + } catch { + return false; } }; -const getFallbackDestinationCurrencyCode = (token?: SwapTokenOption) => { - if (!token?.symbol) return ""; - return `${token.symbol.toUpperCase()}_${getChainCurrencySuffix( - token.chainId, - token.chainName - )}`; +const getOnrampChainIdNumber = (chainId?: number | string) => { + if (typeof chainId === "number") return chainId; + const match = chainId?.match(/\d+$/); + return match ? Number(match[0]) : undefined; }; -const getDestinationCurrencyCode = ( +const getOnrampCurrencyAddress = (currency?: OnrampCryptoCurrency) => + currency?.contract ?? currency?.token; + +const getOnrampCryptoCurrency = ( options: OnrampOptionsResponse | null, - token?: SwapTokenOption + token?: SwapTokenOption, ) => { - if (!token?.chainId || !token.contractAddress) { - return getFallbackDestinationCurrencyCode(token); - } + if (!token?.chainId || !token.contractAddress) return undefined; const targetAddress = token.contractAddress.toLowerCase(); - const currencies = options?.selection?.cryptoCurrencies ?? []; - const matched = currencies.find((currency) => { - if (currency.chainId !== token.chainId) return false; - const contract = currency.contract?.toLowerCase(); - if (!contract) return false; + return options?.selection?.cryptoCurrencies?.find((currency) => { + if (getOnrampChainIdNumber(currency.chainId) !== token.chainId) + return false; + const address = getOnrampCurrencyAddress(currency)?.toLowerCase(); + if (!address) return false; return ( - contract === targetAddress || - (isNativeAddress(contract) && isNativeAddress(targetAddress)) + address === targetAddress || + (isNativeAddress(address) && isNativeAddress(targetAddress)) ); }); +}; + +const getDestinationRequestDetails = ( + options: OnrampOptionsResponse | null, + token?: SwapTokenOption, +) => { + const matched = getOnrampCryptoCurrency(options, token); + return { + destinationChainId: token?.chainId ? `EVM_${token.chainId}` : undefined, + destinationCurrencyCode: + matched?.currencyCode ?? token?.symbol?.toUpperCase() ?? "", + destinationToken: token?.contractAddress, + }; +}; + +const getRawTokenAmount = (amount: unknown, decimals: number) => { + const parsed = parseDecimal(amount); + if (!parsed?.gt(0)) return null; + return parseUnits( + parsed.toDecimalPlaces(decimals, Decimal.ROUND_DOWN).toFixed(), + decimals, + ); +}; + +const getTransactionExplorerUrl = (chainId?: number, txHash?: string) => { + const baseUrl = getExplorerBaseUrl(chainId); + return baseUrl && txHash ? `${baseUrl}${txHash}` : undefined; +}; + +const getSandboxDepositAmountRaw = (decimals: number) => + BigInt(new Decimal(0.1).mul(Decimal.pow(10, decimals)).toFixed()); + +const getSdkExecuteTransactionHash = (result: any): Hex | undefined => { + const candidates = [ + result?.execute?.txHash, + result?.execute?.transactionHash, + result?.execute?.hash, + result?.executeResponse?.txHash, + result?.executeResponse?.transactionHash, + result?.executeResponse?.hash, + result?.receipt?.transactionHash, + result?.txHash, + result?.transactionHash, + result?.hash, + ]; + + return candidates.find( + (candidate): candidate is Hex => + typeof candidate === "string" && candidate.startsWith("0x"), + ); +}; - return matched?.currencyCode ?? getFallbackDestinationCurrencyCode(token); +const getSdkExecuteExplorerUrl = (result: any): string | undefined => { + const candidates = [ + result?.execute?.explorerUrl, + result?.execute?.explorerURL, + result?.execute?.txExplorerUrl, + result?.execute?.transactionExplorerUrl, + result?.executeResponse?.explorerUrl, + result?.executeResponse?.explorerURL, + result?.executeResponse?.txExplorerUrl, + result?.executeResponse?.transactionExplorerUrl, + result?.explorerUrl, + result?.explorerURL, + result?.txExplorerUrl, + result?.transactionExplorerUrl, + ]; + + return candidates.find( + (candidate): candidate is string => + typeof candidate === "string" && candidate.length > 0, + ); }; const getOnrampTokenKey = (token?: SwapTokenOption) => { @@ -424,10 +935,10 @@ const getOnrampTokenKey = (token?: SwapTokenOption) => { return `${token.chainId}:${token.contractAddress.toLowerCase()}`; }; -const isSameOnrampToken = ( - left?: SwapTokenOption, - right?: SwapTokenOption -) => Boolean(left && right && getOnrampTokenKey(left) === getOnrampTokenKey(right)); +const isSameOnrampToken = (left?: SwapTokenOption, right?: SwapTokenOption) => + Boolean( + left && right && getOnrampTokenKey(left) === getOnrampTokenKey(right), + ); const sortQuotes = (quotes: OnrampQuote[]) => [...quotes].sort((a, b) => { @@ -435,7 +946,7 @@ const sortQuotes = (quotes: OnrampQuote[]) => if (scoreDelta !== 0) return scoreDelta; const destinationDelta = parseDecimal(b.destinationAmount)?.cmp( - parseDecimal(a.destinationAmount) ?? new Decimal(0) + parseDecimal(a.destinationAmount) ?? new Decimal(0), ) ?? 0; return destinationDelta; }); @@ -444,7 +955,7 @@ const matchesSearch = (query: string, values: Array) => { const normalizedQuery = query.trim().toLowerCase(); if (!normalizedQuery) return true; return values.some((value) => - (value ?? "").toLowerCase().includes(normalizedQuery) + (value ?? "").toLowerCase().includes(normalizedQuery), ); }; @@ -455,10 +966,51 @@ const createIdempotencyKey = () => { return `${Date.now()}-${Math.random().toString(16).slice(2)}`; }; +const openOnrampProviderWindow = () => { + if (typeof window === "undefined") return null; + const providerWindow = window.open("about:blank", "_blank"); + if (!providerWindow) return null; + providerWindow.document.title = "Opening payment provider"; + providerWindow.document.body.style.fontFamily = + "system-ui, -apple-system, BlinkMacSystemFont, sans-serif"; + providerWindow.document.body.style.margin = "0"; + providerWindow.document.body.style.display = "grid"; + providerWindow.document.body.style.minHeight = "100vh"; + providerWindow.document.body.style.placeItems = "center"; + providerWindow.document.body.textContent = "Opening payment provider..."; + return providerWindow; +}; + +type OnrampCallbackPayload = { + session?: OnrampSessionResponse; + sessionId: string; + state?: string; + timestamp?: number; + type: string; +}; + +declare global { + interface Window { + setRampSessionId?: (sessionId: string) => void; + } +} + +const isOnrampCallbackPayload = ( + payload: unknown, +): payload is OnrampCallbackPayload => { + if (!payload || typeof payload !== "object") return false; + const candidate = payload as Partial; + return ( + candidate.type === ONRAMP_CALLBACK_MESSAGE_TYPE && + typeof candidate.sessionId === "string" && + candidate.sessionId.length > 0 + ); +}; + const fetchOnrampJson = async ( baseUrl: string, path: string, - init?: RequestInit + init?: RequestInit, ) => { const response = await fetch(`${baseUrl}${path}`, { ...init, @@ -469,14 +1021,31 @@ const fetchOnrampJson = async ( }, }); const text = await response.text(); - const data = text ? JSON.parse(text) : {}; + let data: any = {}; + try { + data = text ? JSON.parse(text) : {}; + } catch { + data = text ? { message: text } : {}; + } if (!response.ok) { - const message = - typeof data?.message === "string" - ? data.message + const errorData = data as OnrampErrorResponse; + const baseMessage = + typeof errorData?.message === "string" + ? errorData.message : `Onramp request failed (${response.status})`; - throw new Error(message); + const details = [errorData?.subcode, errorData?.errorId] + .filter(Boolean) + .join(" · "); + const message = details ? `${baseMessage} (${details})` : baseMessage; + throw new OnrampRequestError({ + code: errorData?.code, + errorId: errorData?.errorId, + message, + rawMessage: baseMessage, + status: response.status, + subcode: errorData?.subcode, + }); } return data as T; @@ -561,7 +1130,20 @@ function TokenLogoPair({ token }: { token?: SwapTokenOption }) { ); } -function CurrencyMark({ code }: { code?: string }) { +function CurrencyMark({ + code, + currency, +}: { + code?: string; + currency?: OnrampFiatCurrencyOption; +}) { + const displayCode = currency?.currencyCode ?? code; + const imageUrl = currency?.flagUrl ?? currency?.symbolUrl; + + if (imageUrl) { + return ; + } + return (
- {(code ?? "?").slice(0, 3).toUpperCase()} + {(currency?.symbol ?? displayCode ?? "?").slice(0, 3).toUpperCase()}
); } @@ -610,7 +1192,9 @@ function ProviderMark({ provider }: { provider?: string }) { function MethodMark({ method }: { method?: string }) { const normalized = (method ?? "").toUpperCase(); const icon = - normalized === "UPI" || normalized === "APPLE_PAY" || normalized === "GOOGLE_PAY" ? ( + normalized === "UPI" || + normalized === "APPLE_PAY" || + normalized === "GOOGLE_PAY" ? (