From 7f10661c5554a2defbb7094e66b7f568ba603b4a Mon Sep 17 00:00:00 2001 From: shrinathprabhu Date: Tue, 26 May 2026 17:57:59 +0530 Subject: [PATCH 1/6] Add predictive quoting --- components/helpers/preview-panel.tsx | 20 +- components/showcase/showcase-wrapper.tsx | 7 +- public/r/nexus-one.json | 4 +- public/r/nexus-provider.json | 2 +- .../components/swap-intent-preview.tsx | 18 +- .../nexus-elements/nexus-one/nexus-one.tsx | 900 ++++++++++++++++-- .../nexus-elements/nexus/NexusProvider.tsx | 61 +- 7 files changed, 904 insertions(+), 108 deletions(-) diff --git a/components/helpers/preview-panel.tsx b/components/helpers/preview-panel.tsx index 20bd88c..5e7e4aa 100644 --- a/components/helpers/preview-panel.tsx +++ b/components/helpers/preview-panel.tsx @@ -9,12 +9,15 @@ import { Button } from "@/registry/nexus-elements/ui/button"; interface PreviewPanelProps { children: ReactNode; connectLabel: string; + renderWhenDisconnected?: boolean; } export function PreviewPanel({ children, connectLabel, + renderWhenDisconnected = false, }: Readonly) { + const [mounted, setMounted] = useState(false); const { status, connector } = useAccount(); const { data: walletClient } = useConnectorClient(); const { nexusSDK, handleInit, loading } = useNexus(); @@ -60,6 +63,10 @@ export function PreviewPanel({ } }, [connector, handleInit, initializing, loading, nexusSDK, walletClient]); + useEffect(() => { + setMounted(true); + }, []); + useEffect(() => { if ( status === "connected" && @@ -74,10 +81,13 @@ export function PreviewPanel({ return (
- {(status === "connected" || status === "connecting") && nexusSDK && ( - <>{children} - )} - {status === "connected" && !nexusSDK && ( + {renderWhenDisconnected && mounted && <>{children}} + {!renderWhenDisconnected && + (status === "connected" || status === "connecting") && + nexusSDK && ( + <>{children} + )} + {!renderWhenDisconnected && status === "connected" && !nexusSDK && ( )} - {status !== "connected" && ( + {!renderWhenDisconnected && status !== "connected" && (

{connectLabel}

)}
diff --git a/components/showcase/showcase-wrapper.tsx b/components/showcase/showcase-wrapper.tsx index 7e2b0b5..4a7ba42 100644 --- a/components/showcase/showcase-wrapper.tsx +++ b/components/showcase/showcase-wrapper.tsx @@ -125,7 +125,12 @@ const ShowcaseWrapper = ({ )}
) : ( - {children} + + {children} + )} ); diff --git a/public/r/nexus-one.json b/public/r/nexus-one.json index 1d265eb..25209cc 100644 --- a/public/r/nexus-one.json +++ b/public/r/nexus-one.json @@ -91,13 +91,13 @@ }, { "path": "registry/nexus-elements/nexus-one/components/swap-intent-preview.tsx", - "content": "\"use client\";\n\nimport React, { useRef, useState } from \"react\";\nimport Decimal from \"decimal.js\";\nimport { ChevronDown, Info, Loader2 } from \"lucide-react\";\nimport { Button } from \"../../ui/button\";\nimport { type NexusOneMode, type DepositOpportunity } from \"../types\";\nimport { type SwapTokenOption } from \"./swap-asset-selector\";\nimport { CHAIN_METADATA, type SwapStepType } from \"@avail-project/nexus-core\";\nimport TransactionProgress from \"../../swaps/components/transaction-progress\";\n\nexport interface SwapIntentSource {\n amount: string;\n value?: string;\n chain: { id: number; logo: string; name: string };\n token: { contractAddress: string; decimals: number; symbol: string };\n}\n\nexport interface SwapIntentDestination {\n amount: string;\n value?: string;\n chain: { id: number; logo: string; name: string };\n token: { contractAddress: string; decimals: number; symbol: string };\n gas: {\n amount: string;\n value?: string;\n token: { contractAddress: string; decimals: number; symbol: string };\n };\n}\n\nexport interface SwapIntentData {\n sources: SwapIntentSource[];\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}\n\nexport interface SwapIntentPreviewProps {\n fromTokens?: SwapTokenOption[];\n fromToken?: SwapTokenOption;\n toToken?: SwapTokenOption;\n fromAmount: string;\n fromAmountUsd?: string;\n toAmount?: string;\n toAmountUsd?: string;\n toAmountTokens?: string;\n totalFeeUsd?: string;\n estimatedTime?: string;\n isLoading?: boolean;\n isRefreshing?: boolean;\n isExecuting?: boolean;\n swapType?: \"exactIn\" | \"exactOut\";\n intentData?: SwapIntentData | null;\n mode?: NexusOneMode;\n opportunity?: DepositOpportunity;\n recipientAddress?: string;\n swapBalances?: any[] | null;\n supportedTokenAssets?: any[] | null;\n activeMode?: NexusOneMode;\n steps?: Array<{ id: number; completed: boolean; step: SwapStepType }>;\n explorerUrls?: {\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n };\n onAccept: () => void;\n onReject: () => void;\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, #006BF4)\";\n\nconst stripNumeric = (value: unknown) =>\n 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.000001)) return value.lt(0) ? \"-<$0.000001\" : \"<$0.000001\";\n\n const amount = absolute.lt(0.01)\n ? formatAmount(absolute, { max: 6 })\n : 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(9).toFixed();\n};\n\nconst unique = (values: string[]) => 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 normalizeIntentToken = <\n T extends { contractAddress?: string; decimals?: number; symbol?: string },\n>(\n token: T | undefined,\n chainId?: number,\n) => {\n const chainMeta = chainId ? CHAIN_METADATA[chainId] : undefined;\n const shouldUseNative = 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: 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) 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\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 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 ? `${address.slice(0, 6)}...${address.slice(-4)}` : address;\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n tabIndex={0}\n style={{\n color: brand,\n display: \"inline-flex\",\n fontFamily,\n fontSize: \"13px\",\n fontWeight: 500,\n lineHeight: \"17px\",\n outline: \"none\",\n position: \"relative\",\n }}\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 tabIndex={0}\n style={{\n alignItems: \"center\",\n color: muted,\n display: \"inline-flex\",\n lineHeight: 0,\n outline: \"none\",\n position: \"relative\",\n }}\n >\n \n {showTooltip && (\n \n {message}\n \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 = \"10px\",\n padding = \"15px 18px\",\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 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}: 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\n const flowMode = mode ?? activeMode ?? \"swap\";\n const isDepositMode = flowMode === \"deposit\";\n const isSendMode = flowMode === \"send\";\n const isExactOutDisplayFlow = (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(intentDest.gas?.token, intentDest.chain.id),\n },\n }\n : undefined;\n const fallbackSources =\n fromTokens && fromTokens.length > 0\n ? fromTokens\n : fromToken\n ? [fromToken]\n : [];\n\n const sourceSymbols =\n normalizedIntentSources.length > 0\n ? unique(normalizedIntentSources.map((source) => source.token.symbol))\n : unique(fallbackSources.map((source) => source.symbol));\n const sourceLabel = formatSymbolSummary(sourceSymbols);\n const sourceAssetCount =\n normalizedIntentSources.length || fallbackSources.length || sourceSymbols.length;\n const hasResolvedQuote = Boolean(normalizedIntentDest && normalizedIntentSources.length > 0);\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 || \"Opportunity\"\n : normalizedIntentDest?.chain.name || toToken?.chainName || \"\";\n\n const requestedDestinationAmount =\n isExactOutDisplayFlow ? parseDecimal(toAmountTokens ?? toAmount) : undefined;\n const quotedDestinationAmount = parseDecimal(normalizedIntentDest?.amount);\n const destinationBalanceAmount = parseDecimal(toToken?.balance);\n const displayOnlyDestinationCoverage =\n requestedDestinationAmount &&\n requestedDestinationAmount.gt(0) &&\n quotedDestinationAmount &&\n requestedDestinationAmount.gt(quotedDestinationAmount) &&\n destinationBalanceAmount &&\n destinationBalanceAmount.gt(0)\n ? Decimal.min(\n requestedDestinationAmount.minus(quotedDestinationAmount),\n destinationBalanceAmount,\n )\n : undefined;\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\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 sourceUsdNumber =\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 sourceUsdNumber !== undefined &&\n destinationUsdNumber !== undefined &&\n sourceUsdNumber.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 gasSuppliedNumber = parseDecimal(bridgeFeeData?.gasSupplied);\n const swapBufferNumber = parseDecimal(intentData?.feesAndBuffer?.buffer);\n const bridgeComponentsTotalNumber = bridgeFeeData\n ? [\n executionGasFeeNumber,\n protocolFeeNumber,\n solverFeeNumber,\n gasSuppliedNumber,\n ].reduce(\n (sum, value) => sum.plus(value ?? new Decimal(0)),\n new Decimal(0),\n )\n : undefined;\n const explicitFeeNumber =\n bridgeTotalNumber ??\n (bridgeComponentsTotalNumber && bridgeComponentsTotalNumber.gt(0)\n ? bridgeComponentsTotalNumber\n : undefined) ??\n parseDecimal(totalFeeUsd) ??\n parseDecimal((intentData as any)?.fees?.total);\n const feeNumber =\n explicitFeeNumber ?? (hasFiatQuote ? new Decimal(0) : undefined);\n const priceImpactBaseUsd =\n hasFiatQuote && feeNumber !== undefined\n ? sourceUsdNumber.minus(feeNumber).minus(swapBufferNumber ?? new Decimal(0))\n : undefined;\n const quoteImpactUsd =\n hasFiatQuote && feeNumber !== undefined\n ? Decimal.max(\n sourceUsdNumber\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 : priceImpactBaseUsd !== undefined && priceImpactBaseUsd.gt(0)\n ? priceImpactUsd.neg().div(priceImpactBaseUsd).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 ...(gasSuppliedNumber && gasSuppliedNumber.gt(0)\n ? [{ label: \"Gas Sponsorship\", value: gasSuppliedNumber }]\n : []),\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 sourceUsdNumber !== undefined\n ? `${formatAmount(sourceUsdNumber)} USD`\n : pendingValue;\n const receiveUsd = hasFiatQuote\n ? `${formatAmount(destinationUsdNumber)} USD`\n : pendingValue;\n const feeUsd =\n feeNumber !== undefined\n ? formatUsdAmount(feeNumber)\n : 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 ? formatTokenAmount(destinationTokenAmount)\n : pendingValue;\n const destinationTokenDisplay = hasResolvedQuote\n ? `${formatTokenAmount(destinationTokenAmount)} ${destTokenSymbol}`\n : pendingLabel;\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 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: source.chain.name,\n tokenAmount: `${formatTokenAmount(source.amount)} ${source.token.symbol}`,\n usdAmount:\n parseDecimal(source.value) !== undefined\n ? formatUsdValue(parseDecimal(source.value) ?? new Decimal(0))\n : pendingValue,\n index,\n };\n })\n : fallbackSources.map((source, index) => {\n const sourceAmount =\n source.userAmount || (fallbackSources.length === 1 ? fromAmount : \"\");\n return {\n key: `${source.chainId ?? \"chain\"}-${source.contractAddress}-${index}`,\n tokenLogo: source.logo || \"\",\n chainLogo: source.chainLogo || \"\",\n symbol: source.symbol,\n chainName: source.chainName || \"\",\n tokenAmount: sourceAmount\n ? `${formatTokenAmount(sourceAmount)} ${source.symbol}`\n : pendingLabel,\n usdAmount:\n source.balanceInFiat && source.balance\n ? formatUsdValue(\n toDecimal(source.userAmount || 0).mul(\n toDecimal(source.balanceInFiat).div(\n Decimal.max(toDecimal(source.balance), 1),\n ),\n ),\n )\n : pendingValue,\n index,\n };\n });\n const displayOnlyDestinationSourceRow =\n displayOnlyDestinationCoverage && displayOnlyDestinationCoverage.gt(0)\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: normalizedIntentDest?.chain.logo || toToken?.chainLogo || \"\",\n symbol: destTokenSymbol,\n chainName: normalizedIntentDest?.chain.name || toToken?.chainName || \"\",\n tokenAmount: `${formatTokenAmount(displayOnlyDestinationCoverage)} ${destTokenSymbol}`,\n usdAmount:\n displayOnlyDestinationCoverageUsd !== undefined\n ? formatUsdValue(displayOnlyDestinationCoverageUsd)\n : pendingValue,\n index: baseSourceDetailRows.length,\n }\n : undefined;\n const sourceDetailRows = displayOnlyDestinationSourceRow\n ? [...baseSourceDetailRows, displayOnlyDestinationSourceRow]\n : baseSourceDetailRows;\n const singleSourceHeader = (() => {\n if (!displayOnlyDestinationSourceRow && normalizedIntentSources.length === 1) {\n const source = normalizedIntentSources[0];\n return {\n amount: formatTokenAmount(source.amount),\n chainName: 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: formatTokenAmount(sourceAmount),\n chainName: source.chainName || \"\",\n symbol: source.symbol,\n };\n }\n\n return null;\n })();\n const sourceHeaderAmount =\n singleSourceHeader?.amount ||\n (sourceUsdNumber !== undefined ? formatAmount(sourceUsdNumber) : pendingValue);\n const sourceHeaderUnit = singleSourceHeader?.symbol || \"USD\";\n const sourceHeaderSubtitle = (() => {\n if (singleSourceHeader) {\n return singleSourceHeader.chainName\n ? `on ${singleSourceHeader.chainName}`\n : \"\";\n }\n\n const count =\n sourceAssetCount + (displayOnlyDestinationSourceRow ? 1 : 0) || 1;\n return `${count} asset${count === 1 ? \"\" : \"s\"}`;\n })();\n const shouldScrollSourceDetails = sourceDetailRows.length > 5;\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\"\n ? \"Send now\"\n : \"Swap now\";\n const shouldPulseCta =\n !isLoading && !isRefreshing && !isExecuting && !quoteUnavailable;\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 \n setShowSourceDetails((value) => !value)}\n />\n \n\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 \n {source.usdAmount}\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 >\n \n \n )}\n \n ) : (\n \n \n {pendingLabel}\n \n \n )}\n \n\n \n\n {isSendMode && recipientAddress && (\n \n )}\n\n \n setShowFeeDetails((value) => !value)}\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 setShowImpactDetails((value) => !value)}\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 subtitle=\"Excess funds are refunded\"\n value={swapBufferDisplay}\n />\n )}\n \n\n {isExecuting && steps && steps.length > 0 && (\n \n 1}\n sources={progressSources.length > 1 ? progressSources : undefined}\n isTransferMode={isSendMode}\n depositOpportunityName={\n isDepositMode\n ? opportunity?.title || opportunity?.protocol\n : undefined\n }\n />\n \n )}\n\n \n {isExecuting ? (\n isDepositMode ? \"Depositing...\" : isSendMode ? \"Sending...\" : \"Swapping...\"\n ) : isLoading ? (\n \n ) : isRefreshing ? (\n \"Refreshing quotes...\"\n ) : quoteUnavailable ? (\n pendingLabel\n ) : (\n ctaLabel\n )}\n \n \n );\n}\n", + "content": "\"use client\";\n\nimport React, { useRef, useState } from \"react\";\nimport Decimal from \"decimal.js\";\nimport { ChevronDown, Info, Loader2 } from \"lucide-react\";\nimport { Button } from \"../../ui/button\";\nimport { type NexusOneMode, type DepositOpportunity } from \"../types\";\nimport { type SwapTokenOption } from \"./swap-asset-selector\";\nimport { CHAIN_METADATA, type SwapStepType } from \"@avail-project/nexus-core\";\nimport TransactionProgress from \"../../swaps/components/transaction-progress\";\n\nexport interface SwapIntentSource {\n amount: string;\n value?: string;\n chain: { id: number; logo: string; name: string };\n token: { contractAddress: string; decimals: number; symbol: string };\n}\n\nexport interface SwapIntentDestination {\n amount: string;\n value?: string;\n chain: { id: number; logo: string; name: string };\n token: { contractAddress: string; decimals: number; symbol: string };\n gas: {\n amount: string;\n value?: string;\n token: { contractAddress: string; decimals: number; symbol: string };\n };\n}\n\nexport interface SwapIntentData {\n sources: SwapIntentSource[];\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}\n\nexport interface SwapIntentPreviewProps {\n fromTokens?: SwapTokenOption[];\n fromToken?: SwapTokenOption;\n toToken?: SwapTokenOption;\n fromAmount: string;\n fromAmountUsd?: string;\n toAmount?: string;\n toAmountUsd?: string;\n toAmountTokens?: string;\n totalFeeUsd?: string;\n estimatedTime?: string;\n isLoading?: boolean;\n isRefreshing?: boolean;\n isExecuting?: boolean;\n swapType?: \"exactIn\" | \"exactOut\";\n intentData?: SwapIntentData | null;\n mode?: NexusOneMode;\n opportunity?: DepositOpportunity;\n recipientAddress?: string;\n swapBalances?: any[] | null;\n supportedTokenAssets?: any[] | null;\n activeMode?: NexusOneMode;\n steps?: Array<{ id: number; completed: boolean; step: SwapStepType }>;\n explorerUrls?: {\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n };\n onAccept: () => void;\n onReject: () => void;\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, #006BF4)\";\n\nconst stripNumeric = (value: unknown) =>\n 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.000001)) return value.lt(0) ? \"-<$0.000001\" : \"<$0.000001\";\n\n const amount = absolute.lt(0.01)\n ? formatAmount(absolute, { max: 6 })\n : 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(9).toFixed();\n};\n\nconst unique = (values: string[]) => 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 normalizeIntentToken = <\n T extends { contractAddress?: string; decimals?: number; symbol?: string },\n>(\n token: T | undefined,\n chainId?: number,\n) => {\n const chainMeta = chainId ? CHAIN_METADATA[chainId] : undefined;\n const shouldUseNative = 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: 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) 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\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 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 ? `${address.slice(0, 6)}...${address.slice(-4)}` : address;\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n tabIndex={0}\n style={{\n color: brand,\n display: \"inline-flex\",\n fontFamily,\n fontSize: \"13px\",\n fontWeight: 500,\n lineHeight: \"17px\",\n outline: \"none\",\n position: \"relative\",\n }}\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 tabIndex={0}\n style={{\n alignItems: \"center\",\n color: muted,\n display: \"inline-flex\",\n lineHeight: 0,\n outline: \"none\",\n position: \"relative\",\n }}\n >\n \n {showTooltip && (\n \n {message}\n \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 = \"10px\",\n padding = \"15px 18px\",\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 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}: 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\n const flowMode = mode ?? activeMode ?? \"swap\";\n const isDepositMode = flowMode === \"deposit\";\n const isSendMode = flowMode === \"send\";\n const isExactOutDisplayFlow = (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(intentDest.gas?.token, intentDest.chain.id),\n },\n }\n : undefined;\n const fallbackSources =\n fromTokens && fromTokens.length > 0\n ? fromTokens\n : fromToken\n ? [fromToken]\n : [];\n\n const sourceSymbols =\n normalizedIntentSources.length > 0\n ? unique(normalizedIntentSources.map((source) => source.token.symbol))\n : unique(fallbackSources.map((source) => source.symbol));\n const sourceLabel = formatSymbolSummary(sourceSymbols);\n const sourceAssetCount =\n normalizedIntentSources.length || fallbackSources.length || sourceSymbols.length;\n const hasResolvedQuote = Boolean(normalizedIntentDest && normalizedIntentSources.length > 0);\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 || \"Opportunity\"\n : normalizedIntentDest?.chain.name || toToken?.chainName || \"\";\n\n const requestedDestinationAmount =\n isExactOutDisplayFlow ? parseDecimal(toAmountTokens ?? toAmount) : undefined;\n const quotedDestinationAmount = parseDecimal(normalizedIntentDest?.amount);\n const destinationBalanceAmount = parseDecimal(toToken?.balance);\n const displayOnlyDestinationCoverage =\n requestedDestinationAmount &&\n requestedDestinationAmount.gt(0) &&\n quotedDestinationAmount &&\n requestedDestinationAmount.gt(quotedDestinationAmount) &&\n destinationBalanceAmount &&\n destinationBalanceAmount.gt(0)\n ? Decimal.min(\n requestedDestinationAmount.minus(quotedDestinationAmount),\n destinationBalanceAmount,\n )\n : undefined;\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\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 gasSuppliedNumber = parseDecimal(bridgeFeeData?.gasSupplied);\n const swapBufferNumber = parseDecimal(intentData?.feesAndBuffer?.buffer);\n const bridgeComponentsTotalNumber = bridgeFeeData\n ? [\n executionGasFeeNumber,\n protocolFeeNumber,\n solverFeeNumber,\n gasSuppliedNumber,\n ].reduce(\n (sum, value) => sum.plus(value ?? new Decimal(0)),\n new Decimal(0),\n )\n : undefined;\n const explicitFeeNumber =\n bridgeTotalNumber ??\n (bridgeComponentsTotalNumber && bridgeComponentsTotalNumber.gt(0)\n ? bridgeComponentsTotalNumber\n : undefined) ??\n parseDecimal(totalFeeUsd) ??\n parseDecimal((intentData as any)?.fees?.total);\n const feeNumber =\n explicitFeeNumber ?? (hasFiatQuote ? new Decimal(0) : undefined);\n const priceImpactBaseUsd =\n hasFiatQuote && feeNumber !== undefined\n ? effectiveSourceUsdNumber.minus(feeNumber).minus(swapBufferNumber ?? new Decimal(0))\n : undefined;\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 : priceImpactBaseUsd !== undefined && priceImpactBaseUsd.gt(0)\n ? priceImpactUsd.neg().div(priceImpactBaseUsd).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 ...(gasSuppliedNumber && gasSuppliedNumber.gt(0)\n ? [{ label: \"Gas Sponsorship\", value: gasSuppliedNumber }]\n : []),\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 intentSourceUsdNumber !== undefined\n ? `${formatAmount(intentSourceUsdNumber)} USD`\n : pendingValue;\n const receiveUsd = hasFiatQuote\n ? `${formatAmount(destinationUsdNumber)} USD`\n : pendingValue;\n const feeUsd =\n feeNumber !== undefined\n ? formatUsdAmount(feeNumber)\n : 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 ? formatTokenAmount(destinationTokenAmount)\n : pendingValue;\n const destinationTokenDisplay = hasResolvedQuote\n ? `${formatTokenAmount(destinationTokenAmount)} ${destTokenSymbol}`\n : pendingLabel;\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 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: source.chain.name,\n tokenAmount: `${formatTokenAmount(source.amount)} ${source.token.symbol}`,\n usdAmount:\n parseDecimal(source.value) !== undefined\n ? formatUsdValue(parseDecimal(source.value) ?? new Decimal(0))\n : pendingValue,\n index,\n };\n })\n : fallbackSources.map((source, index) => {\n const sourceAmount =\n source.userAmount || (fallbackSources.length === 1 ? fromAmount : \"\");\n return {\n key: `${source.chainId ?? \"chain\"}-${source.contractAddress}-${index}`,\n tokenLogo: source.logo || \"\",\n chainLogo: source.chainLogo || \"\",\n symbol: source.symbol,\n chainName: source.chainName || \"\",\n tokenAmount: sourceAmount\n ? `${formatTokenAmount(sourceAmount)} ${source.symbol}`\n : pendingLabel,\n usdAmount:\n source.balanceInFiat && source.balance\n ? formatUsdValue(\n toDecimal(source.userAmount || 0).mul(\n toDecimal(source.balanceInFiat).div(\n Decimal.max(toDecimal(source.balance), 1),\n ),\n ),\n )\n : pendingValue,\n index,\n };\n });\n const displayOnlyDestinationSourceRow =\n displayOnlyDestinationCoverage && displayOnlyDestinationCoverage.gt(0)\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: normalizedIntentDest?.chain.logo || toToken?.chainLogo || \"\",\n symbol: destTokenSymbol,\n chainName: normalizedIntentDest?.chain.name || toToken?.chainName || \"\",\n tokenAmount: `${formatTokenAmount(displayOnlyDestinationCoverage)} ${destTokenSymbol}`,\n usdAmount:\n displayOnlyDestinationCoverageUsd !== undefined\n ? formatUsdValue(displayOnlyDestinationCoverageUsd)\n : pendingValue,\n index: baseSourceDetailRows.length,\n }\n : undefined;\n const sourceDetailRows = displayOnlyDestinationSourceRow\n ? [...baseSourceDetailRows, displayOnlyDestinationSourceRow]\n : baseSourceDetailRows;\n const singleSourceHeader = (() => {\n if (!displayOnlyDestinationSourceRow && normalizedIntentSources.length === 1) {\n const source = normalizedIntentSources[0];\n return {\n amount: formatTokenAmount(source.amount),\n chainName: 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: formatTokenAmount(sourceAmount),\n chainName: source.chainName || \"\",\n symbol: source.symbol,\n };\n }\n\n return null;\n })();\n const sourceHeaderAmount =\n singleSourceHeader?.amount ||\n (intentSourceUsdNumber !== undefined\n ? formatAmount(intentSourceUsdNumber)\n : pendingValue);\n const sourceHeaderUnit = singleSourceHeader?.symbol || \"USD\";\n const sourceHeaderSubtitle = (() => {\n if (singleSourceHeader) {\n return singleSourceHeader.chainName\n ? `on ${singleSourceHeader.chainName}`\n : \"\";\n }\n\n const count =\n sourceAssetCount + (displayOnlyDestinationSourceRow ? 1 : 0) || 1;\n return `${count} asset${count === 1 ? \"\" : \"s\"}`;\n })();\n const shouldScrollSourceDetails = sourceDetailRows.length > 5;\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\"\n ? \"Send now\"\n : \"Swap now\";\n const shouldPulseCta =\n !isLoading && !isRefreshing && !isExecuting && !quoteUnavailable;\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 \n setShowSourceDetails((value) => !value)}\n />\n \n\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 \n {source.usdAmount}\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 >\n \n \n )}\n \n ) : (\n \n \n {pendingLabel}\n \n \n )}\n \n\n \n\n {isSendMode && recipientAddress && (\n \n )}\n\n \n setShowFeeDetails((value) => !value)}\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 setShowImpactDetails((value) => !value)}\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 subtitle=\"Excess funds are refunded\"\n value={swapBufferDisplay}\n />\n )}\n \n\n {isExecuting && steps && steps.length > 0 && (\n \n 1}\n sources={progressSources.length > 1 ? progressSources : undefined}\n isTransferMode={isSendMode}\n depositOpportunityName={\n isDepositMode\n ? opportunity?.title || opportunity?.protocol\n : undefined\n }\n />\n \n )}\n\n \n {isExecuting ? (\n isDepositMode ? \"Depositing...\" : isSendMode ? \"Sending...\" : \"Swapping...\"\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-one/components/swap-intent-preview.tsx" }, { "path": "registry/nexus-elements/nexus-one/nexus-one.tsx", - "content": "\"use client\";\n\nimport React, {\n useState,\n useRef,\n useEffect,\n useCallback,\n useLayoutEffect,\n useMemo,\n} from \"react\";\nimport {\n type NexusOneProps,\n type NexusOneMode,\n type SwapType,\n type DepositOpportunity,\n} from \"./types\";\nimport { SwapIdleForm } from \"./components/swap-idle-form\";\nimport { SendIdleForm } from \"./components/send-idle-form\";\nimport { DepositIdleForm } from \"./components/deposit-idle-form\";\nimport { RecipientInput } from \"./components/recipient-input\";\nimport { StatusAlert } from \"./components/status-alerts\";\nimport {\n SwapAssetSelector,\n type SwapTokenOption,\n deriveTokenOptions,\n} from \"./components/swap-asset-selector\";\nimport {\n SwapIntentPreview,\n type SwapIntentData,\n} from \"./components/swap-intent-preview\";\nimport {\n NexusOneProgressScreen,\n type NexusOneProgressEvent,\n} from \"./components/nexus-one-progress-screen\";\nimport { ReceiveAssetSelector, preloadReceiveTokens } from \"./components/receive-asset-selector\";\nimport { OpportunityList } from \"./components/opportunity-list\";\nimport { AlertCircle, ArrowLeft, ChevronDown, Loader2 } from \"lucide-react\";\nimport { useNexus } from \"../nexus/NexusProvider\";\nimport { useTransactionSteps } from \"../common/tx/useTransactionSteps\";\nimport { findCitreaReceiveToken } from \"./utils/citrea-tokens\";\nimport {\n CHAIN_METADATA,\n ERROR_CODES,\n NEXUS_EVENTS,\n type BridgeStepType,\n type SwapStepType,\n TOKEN_METADATA,\n} from \"@avail-project/nexus-core\";\nimport { useWalletClient, usePublicClient } from \"wagmi\";\nimport {\n erc20Abi,\n isAddress,\n zeroAddress,\n createPublicClient,\n http,\n encodeFunctionData,\n} from \"viem\";\nimport { normalize } from \"viem/ens\";\nimport { mainnet } from \"viem/chains\";\nimport Decimal from \"decimal.js\";\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 SwapHistoryStatus =\n | \"pending\"\n | \"fulfilled\"\n | \"failed\"\n | \"refund-initiated\";\n\ninterface SwapHistoryEntry {\n id: string;\n mode: NexusOneMode;\n status: SwapHistoryStatus;\n createdAt: number;\n startedAt: number;\n endedAt?: number;\n durationSeconds?: number;\n intentData: SwapIntentData | null;\n fromTokens: SwapTokenOption[];\n toToken?: SwapTokenOption;\n requestedToAmount?: string;\n requestedToValue?: string;\n recipientAddress?: string;\n opportunity?: DepositOpportunity;\n feeUsd?: string;\n intentId?: number;\n intentExplorerUrl?: string | null;\n sourceExplorerUrl?: string | null;\n finalExplorerUrl?: string | null;\n error?: string;\n failureMessage?: string;\n failedStepType?: string;\n autoRefundAvailable?: boolean;\n}\n\ntype SwapQuoteIssue = {\n type: \"insufficientSources\";\n message: string;\n missingUsd?: string;\n};\n\ntype CachedMaxSwapQuote = {\n decimals: number;\n maxTokenAmount: Decimal;\n maxUsdAmount?: Decimal;\n symbol: string;\n};\n\ntype CachedIntentUsdRate = {\n amount: string;\n rate: string;\n updatedAt: number;\n value: string;\n};\n\nconst QUOTE_REFRESH_INTERVAL_MS = 30000;\nconst EXACT_OUT_INPUT_DEBOUNCE_MS = 1000;\nconst DRAWER_CLOSE_MS = 220;\nconst MODAL_HEIGHT_TRANSITION_MS = 260;\nconst SWAP_HISTORY_STORAGE_KEY_PREFIX = \"nexus-one-transaction-history-v1\";\nconst waitForNextPaint = () =>\n new Promise((resolve) => {\n if (typeof window === \"undefined\" || !window.requestAnimationFrame) {\n resolve();\n return;\n }\n window.requestAnimationFrame(() => {\n window.setTimeout(() => resolve(), 0);\n });\n });\nconst tooltipSurface = \"#FFFFFE\";\nconst tooltipText = \"var(--foreground-primary, #161615)\";\nconst tooltipBorder = \"var(--border-default, #E8E8E7)\";\nconst uiFont = '\"Geist\", var(--font-geist-sans), system-ui, sans-serif';\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`;\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 isSameTokenSelection = (\n a?: SwapTokenOption | null,\n b?: SwapTokenOption | null,\n) => Boolean(a && b && getTokenSelectionKey(a) === getTokenSelectionKey(b));\n\nconst sanitizeOpportunityForHistory = (\n opportunity?: DepositOpportunity,\n): DepositOpportunity | undefined => {\n if (!opportunity) return undefined;\n return {\n id: opportunity.id,\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 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 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 === \"refund-initiated\";\n\nconst isStoredMode = (value: unknown): value is NexusOneMode =>\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 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 = sortSwapHistoryEntries(entries).map(\n sanitizeHistoryEntry,\n );\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(true)}\n onMouseLeave={() => setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onBlur={() => setShowTooltip(false)}\n tabIndex={0}\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 >\n {showTooltip && (\n \n {tooltipLabel}\n \n )}\n \n \n \n \n \n );\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 = 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 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 const max = amount.abs().gte(1) ? 6 : 8;\n return formatDecimalDisplay(amount, { max });\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 hasValidIntentExplorer = (entry: Pick) =>\n Boolean(\n entry.intentExplorerUrl &&\n entry.intentId !== undefined &&\n Number.isFinite(entry.intentId) &&\n entry.intentId > 0,\n );\n\nconst getExplorerTxUrl = (chainId?: number, txHash?: string | null) => {\n if (!chainId || !txHash) return null;\n const chainMeta = CHAIN_METADATA[chainId];\n const baseUrl =\n (chainMeta as any)?.blockExplorerUrls?.[0] ||\n (chainMeta as any)?.blockExplorers?.default?.url;\n return baseUrl ? `${String(baseUrl).replace(/\\/$/, \"\")}/tx/${txHash}` : null;\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 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 size = 34,\n}: {\n tokenLogo?: string;\n chainLogo?: string;\n tokenSymbol?: string;\n chainName?: string;\n size?: number;\n}) {\n return (\n
\n \n {chainLogo && (\n \n )}\n
\n );\n}\n\nfunction TruncatedAddress({\n address,\n color = \"#006BF4\",\n}: {\n address: string;\n color?: string;\n}) {\n const [showTooltip, setShowTooltip] = useState(false);\n const label =\n address.length > 12 ? `${address.slice(0, 6)}...${address.slice(-4)}` : address;\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n tabIndex={0}\n style={{\n color,\n display: \"inline-flex\",\n fontFamily: uiFont,\n fontSize: \"13px\",\n fontWeight: 500,\n lineHeight: \"18px\",\n outline: \"none\",\n position: \"relative\",\n }}\n >\n {label}\n {showTooltip && (\n \n {address}\n \n )}\n \n );\n}\n\nconst getDisplayDestinationSourceRow = (entry: SwapHistoryEntry) => {\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(entry.intentData?.destination.amount);\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(entry.intentData?.destination.value);\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 tokenLogo: entry.toToken.logo,\n chainLogo: entry.toToken.chainLogo,\n symbol: entry.toToken.symbol,\n chainName: entry.toToken.chainName || \"\",\n amount: displayAmount\n .toDecimalPlaces(Math.max(0, entry.toToken.decimals ?? 18), Decimal.ROUND_DOWN)\n .toFixed(),\n value: rate ? displayAmount.mul(rate).toFixed() : entry.toToken.balanceInFiat,\n };\n};\n\nconst getProgressStepType = (\n step?: SwapStepType | BridgeStepType | null,\n) => String((step as any)?.type ?? (step as any)?.typeID ?? \"\").toUpperCase();\n\nconst isBridgeRefundStepType = (type: string) =>\n type.includes(\"RFF_ID\") || type.includes(\"BRIDGE_DEPOSIT\");\n\nconst isSwapSkippedStepType = (type: string) =>\n type.includes(\"SWAP_SKIPPED\");\n\nconst isAutoRefundAvailableProgressEvent = (\n event?: NexusOneProgressEvent,\n) =>\n event?.name === NEXUS_EVENTS.SWAP_STEP_COMPLETE &&\n isBridgeRefundStepType(getProgressStepType(event.step));\n\nconst getFailureMessageForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n mode: NexusOneMode,\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(\"SOURCE_SWAP\") ||\n type.includes(\"COLLECTION\")\n ) {\n return \"Collection Failed\";\n }\n if (\n type.includes(\"DESTINATION_SWAP\") ||\n type.includes(\"FULFIL\")\n ) {\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(\"RFF\") ||\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 getSourceRows = (entry: SwapHistoryEntry) => {\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 tokenLogo: fallback?.logo,\n chainLogo: source.chain.logo || fallback?.chainLogo,\n symbol: source.token.symbol,\n chainName: source.chain.name,\n amount: source.amount,\n value: source.value,\n };\n });\n\n return displayDestinationSourceRow\n ? [displayDestinationSourceRow, ...sourceRows]\n : sourceRows;\n }\n\n const fallbackRows = entry.fromTokens.map((token, index) => ({\n key: `${token.chainId}-${token.contractAddress}-${index}`,\n tokenLogo: token.logo,\n chainLogo: token.chainLogo,\n symbol: token.symbol,\n chainName: token.chainName || \"\",\n amount: token.userAmount || \"0\",\n value: token.balanceInFiat,\n }));\n\n return displayDestinationSourceRow\n ? [displayDestinationSourceRow, ...fallbackRows]\n : fallbackRows;\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 scrollRef.current?.scrollBy({ top: 72, behavior: \"smooth\" })}\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 >\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 isDeposit = entry.mode === \"deposit\";\n const isSend = entry.mode === \"send\";\n const tokenSymbol = destination?.token.symbol || entry.toToken?.symbol || \"\";\n const chainName = destination?.chain.name || entry.toToken?.chainName || \"\";\n const depositVenue =\n entry.opportunity?.title || entry.opportunity?.protocol || chainName;\n const amount = destination?.amount || \"\";\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 = `Intent #${entry.intentId}`;\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 storedFailureMessage =\n !entry.autoRefundAvailable && entry.failureMessage?.includes(\"Refund\")\n ? undefined\n : entry.failureMessage;\n const failureHeadline =\n storedFailureMessage ||\n (isDeposit\n ? \"Deposit failed. Funds are in your wallet\"\n : isSend\n ? \"Send failed. Funds are in your wallet\"\n : defaultSwapFailureHeadline);\n const receiptLocation = isDeposit ? depositVenue : chainName;\n const receiptSummary = receiptLocation ? `on ${receiptLocation}` : \"\";\n\n return (\n
\n \n \n \n \n {isFailed ? \"x\" : \"โœ“\"}\n
\n \n
\n {isFailed\n ? failureHeadline\n : isDeposit\n ? \"You deposited\"\n : isSend\n ? \"You sent\"\n : \"You received\"}\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: \"#006BF4\",\n cursor: \"pointer\",\n display: \"inline-flex\",\n fontFamily: uiFont,\n fontSize: \"12px\",\n gap: \"4px\",\n padding: 0,\n }}\n >\n {showSourceDetails ? \"Hide Details\" : `${sourceCount} asset${sourceCount === 1 ? \"\" : \"s\"}`}\n \n \n \n \n \n
\n \n
\n \n {isSend && 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({\n status,\n}: {\n status: SwapHistoryStatus;\n}) {\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 === \"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 onRefund,\n}: {\n entries: SwapHistoryEntry[];\n now: number;\n onRefund: (entry: SwapHistoryEntry) => void;\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 swap,\n 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 = entry.toToken?.logo;\n const destinationChainLogo =\n destination?.chain.logo || entry.toToken?.chainLogo || \"\";\n const destinationChainName =\n destination?.chain.name || entry.toToken?.chainName || \"\";\n const destinationSymbol = 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 showIntentExplorer = hasValidIntentExplorer(entry);\n const viewUrl = showIntentExplorer\n ? entry.intentExplorerUrl\n : entry.finalExplorerUrl;\n const canShowRefund =\n entry.status === \"failed\" &&\n Boolean(entry.autoRefundAvailable);\n const status = canShowRefund ? \"refund-initiated\" : entry.status;\n const sourceRows = getSourceRows(entry);\n const firstSource = sourceRows[0];\n\n return (\n \n
\n
\n \n
\n
\n {destinationAmount ? formatTokenDisplay(destinationAmount) : \"--\"}\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 onRefund(entry)}\n style={{\n background: \"#006BF4\",\n border: \"none\",\n borderRadius: \"8px\",\n color: \"#FFFFFE\",\n cursor: entry.intentId ? \"pointer\" : \"not-allowed\",\n fontFamily: uiFont,\n fontSize: \"13px\",\n fontWeight: 600,\n opacity: entry.intentId ? 1 : 0.5,\n padding: \"8px 14px\",\n }}\n >\n Refund\n \n \n )}\n\n \n
\n {firstSource && (\n \n )}\n \n โ†’\n \n \n {showIntentExplorer ? (\n \n Intent #{entry.intentId}\n \n ) : entry.finalExplorerUrl ? (\n \n Final transaction\n \n ) : null}\n
\n {viewUrl && (\n \n View โ†—\n \n )}\n \n \n );\n })}\n \n );\n}\n\n// ---------------------------------------------------------------------------\n// NexusOne\n// ---------------------------------------------------------------------------\n\nexport function NexusOne({\n config,\n embed = true,\n connectedAddress,\n onComplete,\n onStart,\n onError,\n onClose,\n}: NexusOneProps) {\n const {\n nexusSDK,\n bridgableBalance,\n swapBalance,\n getFiatValue,\n resolveTokenUsdRate,\n swapSupportedChainsAndTokens,\n supportedChainsAndTokens,\n fetchSwapBalance,\n } = useNexus();\n\n // Mode is a single value, not an array\n const activeMode = config.mode;\n if (\n activeMode === \"deposit\" &&\n (!config.opportunities || config.opportunities.length === 0)\n ) {\n throw new Error(\n \"NexusOne deposit mode requires config.opportunities with at least one opportunity.\",\n );\n }\n const showCloseButton = !embed && Boolean(onClose);\n\n // Preload receive tokens once SDK is available\n useEffect(() => {\n if (nexusSDK) {\n preloadReceiveTokens();\n }\n }, [nexusSDK]);\n\n const { data: walletClient } = useWalletClient();\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 : walletClientAddress &&\n isAddress(walletClientAddress) &&\n walletClientAddress.toLowerCase() !== zeroAddress\n ? walletClientAddress\n : undefined;\n const historyStorageKey = getSwapHistoryStorageKey(ownerAddress);\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 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 previousDefaultRecipientRef = useRef(defaultRecipientAddress);\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 [closingDrawerStep, setClosingDrawerStep] =\n useState(null);\n const rootContentRef = useRef(null);\n const [rootContentHeight, setRootContentHeight] = useState(\n null,\n );\n const [hasMeasuredRootContent, setHasMeasuredRootContent] = useState(false);\n const [fromTokens, setFromTokens] = useState([]);\n const [sourceSelectionTouched, setSourceSelectionTouched] = useState(false);\n const [sourceSelectionRevision, setSourceSelectionRevision] = useState(0);\n const [, setExactOutQuoteSourceMode] = useState<\"all\" | \"selected\">(\"all\");\n const exactOutQuoteSourceModeRef = useRef<\"all\" | \"selected\">(\"all\");\n const [toToken, setToToken] = useState(\n undefined,\n );\n const appliedTokenPrefillRef = useRef(null);\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 }, [activeMode, fetchSwapBalance, nexusSDK, swapStep]);\n\n useEffect(() => {\n setSourceSelectionTouched(false);\n setExactOutQuoteSourceModeValue(\"all\");\n }, [activeMode, setExactOutQuoteSourceModeValue]);\n\n useEffect(() => {\n const previousDefault = previousDefaultRecipientRef.current;\n previousDefaultRecipientRef.current = defaultRecipientAddress;\n\n if (activeMode !== \"swap\" || !defaultRecipientAddress) return;\n\n setRecipientAddress((current) => {\n if (\n !current ||\n (previousDefault &&\n current.toLowerCase() === previousDefault.toLowerCase())\n ) {\n return defaultRecipientAddress;\n }\n return current;\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 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 maxSwapQuoteCacheRef = useRef>({});\n const intentDestinationUsdRateCacheRef = useRef<\n Record\n >({});\n const intentSymbolUsdRateCacheRef = useRef>(\n {},\n );\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 [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\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 } | null>(null);\n\n useEffect(() => {\n swapStepRef.current = swapStep;\n }, [swapStep]);\n\n useEffect(() => {\n return () => {\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n }\n };\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 setSwapStep(\"idle\");\n return;\n }\n\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n }\n\n setClosingDrawerStep(swapStep);\n drawerCloseTimerRef.current = setTimeout(() => {\n setSwapStep(\"idle\");\n setClosingDrawerStep(null);\n drawerCloseTimerRef.current = null;\n }, DRAWER_CLOSE_MS);\n }, [swapStep]);\n\n const openDrawerStep = useCallback((nextStep: SwapStep) => {\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n drawerCloseTimerRef.current = null;\n }\n setClosingDrawerStep(null);\n setSwapStep(nextStep);\n }, []);\n\n const syncRootContentHeight = useCallback(() => {\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 setRootContentHeight((previousHeight) =>\n previousHeight === nextHeight ? previousHeight : nextHeight,\n );\n setHasMeasuredRootContent(true);\n }, []);\n\n useLayoutEffect(() => {\n syncRootContentHeight();\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(syncRootContentHeight);\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 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?.icon)\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 = matchedAsset?.icon ?? (isNativeSource ? chainMeta?.logo : \"\");\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: 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: chainMeta?.name ?? source.chain.name,\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 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 }\n };\n\n const clearSelectedSources = () => {\n setFromTokens((current) => (current.length === 0 ? current : []));\n setSourceSelectionTouched(false);\n setExactOutQuoteSourceModeValue(\"all\");\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 = 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\n const minimumSourceUsd = new Decimal(1);\n const hasMinimumSourceUsdBalance = (\n token: Pick,\n ) => (parseFiatNumber(token.balanceInFiat) ?? new Decimal(0)).gte(minimumSourceUsd);\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 = (intent?: SwapIntentData | null) => {\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 (!amount || !value || amount.lte(0) || value.lte(0) || !chainId || !symbol) {\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(\n (breakdownSum, item) => {\n const value = parseFiatNumber(item.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd)\n ? breakdownSum.plus(value)\n : breakdownSum;\n },\n new Decimal(0),\n ),\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 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 const getTotalBalancePercentUsdAmount = (pct: number) =>\n getSwapBalanceTotalUsd().mul(pct).div(100);\n const formatTokenAmountFromUsd = (\n usdAmount: Decimal,\n token: Pick,\n ) => {\n const rate = getUsdRateForSymbol(token.symbol);\n if (rate.lte(0)) return undefined;\n return usdAmount\n .div(rate)\n .toDecimalPlaces(Math.max(0, token.decimals ?? 18), Decimal.ROUND_DOWN)\n .toFixed();\n };\n\n const getMaxSwapQuoteCacheKey = (token?: SwapTokenOption) => {\n if (!token?.chainId) return \"\";\n return [\n token.chainId,\n (token.contractAddress || zeroAddress).toLowerCase(),\n token.symbol.toUpperCase(),\n ].join(\":\");\n };\n\n const getCachedMaxSwapQuote = (token?: SwapTokenOption) => {\n const key = getMaxSwapQuoteCacheKey(token);\n return key ? maxSwapQuoteCacheRef.current[key] : undefined;\n };\n\n const getCachedDestinationUsdRate = (token?: SwapTokenOption) => {\n const intentCachedRate = getCachedIntentUsdRate(token);\n if (intentCachedRate && intentCachedRate.gt(0)) {\n return intentCachedRate;\n }\n\n const cached = getCachedMaxSwapQuote(token);\n if (\n !cached ||\n !cached.maxUsdAmount ||\n cached.maxUsdAmount.lte(0) ||\n cached.maxTokenAmount.lte(0)\n ) {\n return undefined;\n }\n return cached.maxUsdAmount.div(cached.maxTokenAmount);\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 resolveMaxSwapQuote = async (token: SwapTokenOption) => {\n const key = getMaxSwapQuoteCacheKey(token);\n if (!key) return undefined;\n\n const cached = maxSwapQuoteCacheRef.current[key];\n if (cached) return cached;\n\n const calculateMaxForSwap = nexusSDK?.calculateMaxForSwap;\n if (typeof calculateMaxForSwap !== \"function\" || !token.chainId) {\n return undefined;\n }\n\n const max = await calculateMaxForSwap({\n toChainId: token.chainId,\n toTokenAddress: (token.contractAddress || zeroAddress) as `0x${string}`,\n });\n const decimals = Number.isFinite(Number(max.decimals))\n ? Number(max.decimals)\n : token.decimals || 18;\n const maxAmount =\n parseFiatNumber(max.maxAmount) ??\n (max.maxAmountRaw !== undefined\n ? new Decimal(max.maxAmountRaw.toString()).div(\n new Decimal(10).pow(decimals),\n )\n : undefined);\n\n if (!maxAmount || maxAmount.lte(0)) return undefined;\n\n const safeMaxAmount = maxAmount.mul(receiveMaxSafetyMultiplier);\n const destinationRate = await resolveUsdRateForSymbol(max.symbol || token.symbol);\n let maxUsdAmount =\n destinationRate.gt(0) ? safeMaxAmount.mul(destinationRate) : undefined;\n\n if (!maxUsdAmount || maxUsdAmount.lte(0)) {\n const sourcesUsd = await (max.sources ?? []).reduce(\n async (sumPromise, source) => {\n const sum = await sumPromise;\n const amount = parseFiatNumber(source.amount) ?? new Decimal(0);\n if (amount.lte(0)) return sum;\n\n const sourceRate = await resolveUsdRateForSymbol(source.symbol);\n return sourceRate.gt(0) ? sum.plus(amount.mul(sourceRate)) : sum;\n },\n Promise.resolve(new Decimal(0)),\n );\n\n if (sourcesUsd.gt(0)) {\n maxUsdAmount = sourcesUsd.mul(receiveMaxSafetyMultiplier);\n }\n }\n\n const quote: CachedMaxSwapQuote = {\n decimals,\n maxTokenAmount: safeMaxAmount,\n maxUsdAmount,\n symbol: max.symbol || token.symbol,\n };\n maxSwapQuoteCacheRef.current[key] = quote;\n return quote;\n };\n\n const getPercentAmountFromMaxQuote = async (\n token: SwapTokenOption,\n pct: number,\n preferUsd: boolean,\n ) => {\n const maxQuote = await resolveMaxSwapQuote(token);\n if (!maxQuote) return undefined;\n\n const ratio = new Decimal(pct).div(100);\n if (preferUsd && maxQuote.maxUsdAmount && maxQuote.maxUsdAmount.gt(0)) {\n return {\n amount: maxQuote.maxUsdAmount\n .mul(ratio)\n .toDecimalPlaces(2, Decimal.ROUND_DOWN)\n .toFixed(),\n mode: \"usd\" as const,\n };\n }\n\n return {\n amount: maxQuote.maxTokenAmount\n .mul(ratio)\n .toDecimalPlaces(Math.max(0, maxQuote.decimals), Decimal.ROUND_DOWN)\n .toFixed(),\n mode: \"token\" as const,\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 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 = (token: SwapTokenOption, tokenAmount: Decimal) => {\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 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(source, targetUsd)\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.toDecimalPlaces(6, Decimal.ROUND_DOWN).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(remainingTokenAmount, 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.toDecimalPlaces(6, Decimal.ROUND_DOWN).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 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 ? token.sourceTokens : [token],\n );\n const seen = new Set();\n return expanded.filter((token) => {\n if (!token.chainId || !token.contractAddress) return false;\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 = 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 = (breakdown.symbol ?? asset.symbol ?? \"\").toUpperCase();\n const assetSymbol = (asset.symbol ?? \"\").toUpperCase();\n const isNativeBalance =\n isNativeTokenAddress(breakdown.contractAddress) ||\n Boolean(nativeSymbol && (breakdownSymbol === nativeSymbol || assetSymbol === nativeSymbol));\n\n if (!isNativeBalance) continue;\n balance = balance.plus(parseFiatNumber(breakdown.balance) ?? new Decimal(0));\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 ) 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: chainMeta?.name ?? breakdown.chain?.name,\n contractAddress,\n decimals: breakdown.decimals ?? asset.decimals ?? 18,\n logo: asset.icon ?? \"\",\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 getExactOutSourceTokens = (\n mode: \"all\" | \"selected\" = exactOutQuoteSourceModeRef.current,\n ) => {\n if (\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n mode === \"selected\" &&\n fromTokens.length > 0\n ) {\n return filterMinimumSourceUsdTokens(getExpandedSourceTokens(fromTokens)).filter(\n hasGasForSource,\n );\n }\n\n return getGasCapableBalanceSourceTokens();\n };\n\n const buildFromSourcesPayload = (tokens: SwapTokenOption[]) => {\n const eligibleTokens = filterMinimumSourceUsdTokens(tokens).filter(\n (token) => token.chainId && token.contractAddress,\n );\n return {\n fromSources: eligibleTokens.map((token) => ({\n chainId: token.chainId!,\n tokenAddress: token.contractAddress as `0x${string}`,\n })),\n };\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 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 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 const selectedSourceTotal =\n exactOutQuoteSourceModeRef.current === \"selected\" && fromTokens.length > 0\n ? fromTokens.reduce(\n (sum, token) => {\n const value = parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd) ? sum.plus(value) : sum;\n },\n new Decimal(0),\n )\n : undefined;\n\n if (selectedSourceTotal && selectedSourceTotal.gt(0)) {\n return selectedSourceTotal;\n }\n\n const allSourceTotal = getGasCapableBalanceSourceTokens().reduce(\n (sum, token) => {\n const value = parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd) ? sum.plus(value) : sum;\n },\n new Decimal(0),\n );\n\n return allSourceTotal.gt(0) ? allSourceTotal : getSwapBalanceTotalUsd();\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(missingTokenAmount.mul(fiatBalance.div(availableTokenAmount)));\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 = (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 = getExactOutRequestedUsd();\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 ||\n missingUsd.lte(0) ||\n 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 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()).div(new Decimal(10).pow(decimals)).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 receiveMaxSafetyMultiplier = new Decimal(\"0.9\");\n const currentSwapEntry =\n currentSwapId !== null\n ? swapHistory.find((entry) => entry.id === currentSwapId)\n : undefined;\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) =>\n entry.id === id ? { ...entry, ...patch } : entry,\n ),\n ),\n );\n };\n\n const patchCurrentSwapHistoryEntry = (patch: Partial) => {\n patchSwapHistoryEntry(currentSwapIdRef.current, patch);\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,\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 ) => {\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 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: activeMode === \"send\" ? recipientAddress : undefined,\n opportunity: selectedOpportunity,\n feeUsd: intentFeeUsd,\n sourceExplorerUrl: null,\n finalExplorerUrl: null,\n intentExplorerUrl: null,\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\",\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(\n 1,\n Math.round((now - startedAt) / 1000),\n ),\n sourceExplorerUrl: explorerUrlsRef.current.sourceExplorerUrl,\n finalExplorerUrl: explorerUrlsRef.current.destinationExplorerUrl,\n ...patch,\n });\n void fetchSwapBalance();\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 handleRefundIntent = async (entry: SwapHistoryEntry) => {\n if (!nexusSDK || !entry.intentId) return;\n patchSwapHistoryEntry(entry.id, { status: \"refund-initiated\" });\n try {\n await nexusSDK.refundIntent(entry.intentId);\n void fetchSwapBalance();\n } catch (error: any) {\n patchSwapHistoryEntry(entry.id, {\n status: \"failed\",\n error: error?.message || \"Refund failed. Please try again.\",\n });\n void fetchSwapBalance();\n }\n };\n\n const applySwapIntent = useCallback(\n (intent: SwapIntentData) => {\n lastSwapIntentRefreshAtRef.current = Date.now();\n cacheDestinationUsdRateFromIntent(intent);\n setIntentData(intent);\n setIntentToAmount(intent.destination?.amount || undefined);\n setSwapQuoteIssue(null);\n\n if (\n (activeMode === \"send\" ||\n (activeMode === \"deposit\" && swapType === \"exactOut\"))\n ) {\n syncingIntentSourcesRef.current = true;\n setFromTokens((intent.sources ?? []).map(buildIntentSourceToken));\n }\n\n try {\n const bridgeFees = intent.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 bridgeComponentsTotal = bridgeFeeData\n ? [\n executionGasFee,\n parseFiatNumber(bridgeFeeData.protocol),\n parseFiatNumber(bridgeFeeData.solver),\n parseFiatNumber(bridgeFeeData.gasSupplied),\n ].reduce(\n (sum, value) => sum.plus(value ?? new Decimal(0)),\n new Decimal(0),\n )\n : undefined;\n const bridgeTotal =\n typeof bridgeFees === \"string\"\n ? parseFiatNumber(bridgeFees)\n : parseFiatNumber(bridgeFeeData?.total) ??\n (bridgeComponentsTotal && bridgeComponentsTotal.gt(0)\n ? bridgeComponentsTotal\n : undefined);\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 [activeMode, swapType, swapBalance],\n );\n\n // Register swap intent hook immediately before executing a swap to prevent race conditions across multiple components\n const registerIntentHook = (runId: number) => {\n if (!nexusSDK) return;\n nexusSDK.setOnSwapIntentHook(async ({ intent, allow, deny, refresh }) => {\n if (swapRunIdRef.current !== runId) {\n deny();\n return;\n }\n // Store callbacks so accept/reject buttons can call them\n swapIntentRef.current = { intent, allow, deny, refresh, runId };\n // Populate intent data for preview\n applySwapIntent(intent);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n });\n };\n\n // Deposit-specific\n const [selectedOpportunity, setSelectedOpportunity] = useState<\n DepositOpportunity | undefined\n >(() =>\n activeMode === \"deposit\" && config.opportunities?.length === 1\n ? config.opportunities[0]\n : undefined,\n );\n const [pendingOpportunity, setPendingOpportunity] = useState<\n DepositOpportunity | undefined\n >(undefined);\n const [depositAmountMode, setDepositAmountMode] = useState<\"token\" | \"usd\">(\n \"token\",\n );\n\n const toTokenFromOpportunity = (\n opp: DepositOpportunity,\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() ||\n 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: matchedToken?.decimals ?? citreaToken?.decimals ?? tokenMeta?.decimals ?? 18,\n logo: opp.tokenLogo || matchedToken?.logo || citreaToken?.logo || tokenMeta?.icon,\n chainName: CHAIN_METADATA[opp.chainId]?.name ?? citreaToken?.chainName,\n chainLogo: CHAIN_METADATA[opp.chainId]?.logo ?? citreaToken?.chainLogo,\n };\n };\n\n const getDestinationBalanceFromSwapBalances = (\n token?: SwapTokenOption,\n ) => {\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() === 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?: { token: `0x${string}`; chain: number }) => {\n if (!pair?.token || !pair.chain) return undefined;\n\n const normalizeAddress = (address?: string) => {\n if (!address) return \"\";\n return isNativeTokenAddress(address) ? zeroAddress : address.toLowerCase();\n };\n const targetAddress = normalizeAddress(pair.token);\n\n const balanceToken = deriveTokenOptions(swapBalance ?? []).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((item) => item.id === pair.chain);\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 tokenSymbol = matchedToken?.symbol ?? citreaToken?.symbol ?? \"Token\";\n const tokenMeta = TOKEN_METADATA[tokenSymbol as keyof typeof TOKEN_METADATA];\n\n if (!chain && !matchedToken && !citreaToken) return undefined;\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: matchedToken?.decimals ?? citreaToken?.decimals ?? tokenMeta?.decimals ?? 18,\n logo: matchedToken?.logo || citreaToken?.logo || tokenMeta?.icon,\n chainName:\n chain?.name ?? CHAIN_METADATA[pair.chain]?.name ?? citreaToken?.chainName,\n chainLogo:\n chain?.logo ?? CHAIN_METADATA[pair.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 ].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([{ ...sourceToken, userAmount: \"\" }]);\n setSourceSelectionTouched(true);\n }\n if (destinationToken) {\n setToToken(destinationToken);\n }\n setSwapType(\"exactIn\");\n appliedTokenPrefillRef.current = prefillKey;\n }, [\n activeMode,\n config.prefill?.destination?.chain,\n config.prefill?.destination?.token,\n config.prefill?.source?.chain,\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?.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 = getDestinationBalanceFromSwapBalances(balanceToken);\n if (swapBalanceValue) {\n setDestinationBalance(swapBalanceValue);\n return;\n }\n\n const chainMeta = CHAIN_METADATA[balanceToken.chainId];\n const rpcUrl = chainMeta?.rpcUrls?.[0];\n if (!rpcUrl) return;\n\n let cancelled = false;\n const client = createPublicClient({\n chain: {\n id: balanceToken.chainId,\n name: chainMeta?.name ?? balanceToken.chainName ?? \"Destination Chain\",\n nativeCurrency: chainMeta?.nativeCurrency ?? {\n decimals: 18,\n name: \"Ether\",\n symbol: \"ETH\",\n },\n rpcUrls: {\n default: { http: [rpcUrl] },\n public: { http: [rpcUrl] },\n },\n blockExplorers: chainMeta?.blockExplorerUrls?.[0]\n ? {\n default: {\n name: chainMeta.name,\n url: chainMeta.blockExplorerUrls[0],\n },\n }\n : undefined,\n } as any,\n transport: http(rpcUrl),\n });\n\n const fetchDestinationBalance = async () => {\n try {\n let rawBalance: bigint;\n let decimals = balanceToken.decimals || 18;\n\n if (isNativeTokenAddress(balanceToken.contractAddress)) {\n rawBalance = await client.getBalance({\n address: ownerAddress as `0x${string}`,\n });\n decimals = 18;\n } else {\n const tokenAddress = balanceToken.contractAddress as `0x${string}`;\n const [balanceResult, decimalsResult] = await Promise.all([\n client.readContract({\n abi: erc20Abi,\n address: tokenAddress,\n functionName: \"balanceOf\",\n args: [ownerAddress as `0x${string}`],\n }) as Promise,\n client\n .readContract({\n abi: erc20Abi,\n address: tokenAddress,\n functionName: \"decimals\",\n })\n .catch(() => decimals),\n ]);\n\n rawBalance = balanceResult;\n decimals = Number(decimalsResult) || decimals;\n }\n\n if (!cancelled) {\n setDestinationBalance(\n `${formatReadableTokenBalanceAmount(rawBalance, decimals)} ${balanceToken.symbol}`,\n );\n }\n } catch (error) {\n console.warn(\"Unable to fetch destination token balance\", error);\n }\n };\n\n void fetchDestinationBalance();\n\n return () => {\n cancelled = true;\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\") return;\n if (selectedOpportunity) return;\n if (config.opportunities?.length === 1) {\n const [opp] = config.opportunities;\n setSelectedOpportunity(opp);\n setSwapType(\"exactOut\");\n setToToken(toTokenFromOpportunity(opp));\n }\n }, [activeMode, config.opportunities, selectedOpportunity, supportedChainsAndTokens]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || !selectedOpportunity) return;\n setToToken((current) => ({\n ...toTokenFromOpportunity(selectedOpportunity),\n balance: current?.balance ?? \"0\",\n balanceInFiat: current?.balanceInFiat ?? \"$0.00\",\n }));\n }, [activeMode, selectedOpportunity, supportedChainsAndTokens]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (selectedOpportunity) return;\n if (!config.opportunities || config.opportunities.length <= 1) return;\n setPendingOpportunity((current) => current ?? config.opportunities?.[0]);\n }, [activeMode, config.opportunities, selectedOpportunity]);\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 (activeMode !== \"deposit\" && activeMode !== \"send\") return;\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 One 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) return new Decimal(0);\n const fiat = getFiatValue(1, selectedOpportunity.tokenSymbol);\n if (Number.isFinite(fiat) && fiat > 0) {\n return new Decimal(fiat);\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 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?.toDecimalPlaces(toToken?.decimals ?? 18).toFixed() ??\n \"0\";\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 = () => {\n const shouldLoadQuote = canRefreshExactOutQuote();\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n setTxError(null);\n setSwapQuoteIssue(null);\n }\n return shouldLoadQuote;\n };\n const defaultDepositSourceTokens = useMemo(() => {\n if (activeMode !== \"deposit\" || !swapBalance) return [];\n return deriveTokenOptions(swapBalance)\n .filter(hasMinimumSourceUsdBalance)\n .map((token) => ({\n ...token,\n userAmount: \"\",\n }));\n }, [activeMode, swapBalance]);\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 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 toToken.contractAddress &&\n (breakdownAddress.toLowerCase() === 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 if (!fiatBalance || fiatBalance.lt(minimumSourceUsd)) continue;\n return [\n {\n chainId,\n chainLogo: chainMeta?.logo ?? breakdown.chain?.logo ?? toToken.chainLogo,\n chainName: chainMeta?.name ?? breakdown.chain?.name ?? toToken.chainName,\n contractAddress: breakdown.contractAddress ?? toToken.contractAddress,\n decimals: breakdown.decimals ?? asset.decimals ?? toToken.decimals ?? 18,\n logo: asset.icon ?? 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 },\n ];\n }\n }\n\n return [];\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 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) => getTokenSelectionKey(token) === getTokenSelectionKey(locked),\n ),\n );\n if (missing.length === 0) return current;\n return [...current, ...missing.map((token) => ({ ...token, userAmount: \"\" }))];\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 (\n defaultDepositSourceTokens.length === 0 &&\n lockedDestinationSourceTokens.length === 0\n ) {\n return;\n }\n\n setFromTokens((current) => {\n const lockedKeys = new Set(\n lockedDestinationSourceTokens.map(getTokenSelectionKey),\n );\n const canInitialize =\n current.length === 0 ||\n current.every((token) => lockedKeys.has(getTokenSelectionKey(token)));\n if (!canInitialize) return current;\n\n const next: SwapTokenOption[] = [];\n const seen = new Set();\n for (const token of [\n ...defaultDepositSourceTokens,\n ...lockedDestinationSourceTokens,\n ]) {\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 defaultDepositSourceTokens,\n depositTokenAmountForQuote?.toFixed(),\n lockedDestinationSourceTokens,\n sourceSelectionTouched,\n toToken,\n ]);\n\n // ---------------------------------------------------------------------------\n // Handlers\n // ---------------------------------------------------------------------------\n\n const handleReset = () => {\n clearPendingSwapIntent();\n setAmount(\"\");\n setRecipientAddress(\"\");\n setTxError(null);\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n clearSelectedSources();\n setToToken(undefined);\n setSelectedOpportunity(undefined);\n setPendingOpportunity(undefined);\n setDepositAmountMode(\"token\");\n };\n\n const resetInputsAfterSuccessfulExecution = () => {\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 setToToken(undefined);\n setDepositAmountMode(\"token\");\n };\n\n const handleSelectDepositOpportunity = (opp: DepositOpportunity) => {\n clearPendingSwapIntent();\n setTxError(null);\n setSwapQuoteIssue(null);\n setSelectedOpportunity(opp);\n setPendingOpportunity(opp);\n setSwapType(\"exactOut\");\n setDepositAmountMode(\"token\");\n setAmount(\"\");\n clearSelectedSources();\n setToToken(toTokenFromOpportunity(opp));\n };\n\n const handleClose = () => {\n clearPendingSwapIntent();\n onClose?.();\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 โ€” SDK will trigger setOnSwapIntentHook for preview */\n const handleEnterPreview = async (\n options: { background?: boolean } = {},\n ) => {\n const { background = false } = options;\n const isExactOutFlow = activeMode === \"deposit\" || activeMode === \"send\";\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 setTxError(null);\n setSwapQuoteIssue(null);\n\n if (\n !background &&\n swapIntentRef.current?.runId === swapRunIdRef.current &&\n intentData &&\n !intentLoading &&\n (activeMode !== \"send\" || Boolean(recipientAddress)) &&\n ((activeMode !== \"deposit\" && activeMode !== \"send\") ||\n (intentData.sources ?? []).length > 0)\n ) {\n swapStepRef.current = \"preview-intent\";\n setSwapStep(\"preview-intent\");\n return;\n }\n\n if (\n !background &&\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (!intentData ||\n !swapIntentRef.current ||\n swapIntentRef.current.runId !== swapRunIdRef.current ||\n (intentData.sources ?? []).length === 0)\n ) {\n setTxError(\"Quote unavailable. Please wait for sources to be selected.\");\n return;\n }\n\n const hasCustomSwapRecipient =\n activeMode === \"swap\" &&\n Boolean(recipientAddress) &&\n (!defaultRecipientAddress ||\n recipientAddress.toLowerCase() !== defaultRecipientAddress.toLowerCase());\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() ===\n ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\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 // Claim ownership of global singleton hook before executing SDK swap\n registerIntentHook(runId);\n\n const getSwapStepListFromEvent = (event: { args: any }) => {\n const args = (event as any).args;\n return Array.isArray(args)\n ? args\n : Array.isArray(args?.steps)\n ? args.steps\n : [];\n };\n\n const handleSwapEvent = (event: { name: string; args: any }) => {\n console.log(\"[NexusOne][SDK swap event]\", event.name, event);\n if (event.name === NEXUS_EVENTS.SWAP_STEPS_LIST) {\n const stepList = getSwapStepListFromEvent(event);\n if (stepList.length > 0) {\n swapStepsListRef.current = stepList as SwapStepType[];\n appendProgressListEvent(event.name, stepList);\n onStepsList(stepList);\n }\n return;\n }\n if (event.name === NEXUS_EVENTS.STEPS_LIST) {\n const args = (event as any).args;\n const stepList = Array.isArray(args)\n ? args\n : Array.isArray(args?.steps)\n ? args.steps\n : [];\n if (stepList.length > 0) {\n appendProgressListEvent(event.name, stepList);\n onStepsList(stepList);\n }\n return;\n }\n if (event.name === NEXUS_EVENTS.STEP_COMPLETE) {\n const step = event.args as BridgeStepType;\n appendProgressEvent(event.name, step, true);\n if (\n (step as any)?.type === \"TRANSACTION_SENT\" ||\n (step as any)?.type === \"TRANSACTION_CONFIRMED\"\n ) {\n markSwapExecutionStarted();\n }\n if ((step as any)?.data?.explorerURL) {\n mergeExplorerUrls({\n destinationExplorerUrl: (step as any).data.explorerURL,\n });\n }\n if ((step as any)?.completed !== false) {\n onStepComplete(step as any);\n }\n return;\n }\n if (event.name === \"SWAP_SKIPPED\") {\n const step =\n event.args && typeof event.args === \"object\"\n ? event.args\n : ({\n completed: true,\n data: event.args,\n type: \"SWAP_SKIPPED\",\n typeID: \"SWAP_SKIPPED\",\n } as unknown as SwapStepType);\n enterSkippedSwapProgress();\n appendProgressEvent(NEXUS_EVENTS.SWAP_STEP_COMPLETE, step, true);\n onStepComplete(step as SwapStepType);\n return;\n }\n if (event.name === NEXUS_EVENTS.SWAP_STEP_COMPLETE) {\n const step = event.args;\n const swapSkipped = isSwapSkippedStepType(getProgressStepType(step));\n if (swapSkipped) {\n enterSkippedSwapProgress();\n }\n appendProgressEvent(event.name, step, true);\n if (\n [\n \"SOURCE_SWAP_BATCH_TX\",\n \"SOURCE_SWAP_HASH\",\n \"BRIDGE_DEPOSIT\",\n \"RFF_ID\",\n \"DESTINATION_SWAP_BATCH_TX\",\n \"DESTINATION_SWAP_HASH\",\n \"SWAP_COMPLETE\",\n \"SWAP_SKIPPED\",\n ].includes(step?.type ?? \"\")\n ) {\n markSwapExecutionStarted();\n }\n if (step?.type === \"SOURCE_SWAP_HASH\" && step.explorerURL) {\n mergeExplorerUrls({ sourceExplorerUrl: step.explorerURL });\n }\n if (step?.type === \"DESTINATION_SWAP_HASH\" && step.explorerURL) {\n mergeExplorerUrls({ destinationExplorerUrl: step.explorerURL });\n }\n if (step?.type === \"BRIDGE_DEPOSIT\" && (step as any).data?.explorerURL) {\n mergeExplorerUrls({\n sourceExplorerUrl: (step as any).data.explorerURL,\n });\n }\n if (step?.type === \"RFF_ID\") {\n const nextIntentId = Number((step as any).data);\n if (Number.isFinite(nextIntentId) && nextIntentId > 0) {\n patchCurrentSwapHistoryEntry({ intentId: nextIntentId });\n }\n }\n if (step?.completed !== false) {\n onStepComplete(step);\n }\n }\n };\n\n try {\n if (!isExactOutFlow) {\n const fromPayload: {\n chainId: number;\n tokenAddress: `0x${string}`;\n amount: 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 = cleanAmount\n .toDecimalPlaces(Math.max(0, token.decimals || 18), Decimal.ROUND_DOWN)\n .toFixed();\n\n fromPayload.push({\n chainId: token.chainId!,\n tokenAddress: token.contractAddress as `0x${string}`,\n amount: nexusSDK.utils.parseUnits(\n safeTokenAmountStr,\n token.decimals || 18,\n ),\n });\n }\n\n if (fromPayload.length === 0) {\n throw new Error(\"No source amount available for swap.\");\n }\n\n resetExplorerUrls();\n // Start exact-in swap โ€” the intent hook will fire and populate preview\n const result = await nexusSDK.swapWithExactIn(\n {\n from: fromPayload,\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n },\n {\n onEvent: (event: any) => {\n if (swapRunIdRef.current !== runId) return;\n handleSwapEvent(event);\n },\n },\n );\n if (!result?.success) {\n throw new Error(result?.error || \"Swap failed\");\n }\n const intentExplorerUrl = result.result.explorerURL || null;\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ?? currentSwapEntry?.intentId;\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n finishCurrentSwapHistoryEntry(\"fulfilled\", {\n intentExplorerUrl,\n intentId,\n finalExplorerUrl:\n explorerUrlsRef.current.destinationExplorerUrl ||\n explorerUrlsRef.current.sourceExplorerUrl,\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 = nexusSDK.utils.parseUnits(\n exactOutAmountString,\n toToken.decimals || 18,\n );\n\n resetExplorerUrls();\n\n const fromSourcesPayload = buildFromSourcesPayload(\n getExactOutSourceTokens(),\n );\n\n const isNative =\n !toToken.contractAddress ||\n toToken.contractAddress.toLowerCase() ===\n \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\" ||\n toToken.contractAddress ===\n \"0x0000000000000000000000000000000000000000\";\n let executeConfig: any;\n if (activeMode === \"deposit\" && !selectedOpportunity?.execute) {\n throw new Error(\n \"Selected deposit opportunity is missing execute parameters.\",\n );\n }\n\n if (activeMode === \"deposit\" && selectedOpportunity?.execute) {\n executeConfig =\n typeof selectedOpportunity.execute === \"function\"\n ? selectedOpportunity.execute(\n amountBigInt,\n (ownerAddress ?? connectedAddress) as `0x${string}`,\n )\n : selectedOpportunity.execute;\n } else if (\n activeMode === \"send\" &&\n resolvedRecipientAddress\n ) {\n if (isNative) {\n executeConfig = {\n to: resolvedRecipientAddress as `0x${string}`,\n value: amountBigInt,\n gas: BigInt(100000),\n };\n } else {\n executeConfig = {\n to: toToken.contractAddress as `0x${string}`,\n data: encodeFunctionData({\n abi: erc20Abi,\n functionName: \"transfer\",\n args: [resolvedRecipientAddress as `0x${string}`, amountBigInt],\n }),\n gas: BigInt(100000),\n };\n }\n }\n\n if (executeConfig) {\n const onEvent = (event: any) => {\n if (swapRunIdRef.current !== runId) return;\n handleSwapEvent(event);\n };\n const sdkWithOptionalTransfer = nexusSDK as any;\n const result =\n activeMode === \"send\" &&\n typeof sdkWithOptionalTransfer.swapAndTransfer === \"function\"\n ? await sdkWithOptionalTransfer.swapAndTransfer(\n {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmount: amountBigInt,\n recipient: resolvedRecipientAddress as `0x${string}`,\n ...fromSourcesPayload,\n },\n { onEvent },\n )\n : await nexusSDK.swapAndExecute(\n {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmount: amountBigInt,\n execute: executeConfig,\n ...fromSourcesPayload,\n },\n { onEvent },\n );\n\n const swapResult = result?.swapResult ?? result?.result ?? null;\n const swapSkipped = Boolean((result as any)?.swapSkipped);\n if (!swapResult && !swapSkipped && activeMode !== \"send\") {\n throw new Error(\"Swap failed\");\n }\n const executeTxHash =\n result?.executeResponse?.txHash ||\n result?.transactionHash ||\n result?.txHash ||\n null;\n const intentExplorerUrl =\n swapResult?.explorerURL || result?.intentExplorerUrl || null;\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ?? currentSwapEntry?.intentId;\n const finalExplorerUrl =\n result?.explorerUrl ||\n result?.executeExplorerUrl ||\n getExplorerTxUrl(toToken.chainId, executeTxHash);\n if (finalExplorerUrl) {\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n patchCurrentSwapHistoryEntry({\n intentExplorerUrl,\n intentId,\n finalExplorerUrl,\n });\n } else {\n const result = await nexusSDK.swapWithExactOut(\n {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmount: amountBigInt,\n ...fromSourcesPayload,\n },\n {\n onEvent: (event: any) => {\n if (swapRunIdRef.current !== runId) return;\n handleSwapEvent(event);\n },\n },\n );\n if (!result?.success) {\n throw new Error(result?.error || \"Swap failed\");\n }\n const intentExplorerUrl = result.result.explorerURL || null;\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ?? currentSwapEntry?.intentId;\n patchCurrentSwapHistoryEntry({ intentExplorerUrl, intentId });\n }\n\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n finishCurrentSwapHistoryEntry(\"fulfilled\");\n resetInputsAfterSuccessfulExecution();\n onComplete?.();\n setSwapStep(\"success\");\n }\n }\n } catch (err: any) {\n console.error(\"Error in handleEnterPreview:\", err);\n if (swapRunIdRef.current !== runId) {\n return;\n }\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n const hasActiveExecution =\n swapStepRef.current === \"progress\" && Boolean(currentSwapIdRef.current);\n const showFailedProgressThenReceipt = (\n error: string,\n patch: Partial = {},\n ) => {\n const failedProgressEvent = progressEventsRef.current.at(-1);\n const fallbackFailedStep =\n activeMode === \"deposit\" || activeMode === \"send\"\n ? ({ type: \"APPROVAL\", typeID: \"AP\" } as BridgeStepType)\n : ({\n type: \"DETERMINING_SWAP\",\n typeID: \"DETERMINING_SWAP\",\n } as unknown as SwapStepType);\n const failedStep =\n failedProgressEvent?.step ?? fallbackFailedStep;\n const autoRefundAvailable =\n isAutoRefundAvailableProgressEvent(failedProgressEvent);\n setFailedProgressStep(failedStep);\n finishCurrentSwapHistoryEntry(\"failed\", {\n error,\n autoRefundAvailable,\n failureMessage: getFailureMessageForProgressStep(\n failedStep,\n 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 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 const errorMessage =\n err?.message ||\n (typeof err === \"string\"\n ? err\n : \"Transaction failed. Please try again or check console.\");\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 useEffect(() => {\n if (activeMode !== \"swap\" || swapStep !== \"idle\") return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = 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 clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n const timer = window.setTimeout(() => {\n void handleEnterPreview({ background: true });\n }, EXACT_OUT_INPUT_DEBOUNCE_MS);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [activeMode, amount, fromTokens, swapStep, toToken]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || swapStep !== \"idle\") return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = 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 clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n const timer = window.setTimeout(() => {\n void handleEnterPreview({ background: true });\n }, EXACT_OUT_INPUT_DEBOUNCE_MS);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n amount,\n depositAmountMode,\n sourceSelectionRevision,\n selectedOpportunity,\n swapStep,\n toToken,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"send\" || swapStep !== \"idle\") return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = 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 clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n const timer = window.setTimeout(() => {\n void handleEnterPreview({ background: true });\n }, EXACT_OUT_INPUT_DEBOUNCE_MS);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [activeMode, amount, sourceSelectionRevision, swapStep, toToken]);\n\n const refreshActiveSwapIntent = useCallback(async () => {\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 isPreviewRefresh = swapStepRef.current === \"preview-intent\";\n if (isPreviewRefresh) {\n setPreviewQuoteRefreshing(true);\n } else {\n setQuoteRefreshing(true);\n }\n try {\n const updated = await activeIntent.refresh();\n if (!updated || swapRunIdRef.current !== runId) return;\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 (swapRunIdRef.current === runId) {\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\" || activeMode === \"deposit\" || activeMode === \"send\") &&\n Boolean(intentData && swapIntentRef.current) &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n\n if (!hasRefreshableIntent) return;\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 (\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 intentData,\n intentLoading,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n refreshActiveSwapIntent,\n swapStep,\n ]);\n\n useEffect(() => {\n const hasRefreshableIntent =\n (activeMode === \"swap\" || activeMode === \"deposit\" || activeMode === \"send\") &&\n Boolean(intentData && swapIntentRef.current) &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n\n if (!hasRefreshableIntent) {\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 }, [activeMode, intentData, swapStep]);\n\n /** User accepted swap from the preview โ€” call allow() from the intent hook */\n const handleSwapAccept = () => {\n if (swapIntentRef.current) {\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 swapIntentRef.current.allow();\n // The swap promise in handleEnterPreview will resolve/reject\n }\n };\n\n // ---------------------------------------------------------------------------\n // Header title\n // ---------------------------------------------------------------------------\n const getTitle = () => {\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\") return \"Swap Failed\";\n return \"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\") return \"Deposit Failed\";\n return \"Deposit\";\n }\n if (activeMode === \"send\") {\n if (swapStep === \"progress\") return \"Sendingโ€ฆ\";\n if (swapStep === \"success\") return \"Send Complete\";\n if (swapStep === \"failed\") return \"Send Failed\";\n return \"Send\";\n }\n return \"Nexus One\";\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 !== \"idle\" &&\n swapStep !== \"choose-swap-asset\" &&\n swapStep !== \"choose-receive-asset\" &&\n swapStep !== \"enter-recipient\";\n const handleBack = () => {\n if (swapStep === \"history\") {\n setSwapStep(\"idle\");\n return;\n }\n if (swapStep === \"choose-swap-asset\") {\n closeDrawerToIdle();\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 = (\n val: string,\n panel: \"send\" | \"receive\",\n ) => {\n syncingIntentSourcesRef.current = false;\n setSwapQuoteIssue(null);\n setTxError(null);\n const nextAmount = parseFiatNumber(val);\n const hasSelectedSourceToken = fromTokens.some(\n (token) => token.chainId && token.contractAddress,\n );\n const shouldLoadQuote = Boolean(\n nextAmount?.gt(0) && toToken && hasSelectedSourceToken,\n );\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\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 One 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 handleDepositAmountChange = (val: string) => {\n syncingIntentSourcesRef.current = false;\n setExactOutQuoteSourceModeValue(\"all\");\n maxPercentRunRef.current += 1;\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setSwapQuoteIssue(null);\n const nextAmount = parseFiatNumber(val);\n const shouldLoadQuote = Boolean(\n nextAmount?.gt(0) && toToken && selectedOpportunity,\n );\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n } else {\n clearSelectedSources();\n }\n setAmount(val);\n };\n\n const handleSendAmountChange = (val: string) => {\n syncingIntentSourcesRef.current = false;\n setExactOutQuoteSourceModeValue(\"all\");\n maxPercentRunRef.current += 1;\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setSwapQuoteIssue(null);\n setSwapType(\"exactOut\");\n const nextAmount = parseFiatNumber(val);\n const shouldLoadQuote = Boolean(nextAmount?.gt(0) && toToken);\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n } else {\n clearSelectedSources();\n }\n setAmount(val);\n };\n\n const handleDepositAmountModeToggle = () => {\n syncingIntentSourcesRef.current = false;\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.div(rate).toDecimalPlaces(toToken?.decimals ?? 18);\n setAmount(converted.toFixed());\n }\n clearPendingSwapIntent();\n setDepositAmountMode((current) => (current === \"token\" ? \"usd\" : \"token\"));\n };\n\n const handleDepositPercentSelect = async (pct: number) => {\n if (!toToken) return;\n\n syncingIntentSourcesRef.current = false;\n setTxError(null);\n setSwapQuoteIssue(null);\n const runId = ++maxPercentRunRef.current;\n\n if (pct !== 100) {\n const usdAmount = getTotalBalancePercentUsdAmount(pct);\n const shouldUseMaxQuoteFallback =\n depositAmountMode === \"usd\" && getDepositTokenUsdRate().lte(0);\n const nextAmount =\n depositAmountMode === \"usd\"\n ? usdAmount.toDecimalPlaces(2, Decimal.ROUND_DOWN).toFixed()\n : formatTokenAmountFromUsd(usdAmount, toToken);\n\n if (nextAmount && !shouldUseMaxQuoteFallback) {\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleDepositAmountChange(nextAmount);\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(true);\n setMaxCalculationPercent(pct);\n try {\n await waitForNextPaint();\n const fallback = await getPercentAmountFromMaxQuote(\n toToken,\n pct,\n depositAmountMode === \"usd\",\n );\n if (runId !== maxPercentRunRef.current) return;\n if (!fallback) {\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 setDepositAmountMode(fallback.mode);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleDepositAmountChange(fallback.amount);\n } catch (error: any) {\n if (runId !== maxPercentRunRef.current) return;\n console.error(\"Unable to calculate percentage deposit amount\", error);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n if (isInsufficientSourcesError(error)) {\n setSwapQuoteIssue(buildInsufficientSourcesIssue(error));\n return;\n }\n setTxError(\n error?.message || \"Unable to calculate this percentage for the deposit asset.\",\n );\n }\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(true);\n setMaxCalculationPercent(100);\n try {\n await waitForNextPaint();\n const maxAmount = await getPercentAmountFromMaxQuote(\n toToken,\n 100,\n depositAmountMode === \"usd\",\n );\n if (runId !== maxPercentRunRef.current) return;\n if (!maxAmount) {\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n setTxError(\"No depositable amount is available for this opportunity.\");\n return;\n }\n\n setDepositAmountMode(maxAmount.mode);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleDepositAmountChange(maxAmount.amount);\n } catch (error: any) {\n if (runId !== maxPercentRunRef.current) return;\n console.error(\"Unable to calculate max deposit amount\", error);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n if (isInsufficientSourcesError(error)) {\n setSwapQuoteIssue(buildInsufficientSourcesIssue(error));\n return;\n }\n setTxError(\n error?.message || \"Unable to calculate the max deposit amount.\",\n );\n }\n };\n\n const handleSendPercentSelect = async (pct: number) => {\n if (!toToken) return;\n\n syncingIntentSourcesRef.current = false;\n setTxError(null);\n setSwapQuoteIssue(null);\n const runId = ++maxPercentRunRef.current;\n\n if (pct !== 100) {\n const usdAmount = getTotalBalancePercentUsdAmount(pct);\n const nextAmount = formatTokenAmountFromUsd(usdAmount, toToken);\n\n if (nextAmount) {\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleSendAmountChange(nextAmount);\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(true);\n setMaxCalculationPercent(pct);\n try {\n await waitForNextPaint();\n const fallback = await getPercentAmountFromMaxQuote(toToken, pct, false);\n if (runId !== maxPercentRunRef.current) return;\n if (!fallback) {\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 setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleSendAmountChange(fallback.amount);\n } catch (error: any) {\n if (runId !== maxPercentRunRef.current) return;\n console.error(\"Unable to calculate percentage send amount\", error);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n if (isInsufficientSourcesError(error)) {\n setSwapQuoteIssue(buildInsufficientSourcesIssue(error));\n return;\n }\n setTxError(\n error?.message || \"Unable to calculate this percentage for the send asset.\",\n );\n }\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(true);\n setMaxCalculationPercent(100);\n try {\n await waitForNextPaint();\n const maxAmount = await getPercentAmountFromMaxQuote(toToken, 100, false);\n if (runId !== maxPercentRunRef.current) return;\n if (!maxAmount) {\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n setTxError(\"No transferable amount is available for this asset.\");\n return;\n }\n\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleSendAmountChange(maxAmount.amount);\n } catch (error: any) {\n if (runId !== maxPercentRunRef.current) return;\n console.error(\"Unable to calculate max send amount\", error);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n if (isInsufficientSourcesError(error)) {\n setSwapQuoteIssue(buildInsufficientSourcesIssue(error));\n return;\n }\n setTxError(error?.message || \"Unable to calculate the max send amount.\");\n }\n };\n\n // ---------------------------------------------------------------------------\n // Render\n // ---------------------------------------------------------------------------\n const exactOutInsufficientSourceIssue =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n swapQuoteIssue?.type === \"insufficientSources\"\n ? swapQuoteIssue\n : null;\n const isExactOutRouteLoading =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n swapStep === \"idle\" &&\n swapType === \"exactOut\" &&\n Boolean(toToken && (receiveMaxCalculating || (amount && Number(amount) > 0))) &&\n !exactOutInsufficientSourceIssue &&\n (quoteRefreshing || intentLoading || receiveMaxCalculating);\n const hasCurrentRunnableIntent =\n Boolean(intentData && swapIntentRef.current) &&\n swapIntentRef.current?.runId === swapRunIdRef.current &&\n !intentLoading;\n const hasIntentSources = Boolean((intentData?.sources ?? []).length > 0);\n const isQuoteUnavailableForAutoSourceFlow =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n Boolean(hasPositiveDecimalInput(amount) && toToken) &&\n !quoteRefreshing &&\n !receiveMaxCalculating &&\n !intentLoading &&\n !exactOutInsufficientSourceIssue &&\n (!hasCurrentRunnableIntent || !hasIntentSources);\n const hasPositiveRootAmount = hasPositiveDecimalInput(amount);\n const hasReadySwapQuoteInput = hasReadyExactInSwapInput(fromTokens, toToken);\n const isSwapCtaDisabled =\n !hasReadySwapQuoteInput ||\n receiveMaxCalculating ||\n quoteRefreshing ||\n Boolean(exactOutInsufficientSourceIssue);\n const isDepositCtaDisabled =\n !hasPositiveRootAmount ||\n !toToken ||\n quoteRefreshing ||\n receiveMaxCalculating ||\n isQuoteUnavailableForAutoSourceFlow ||\n Boolean(exactOutInsufficientSourceIssue);\n const sendNeedsRecipient = activeMode === \"send\" && !recipientAddress;\n const isSendCtaDisabled =\n !hasPositiveRootAmount ||\n !toToken ||\n hasSameOwnerSendRecipient ||\n receiveMaxCalculating ||\n (!sendNeedsRecipient &&\n (quoteRefreshing || isQuoteUnavailableForAutoSourceFlow)) ||\n Boolean(exactOutInsufficientSourceIssue);\n const quoteCtaLabel = (fallback: string) =>\n exactOutInsufficientSourceIssue\n ? \"Insufficient balance\"\n : receiveMaxCalculating\n ? \"Calculating...\"\n : quoteRefreshing\n ? \"Fetching quotes...\"\n : isQuoteUnavailableForAutoSourceFlow\n ? \"Quote unavailable\"\n : !hasPositiveRootAmount\n ? \"Enter amount\"\n : fallback;\n const sendCtaLabel =\n exactOutInsufficientSourceIssue\n ? \"Insufficient balance\"\n : !hasPositiveRootAmount\n ? \"Enter amount\"\n : !toToken\n ? \"Select token\"\n : hasSameOwnerSendRecipient\n ? \"Change recipient\"\n : sendNeedsRecipient\n ? \"Add recipient\"\n : quoteCtaLabel(\"Review send\");\n const previewIntentSourceUsdNumber = (intentData?.sources ?? []).reduce(\n (sum, source) => sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0),\n );\n const previewSourceUsdNumber =\n 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 totalSwapBalanceUsd = getSwapBalanceTotalUsd()\n .toDecimalPlaces(2)\n .toFixed();\n const sendAmountUsd =\n amount && toToken\n ? getTokenUsdValue(\n {\n ...toToken,\n userAmount: amount,\n userAmountMode: \"token\",\n },\n amount,\n ).toNumber()\n : 0;\n const resolvedToToken =\n toToken ??\n (activeMode === \"deposit\" && selectedOpportunity\n ? toTokenFromOpportunity(selectedOpportunity)\n : undefined);\n const toTokenWithFetchedBalance =\n resolvedToToken && destinationBalance\n ? { ...resolvedToToken, balance: destinationBalance }\n : resolvedToToken;\n const isIdleSwapQuoteLoading =\n activeMode === \"swap\" && swapStep === \"idle\" && quoteRefreshing;\n const isReceiveAmountLoading =\n receiveMaxCalculating ||\n (isIdleSwapQuoteLoading && swapType === \"exactIn\" && !intentToAmount);\n const isReceiveUsdLoading =\n receiveMaxCalculating ||\n (isIdleSwapQuoteLoading && swapType === \"exactIn\" && !previewToAmountUsd);\n const hasQuoteRefreshCountdown =\n (activeMode === \"swap\" || activeMode === \"deposit\" || activeMode === \"send\") &&\n Boolean(intentData && swapIntentRef.current) &&\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 isDrawerOverlayActive =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n swapStep === \"enter-recipient\" ||\n closingDrawerStep !== null;\n\n return (\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 {/* Protocol chip appended next to Title when Deposit Protocol selected */}\n {isTitleCentered() &&\n activeMode === \"deposit\" &&\n swapStep === \"idle\" &&\n selectedOpportunity && (\n
\n {\n clearPendingSwapIntent();\n setSelectedOpportunity(undefined);\n setToToken(undefined);\n clearSelectedSources();\n setAmount(\"\");\n setDepositAmountMode(\"token\");\n }}\n className=\"flex items-center gap-1 pl-2 pr-1.5 py-1 rounded-[4px] hover:bg-black/5 transition-colors\"\n style={{\n fontFamily: \"var(--font-geist-mono), sans-serif\",\n fontSize: \"10px\",\n fontWeight: 500,\n color: \"var(--foreground-muted, #848483)\",\n background: \"var(--background-tertiary, #F0F0EF)\",\n border: \"none\",\n cursor: \"pointer\",\n }}\n >\n {selectedOpportunity.title || selectedOpportunity.protocol}\n \n \n
\n )}\n \n\n {/* Right side icons */}\n \n {hasQuoteRefreshCountdown && (\n \n )}\n setSwapStep(\"history\")}\n style={{\n alignItems: \"center\",\n backgroundColor: \"#FFFFFE\",\n borderRadius: \"8px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n height: \"32px\",\n justifyContent: \"center\",\n outline: \"1px solid #E8E8E7\",\n width: \"32px\",\n cursor: \"pointer\",\n border: \"none\",\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 />\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 fromTokens={fromTokens}\n toToken={toTokenWithFetchedBalance}\n receiveQuoteUsd={previewToAmountUsd}\n sourceRouteStatus={\n exactOutInsufficientSourceIssue\n ? \"insufficient\"\n : isExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n sourceRouteMessage={exactOutInsufficientSourceIssue?.message}\n totalBalance={totalSwapBalanceUsd}\n usdValue={amount && usdValue > 0 ? usdValue.toFixed(2) : \"\"}\n swapType={swapType}\n onOpenSourcePicker={(index) => {\n setEditingAssetIndex(index ?? null);\n openDrawerStep(\"choose-swap-asset\");\n }}\n onOpenDestPicker={() => openDrawerStep(\"choose-receive-asset\")}\n onOpenRecipientPicker={handleOpenRecipientEditor}\n recipientAddress={effectiveRecipientAddress}\n defaultRecipientAddress={defaultRecipientAddress}\n onUpdateTokens={setFromTokens}\n />\n\n {txError && !exactOutInsufficientSourceIssue && (\n \n )}\n\n {/* CTA Button */}\n \n void handleEnterPreview()}\n disabled={isSwapCtaDisabled}\n style={{\n alignItems: \"center\",\n backgroundColor: exactOutInsufficientSourceIssue\n ? \"#FCEEED\"\n : isSwapCtaDisabled\n\t ? \"#F0F0EF\"\n\t : \"#006BF4\",\n\t border: exactOutInsufficientSourceIssue\n\t ? \"1px solid #F7C4C1\"\n\t : \"none\",\n\t borderRadius: exactOutInsufficientSourceIssue ? \"4px\" : \"8px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"8px\",\n height: \"48px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n\t cursor: isSwapCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {exactOutInsufficientSourceIssue ? (\n \n ) : (quoteRefreshing || receiveMaxCalculating) ? (\n \n ) : null}\n \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 {/* Opportunity list */}\n {config.opportunities &&\n config.opportunities.length > 0 &&\n !selectedOpportunity && (\n <>\n \n\n {/* Done button for opportunity selection */}\n \n {\n const opportunity =\n pendingOpportunity ?? config.opportunities?.[0];\n if (opportunity) {\n handleSelectDepositOpportunity(opportunity);\n setSwapStep(\"idle\");\n }\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: \"#006BF4\",\n borderRadius: \"8px\",\n boxShadow: \"#5555550D 0px 1px 4px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flex: 1,\n height: \"48px\",\n justifyContent: \"center\",\n border: \"none\",\n cursor: \"pointer\",\n }}\n >\n \n Done\n \n \n \n \n )}\n\n {/* After opportunity selected โ€” show deposit form */}\n {(!config.opportunities ||\n config.opportunities.length === 0 ||\n selectedOpportunity) && (\n <>\n openDrawerStep(\"choose-swap-asset\")}\n onSetPercent={handleDepositPercentSelect}\n routeStatus={\n exactOutInsufficientSourceIssue\n ? \"insufficient\"\n : isExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n routeMessage={exactOutInsufficientSourceIssue?.message}\n isCalculatingMax={receiveMaxCalculating}\n calculatingPercent={maxCalculationPercent}\n isQuoteRefreshing={quoteRefreshing || intentLoading}\n showAutoBadge={!sourceSelectionTouched}\n />\n\n {txError && !exactOutInsufficientSourceIssue && (\n \n )}\n\n \n void handleEnterPreview()}\n disabled={isDepositCtaDisabled}\n style={{\n alignItems: \"center\",\n backgroundColor: exactOutInsufficientSourceIssue\n ? \"#FCEEED\"\n : isDepositCtaDisabled\n\t ? \"#F0F0EF\"\n\t : \"#006BF4\",\n\t border: exactOutInsufficientSourceIssue\n\t ? \"1px solid #F7C4C1\"\n\t : \"none\",\n\t borderRadius: exactOutInsufficientSourceIssue ? \"4px\" : \"8px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"8px\",\n height: \"48px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n\t cursor: isDepositCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {exactOutInsufficientSourceIssue ? (\n \n ) : quoteRefreshing || 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 0 ? sendAmountUsd.toFixed(2) : \"\"\n }\n onOpenAssetPicker={() => openDrawerStep(\"choose-receive-asset\")}\n onOpenSourcePicker={() => {\n setEditingAssetIndex(null);\n openDrawerStep(\"choose-swap-asset\");\n }}\n onOpenRecipientPicker={handleOpenRecipientEditor}\n recipientAddress={recipientAddress || \"\"}\n onSetPercent={handleSendPercentSelect}\n routeStatus={\n exactOutInsufficientSourceIssue\n ? \"insufficient\"\n : isExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n routeMessage={exactOutInsufficientSourceIssue?.message}\n isCalculatingMax={receiveMaxCalculating}\n calculatingPercent={maxCalculationPercent}\n isQuoteRefreshing={quoteRefreshing}\n showAutoBadge={!sourceSelectionTouched}\n />\n\n {txError && !exactOutInsufficientSourceIssue && (\n \n )}\n\n \n {\n if (sendNeedsRecipient) {\n handleOpenRecipientEditor();\n return;\n }\n void handleEnterPreview();\n }}\n disabled={isSendCtaDisabled}\n style={{\n alignItems: \"center\",\n backgroundColor: exactOutInsufficientSourceIssue\n ? \"#FCEEED\"\n : isSendCtaDisabled\n\t ? \"#F0F0EF\"\n\t : \"#006BF4\",\n\t border: exactOutInsufficientSourceIssue\n\t ? \"1px solid #F7C4C1\"\n\t : \"none\",\n\t borderRadius: exactOutInsufficientSourceIssue ? \"4px\" : \"8px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"8px\",\n height: \"48px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n\t cursor: isSendCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {exactOutInsufficientSourceIssue ? (\n \n ) : !sendNeedsRecipient && (quoteRefreshing || 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 />\n \n \n \n \n \n {\n setTxError(null);\n closeDrawerToIdle();\n }}\n aria-label=\"Back\"\n style={{\n alignItems: \"center\",\n backgroundColor: \"#FFFFFE\",\n border: \"1px solid #E8E8E7\",\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 >\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 label={null}\n placeholder=\"Wallet address\"\n hasError={Boolean(txError)}\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 0\n ? sendAmountUsd.toFixed(2)\n : undefined\n }\n selectedTokens={fromTokens}\n editingAssetIndex={editingAssetIndex}\n onSelectionChange={\n activeMode === \"deposit\" || activeMode === \"send\"\n ? (tokens) => {\n setSourceSelectionTouched(true);\n setExactOutQuoteSourceModeValue(\"selected\");\n invalidateExactOutQuoteForRefresh();\n setSourceSelectionRevision((current) => current + 1);\n setFromTokens(\n tokens.map((token) => ({\n ...token,\n userAmount: \"\",\n })),\n );\n }\n : undefined\n }\n onClearSelection={\n activeMode === \"deposit\" || activeMode === \"send\"\n ? () => {\n setSourceSelectionTouched(true);\n setExactOutQuoteSourceModeValue(\"selected\");\n invalidateExactOutQuoteForRefresh();\n setSourceSelectionRevision((current) => current + 1);\n setFromTokens((current) =>\n current.length === 0 ? current : [],\n );\n }\n : undefined\n }\n onToggle={(token) => {\n if (activeMode === \"deposit\" || activeMode === \"send\") {\n setSourceSelectionTouched(true);\n setExactOutQuoteSourceModeValue(\"selected\");\n invalidateExactOutQuoteForRefresh();\n setSourceSelectionRevision((current) => current + 1);\n } else {\n clearPendingSwapIntent();\n }\n setFromTokens((prev) => {\n const isSameSelection = (a: SwapTokenOption, b: SwapTokenOption) => {\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 isDepositOrSendSourcePicker =\n activeMode === \"deposit\" || activeMode === \"send\";\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\" && prev.length === 0\n ? amount\n : \"\",\n });\n\n if (\n isDepositOrSendSourcePicker &&\n token.isUnified &&\n sourceTokens.length > 0\n ) {\n const hasUnifiedSelection = prev.some(isSameUnifiedGroup);\n const areAllChildrenSelected = sourceTokens.every((source) =>\n prev.some((item) => isSameSelection(item, source)),\n );\n const withoutGroup = prev.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) => withDefaultAmount(source)),\n ];\n }\n\n if (isDepositOrSendSourcePicker && !token.isUnified) {\n const unifiedSelection = prev.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 = prev.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 = prev.find((item) =>\n isSameSelection(item, token),\n );\n if (exists) {\n return prev.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 = prev.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 [\n ...next,\n withDefaultAmount(token),\n ];\n });\n }}\n onDone={closeDrawerToIdle}\n onSelect={(token) => {\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 setSourceSelectionTouched(true);\n setExactOutQuoteSourceModeValue(\"selected\");\n invalidateExactOutQuoteForRefresh();\n setSourceSelectionRevision((current) => current + 1);\n setFromTokens([{ ...token, userAmount: amount }]);\n closeDrawerToIdle();\n }\n }}\n onBack={closeDrawerToIdle}\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 (activeMode === \"send\" || activeMode === \"deposit\") {\n setExactOutQuoteSourceModeValue(\"all\");\n if (tokenChanged) {\n clearPendingSwapIntent();\n setAmount(\"\");\n }\n setSwapType(\"exactOut\");\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 onBack={closeDrawerToIdle}\n />\n \n \n )}\n\n \n );\n}\n\nexport default NexusOne;\n", + "content": "\"use client\";\n\nimport React, {\n useState,\n useRef,\n useEffect,\n useCallback,\n useLayoutEffect,\n useMemo,\n} from \"react\";\nimport {\n type NexusOneProps,\n type NexusOneMode,\n type SwapType,\n type DepositOpportunity,\n} from \"./types\";\nimport { SwapIdleForm } from \"./components/swap-idle-form\";\nimport { SendIdleForm } from \"./components/send-idle-form\";\nimport { DepositIdleForm } from \"./components/deposit-idle-form\";\nimport { RecipientInput } from \"./components/recipient-input\";\nimport { StatusAlert } from \"./components/status-alerts\";\nimport {\n SwapAssetSelector,\n type SwapTokenOption,\n deriveTokenOptions,\n} from \"./components/swap-asset-selector\";\nimport {\n SwapIntentPreview,\n type SwapIntentData,\n} from \"./components/swap-intent-preview\";\nimport {\n NexusOneProgressScreen,\n type NexusOneProgressEvent,\n} from \"./components/nexus-one-progress-screen\";\nimport { ReceiveAssetSelector, preloadReceiveTokens } from \"./components/receive-asset-selector\";\nimport { OpportunityList } from \"./components/opportunity-list\";\nimport { AlertCircle, ArrowLeft, ChevronDown, Loader2 } from \"lucide-react\";\nimport { useNexus } from \"../nexus/NexusProvider\";\nimport { useTransactionSteps } from \"../common/tx/useTransactionSteps\";\nimport { findCitreaReceiveToken } from \"./utils/citrea-tokens\";\nimport {\n CHAIN_METADATA,\n ERROR_CODES,\n NEXUS_EVENTS,\n type BridgeStepType,\n type EthereumProvider,\n type SwapStepType,\n TOKEN_CONTRACT_ADDRESSES,\n TOKEN_METADATA,\n} from \"@avail-project/nexus-core\";\nimport {\n useAccount,\n useConnect,\n useConnectorClient,\n useWalletClient,\n usePublicClient,\n} from \"wagmi\";\nimport {\n erc20Abi,\n isAddress,\n zeroAddress,\n createPublicClient,\n http,\n encodeFunctionData,\n} from \"viem\";\nimport { normalize } from \"viem/ens\";\nimport { mainnet } from \"viem/chains\";\nimport Decimal from \"decimal.js\";\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 SwapHistoryStatus =\n | \"pending\"\n | \"fulfilled\"\n | \"failed\"\n | \"refund-initiated\";\n\ninterface SwapHistoryEntry {\n id: string;\n mode: NexusOneMode;\n status: SwapHistoryStatus;\n createdAt: number;\n startedAt: number;\n endedAt?: number;\n durationSeconds?: number;\n intentData: SwapIntentData | null;\n fromTokens: SwapTokenOption[];\n toToken?: SwapTokenOption;\n requestedToAmount?: string;\n requestedToValue?: string;\n recipientAddress?: string;\n opportunity?: DepositOpportunity;\n feeUsd?: string;\n intentId?: number;\n intentExplorerUrl?: string | null;\n sourceExplorerUrl?: string | null;\n finalExplorerUrl?: string | null;\n error?: string;\n failureMessage?: string;\n failedStepType?: string;\n autoRefundAvailable?: boolean;\n}\n\ntype SwapQuoteIssue = {\n type: \"insufficientSources\";\n message: string;\n missingUsd?: string;\n};\n\ntype CachedMaxSwapQuote = {\n decimals: number;\n maxTokenAmount: Decimal;\n maxUsdAmount?: Decimal;\n symbol: string;\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 PredictiveQuoteBaseline = {\n destinationUsdRate: string;\n exactInDestinationAmountPerSourceUsd?: string;\n exactOutSourceUsdPerDestinationUsd?: string;\n updatedAt: number;\n};\n\nconst QUOTE_REFRESH_INTERVAL_MS = 30000;\nconst EXACT_OUT_INPUT_DEBOUNCE_MS = 1000;\nconst DRAWER_CLOSE_MS = 220;\nconst MODAL_HEIGHT_TRANSITION_MS = 260;\nconst BASIS_POINTS = 10000;\nconst PREDICTIVE_EXACT_IN_DISCOUNT_BPS = 50;\nconst PREDICTIVE_EXACT_OUT_BUFFER_BPS = 100;\nconst PREDICTIVE_QUOTE_DISPLAY_DECIMALS = 8;\nconst SWAP_HISTORY_STORAGE_KEY_PREFIX = \"nexus-one-transaction-history-v1\";\nconst waitForNextPaint = () =>\n new Promise((resolve) => {\n if (typeof window === \"undefined\" || !window.requestAnimationFrame) {\n resolve();\n return;\n }\n window.requestAnimationFrame(() => {\n window.setTimeout(() => resolve(), 0);\n });\n });\nconst tooltipSurface = \"#FFFFFE\";\nconst tooltipText = \"var(--foreground-primary, #161615)\";\nconst tooltipBorder = \"var(--border-default, #E8E8E7)\";\nconst uiFont = '\"Geist\", var(--font-geist-sans), system-ui, sans-serif';\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`;\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 isSameTokenSelection = (\n a?: SwapTokenOption | null,\n b?: SwapTokenOption | null,\n) => Boolean(a && b && getTokenSelectionKey(a) === getTokenSelectionKey(b));\n\nconst sanitizeOpportunityForHistory = (\n opportunity?: DepositOpportunity,\n): DepositOpportunity | undefined => {\n if (!opportunity) return undefined;\n return {\n id: opportunity.id,\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 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 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 === \"refund-initiated\";\n\nconst isStoredMode = (value: unknown): value is NexusOneMode =>\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 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 = sortSwapHistoryEntries(entries).map(\n sanitizeHistoryEntry,\n );\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(true)}\n onMouseLeave={() => setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onBlur={() => setShowTooltip(false)}\n tabIndex={0}\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 >\n {showTooltip && (\n \n {tooltipLabel}\n \n )}\n \n \n \n \n \n );\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 = 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 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 const max = amount.abs().gte(1) ? 6 : 8;\n return formatDecimalDisplay(amount, { max });\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 hasValidIntentExplorer = (entry: Pick) =>\n Boolean(\n entry.intentExplorerUrl &&\n entry.intentId !== undefined &&\n Number.isFinite(entry.intentId) &&\n entry.intentId > 0,\n );\n\nconst getExplorerTxUrl = (chainId?: number, txHash?: string | null) => {\n if (!chainId || !txHash) return null;\n const chainMeta = CHAIN_METADATA[chainId];\n const baseUrl =\n (chainMeta as any)?.blockExplorerUrls?.[0] ||\n (chainMeta as any)?.blockExplorers?.default?.url;\n return baseUrl ? `${String(baseUrl).replace(/\\/$/, \"\")}/tx/${txHash}` : null;\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 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 size = 34,\n}: {\n tokenLogo?: string;\n chainLogo?: string;\n tokenSymbol?: string;\n chainName?: string;\n size?: number;\n}) {\n return (\n
\n \n {chainLogo && (\n \n )}\n
\n );\n}\n\nfunction TruncatedAddress({\n address,\n color = \"#006BF4\",\n}: {\n address: string;\n color?: string;\n}) {\n const [showTooltip, setShowTooltip] = useState(false);\n const label =\n address.length > 12 ? `${address.slice(0, 6)}...${address.slice(-4)}` : address;\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n tabIndex={0}\n style={{\n color,\n display: \"inline-flex\",\n fontFamily: uiFont,\n fontSize: \"13px\",\n fontWeight: 500,\n lineHeight: \"18px\",\n outline: \"none\",\n position: \"relative\",\n }}\n >\n {label}\n {showTooltip && (\n \n {address}\n \n )}\n \n );\n}\n\nconst getDisplayDestinationSourceRow = (entry: SwapHistoryEntry) => {\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(entry.intentData?.destination.amount);\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(entry.intentData?.destination.value);\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 tokenLogo: entry.toToken.logo,\n chainLogo: entry.toToken.chainLogo,\n symbol: entry.toToken.symbol,\n chainName: entry.toToken.chainName || \"\",\n amount: displayAmount\n .toDecimalPlaces(Math.max(0, entry.toToken.decimals ?? 18), Decimal.ROUND_DOWN)\n .toFixed(),\n value: rate ? displayAmount.mul(rate).toFixed() : entry.toToken.balanceInFiat,\n };\n};\n\nconst getProgressStepType = (\n step?: SwapStepType | BridgeStepType | null,\n) => String((step as any)?.type ?? (step as any)?.typeID ?? \"\").toUpperCase();\n\nconst isBridgeRefundStepType = (type: string) =>\n type.includes(\"RFF_ID\") || type.includes(\"BRIDGE_DEPOSIT\");\n\nconst isSwapSkippedStepType = (type: string) =>\n type.includes(\"SWAP_SKIPPED\");\n\nconst isAutoRefundAvailableProgressEvent = (\n event?: NexusOneProgressEvent,\n) =>\n event?.name === NEXUS_EVENTS.SWAP_STEP_COMPLETE &&\n isBridgeRefundStepType(getProgressStepType(event.step));\n\nconst getFailureMessageForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n mode: NexusOneMode,\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(\"SOURCE_SWAP\") ||\n type.includes(\"COLLECTION\")\n ) {\n return \"Collection Failed\";\n }\n if (\n type.includes(\"DESTINATION_SWAP\") ||\n type.includes(\"FULFIL\")\n ) {\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(\"RFF\") ||\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 getSourceRows = (entry: SwapHistoryEntry) => {\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 tokenLogo: fallback?.logo,\n chainLogo: source.chain.logo || fallback?.chainLogo,\n symbol: source.token.symbol,\n chainName: source.chain.name,\n amount: source.amount,\n value: source.value,\n };\n });\n\n return displayDestinationSourceRow\n ? [displayDestinationSourceRow, ...sourceRows]\n : sourceRows;\n }\n\n const fallbackRows = entry.fromTokens.map((token, index) => ({\n key: `${token.chainId}-${token.contractAddress}-${index}`,\n tokenLogo: token.logo,\n chainLogo: token.chainLogo,\n symbol: token.symbol,\n chainName: token.chainName || \"\",\n amount: token.userAmount || \"0\",\n value: token.balanceInFiat,\n }));\n\n return displayDestinationSourceRow\n ? [displayDestinationSourceRow, ...fallbackRows]\n : fallbackRows;\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 scrollRef.current?.scrollBy({ top: 72, behavior: \"smooth\" })}\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 >\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 isDeposit = entry.mode === \"deposit\";\n const isSend = entry.mode === \"send\";\n const tokenSymbol = destination?.token.symbol || entry.toToken?.symbol || \"\";\n const chainName = destination?.chain.name || entry.toToken?.chainName || \"\";\n const depositVenue =\n entry.opportunity?.title || entry.opportunity?.protocol || chainName;\n const amount = destination?.amount || \"\";\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 = `Intent #${entry.intentId}`;\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 storedFailureMessage =\n !entry.autoRefundAvailable && entry.failureMessage?.includes(\"Refund\")\n ? undefined\n : entry.failureMessage;\n const failureHeadline =\n storedFailureMessage ||\n (isDeposit\n ? \"Deposit failed. Funds are in your wallet\"\n : isSend\n ? \"Send failed. Funds are in your wallet\"\n : defaultSwapFailureHeadline);\n const receiptLocation = isDeposit ? depositVenue : chainName;\n const receiptSummary = receiptLocation ? `on ${receiptLocation}` : \"\";\n\n return (\n
\n \n \n \n \n {isFailed ? \"x\" : \"โœ“\"}\n
\n \n
\n {isFailed\n ? failureHeadline\n : isDeposit\n ? \"You deposited\"\n : isSend\n ? \"You sent\"\n : \"You received\"}\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: \"#006BF4\",\n cursor: \"pointer\",\n display: \"inline-flex\",\n fontFamily: uiFont,\n fontSize: \"12px\",\n gap: \"4px\",\n padding: 0,\n }}\n >\n {showSourceDetails ? \"Hide Details\" : `${sourceCount} asset${sourceCount === 1 ? \"\" : \"s\"}`}\n \n \n \n \n \n
\n \n
\n \n {isSend && 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({\n status,\n}: {\n status: SwapHistoryStatus;\n}) {\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 === \"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 onRefund,\n}: {\n entries: SwapHistoryEntry[];\n now: number;\n onRefund: (entry: SwapHistoryEntry) => void;\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 swap,\n 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 = entry.toToken?.logo;\n const destinationChainLogo =\n destination?.chain.logo || entry.toToken?.chainLogo || \"\";\n const destinationChainName =\n destination?.chain.name || entry.toToken?.chainName || \"\";\n const destinationSymbol = 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 showIntentExplorer = hasValidIntentExplorer(entry);\n const viewUrl = showIntentExplorer\n ? entry.intentExplorerUrl\n : entry.finalExplorerUrl;\n const canShowRefund =\n entry.status === \"failed\" &&\n Boolean(entry.autoRefundAvailable);\n const status = canShowRefund ? \"refund-initiated\" : entry.status;\n const sourceRows = getSourceRows(entry);\n const firstSource = sourceRows[0];\n\n return (\n \n
\n
\n \n
\n
\n {destinationAmount ? formatTokenDisplay(destinationAmount) : \"--\"}\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 onRefund(entry)}\n style={{\n background: \"#006BF4\",\n border: \"none\",\n borderRadius: \"8px\",\n color: \"#FFFFFE\",\n cursor: entry.intentId ? \"pointer\" : \"not-allowed\",\n fontFamily: uiFont,\n fontSize: \"13px\",\n fontWeight: 600,\n opacity: entry.intentId ? 1 : 0.5,\n padding: \"8px 14px\",\n }}\n >\n Refund\n \n \n )}\n\n \n
\n {firstSource && (\n \n )}\n \n โ†’\n \n \n {showIntentExplorer ? (\n \n Intent #{entry.intentId}\n \n ) : entry.finalExplorerUrl ? (\n \n Final transaction\n \n ) : null}\n
\n {viewUrl && (\n \n View โ†—\n \n )}\n \n \n );\n })}\n \n );\n}\n\n// ---------------------------------------------------------------------------\n// NexusOne\n// ---------------------------------------------------------------------------\n\nexport function NexusOne({\n config,\n embed = true,\n connectedAddress,\n onComplete,\n onStart,\n onError,\n onClose,\n}: NexusOneProps) {\n const {\n nexusSDK,\n bridgableBalance,\n swapBalance,\n getFiatValue,\n resolveTokenUsdRate,\n swapSupportedChainsAndTokens,\n supportedChainsAndTokens,\n fetchSwapBalance,\n handleInit,\n loading: nexusLoading,\n } = useNexus();\n\n // Mode is a single value, not an array\n const activeMode = config.mode;\n if (\n activeMode === \"deposit\" &&\n (!config.opportunities || config.opportunities.length === 0)\n ) {\n throw new Error(\n \"NexusOne deposit mode requires config.opportunities with at least one opportunity.\",\n );\n }\n const showCloseButton = !embed && Boolean(onClose);\n\n // Preload receive tokens once SDK is available\n useEffect(() => {\n if (nexusSDK) {\n preloadReceiveTokens();\n }\n }, [nexusSDK]);\n\n const { connector, status: walletStatus } = 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 : walletClientAddress &&\n isAddress(walletClientAddress) &&\n walletClientAddress.toLowerCase() !== zeroAddress\n ? walletClientAddress\n : undefined;\n const historyStorageKey = getSwapHistoryStorageKey(ownerAddress);\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 previousDefaultRecipientRef = useRef(defaultRecipientAddress);\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 [closingDrawerStep, setClosingDrawerStep] =\n useState(null);\n const rootContentRef = useRef(null);\n const [rootContentHeight, setRootContentHeight] = useState(\n null,\n );\n const [hasMeasuredRootContent, setHasMeasuredRootContent] = useState(false);\n const [fromTokens, setFromTokens] = useState([]);\n const [sourceSelectionTouched, setSourceSelectionTouched] = useState(false);\n const [sourceSelectionRevision, setSourceSelectionRevision] = useState(0);\n const [, setExactOutQuoteSourceMode] = useState<\"all\" | \"selected\">(\"all\");\n const exactOutQuoteSourceModeRef = useRef<\"all\" | \"selected\">(\"all\");\n const [toToken, setToToken] = useState(\n undefined,\n );\n const appliedTokenPrefillRef = useRef(null);\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 }, [activeMode, fetchSwapBalance, nexusSDK, swapStep]);\n\n useEffect(() => {\n setSourceSelectionTouched(false);\n setExactOutQuoteSourceModeValue(\"all\");\n }, [activeMode, setExactOutQuoteSourceModeValue]);\n\n useEffect(() => {\n const previousDefault = previousDefaultRecipientRef.current;\n previousDefaultRecipientRef.current = defaultRecipientAddress;\n\n if (activeMode !== \"swap\" || !defaultRecipientAddress) return;\n\n setRecipientAddress((current) => {\n if (\n !current ||\n (previousDefault &&\n current.toLowerCase() === previousDefault.toLowerCase())\n ) {\n return defaultRecipientAddress;\n }\n return current;\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 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 maxSwapQuoteCacheRef = useRef>({});\n const intentDestinationUsdRateCacheRef = useRef<\n Record\n >({});\n const intentSymbolUsdRateCacheRef = useRef>(\n {},\n );\n const predictiveQuoteCacheRef = useRef>(\n {},\n );\n const predictiveQuoteRunRef = useRef(0);\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 [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\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 } | null>(null);\n\n useEffect(() => {\n swapStepRef.current = swapStep;\n }, [swapStep]);\n\n useEffect(() => {\n return () => {\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n }\n };\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 setSwapStep(\"idle\");\n return;\n }\n\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n }\n\n setClosingDrawerStep(swapStep);\n drawerCloseTimerRef.current = setTimeout(() => {\n setSwapStep(\"idle\");\n setClosingDrawerStep(null);\n drawerCloseTimerRef.current = null;\n }, DRAWER_CLOSE_MS);\n }, [swapStep]);\n\n const openDrawerStep = useCallback((nextStep: SwapStep) => {\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n drawerCloseTimerRef.current = null;\n }\n setClosingDrawerStep(null);\n setSwapStep(nextStep);\n }, []);\n\n const syncRootContentHeight = useCallback(() => {\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 setRootContentHeight((previousHeight) =>\n previousHeight === nextHeight ? previousHeight : nextHeight,\n );\n setHasMeasuredRootContent(true);\n }, []);\n\n useLayoutEffect(() => {\n syncRootContentHeight();\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(syncRootContentHeight);\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 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?.icon)\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 = matchedAsset?.icon ?? (isNativeSource ? chainMeta?.logo : \"\");\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: 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: chainMeta?.name ?? source.chain.name,\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 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 setExactOutQuoteSourceModeValue(\"all\");\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 = 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\n const minimumSourceUsd = new Decimal(1);\n const hasMinimumSourceUsdBalance = (\n token: Pick,\n ) => (parseFiatNumber(token.balanceInFiat) ?? new Decimal(0)).gte(minimumSourceUsd);\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 = (intent?: SwapIntentData | null) => {\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 (!amount || !value || amount.lte(0) || value.lte(0) || !chainId || !symbol) {\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(\n (breakdownSum, item) => {\n const value = parseFiatNumber(item.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd)\n ? breakdownSum.plus(value)\n : breakdownSum;\n },\n new Decimal(0),\n ),\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 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 const getTotalBalancePercentUsdAmount = (pct: number) =>\n getSwapBalanceTotalUsd().mul(pct).div(100);\n const formatTokenAmountFromUsd = (\n usdAmount: Decimal,\n token: Pick,\n ) => {\n const rate = getUsdRateForSymbol(token.symbol);\n if (rate.lte(0)) return undefined;\n return usdAmount\n .div(rate)\n .toDecimalPlaces(Math.max(0, token.decimals ?? 18), Decimal.ROUND_DOWN)\n .toFixed();\n };\n\n const getMaxSwapQuoteCacheKey = (token?: SwapTokenOption) => {\n if (!token?.chainId) return \"\";\n return [\n token.chainId,\n (token.contractAddress || zeroAddress).toLowerCase(),\n token.symbol.toUpperCase(),\n ].join(\":\");\n };\n\n const getCachedMaxSwapQuote = (token?: SwapTokenOption) => {\n const key = getMaxSwapQuoteCacheKey(token);\n return key ? maxSwapQuoteCacheRef.current[key] : undefined;\n };\n\n const getCachedDestinationUsdRate = (token?: SwapTokenOption) => {\n const intentCachedRate = getCachedIntentUsdRate(token);\n if (intentCachedRate && intentCachedRate.gt(0)) {\n return intentCachedRate;\n }\n\n const cached = getCachedMaxSwapQuote(token);\n if (\n !cached ||\n !cached.maxUsdAmount ||\n cached.maxUsdAmount.lte(0) ||\n cached.maxTokenAmount.lte(0)\n ) {\n return undefined;\n }\n return cached.maxUsdAmount.div(cached.maxTokenAmount);\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 resolveMaxSwapQuote = async (token: SwapTokenOption) => {\n const key = getMaxSwapQuoteCacheKey(token);\n if (!key) return undefined;\n\n const cached = maxSwapQuoteCacheRef.current[key];\n if (cached) return cached;\n\n const calculateMaxForSwap = nexusSDK?.calculateMaxForSwap;\n if (typeof calculateMaxForSwap !== \"function\" || !token.chainId) {\n return undefined;\n }\n\n const max = await calculateMaxForSwap({\n toChainId: token.chainId,\n toTokenAddress: (token.contractAddress || zeroAddress) as `0x${string}`,\n });\n const decimals = Number.isFinite(Number(max.decimals))\n ? Number(max.decimals)\n : token.decimals || 18;\n const maxAmount =\n parseFiatNumber(max.maxAmount) ??\n (max.maxAmountRaw !== undefined\n ? new Decimal(max.maxAmountRaw.toString()).div(\n new Decimal(10).pow(decimals),\n )\n : undefined);\n\n if (!maxAmount || maxAmount.lte(0)) return undefined;\n\n const safeMaxAmount = maxAmount.mul(receiveMaxSafetyMultiplier);\n const destinationRate = await resolveUsdRateForSymbol(max.symbol || token.symbol);\n let maxUsdAmount =\n destinationRate.gt(0) ? safeMaxAmount.mul(destinationRate) : undefined;\n\n if (!maxUsdAmount || maxUsdAmount.lte(0)) {\n const sourcesUsd = await (max.sources ?? []).reduce(\n async (sumPromise, source) => {\n const sum = await sumPromise;\n const amount = parseFiatNumber(source.amount) ?? new Decimal(0);\n if (amount.lte(0)) return sum;\n\n const sourceRate = await resolveUsdRateForSymbol(source.symbol);\n return sourceRate.gt(0) ? sum.plus(amount.mul(sourceRate)) : sum;\n },\n Promise.resolve(new Decimal(0)),\n );\n\n if (sourcesUsd.gt(0)) {\n maxUsdAmount = sourcesUsd.mul(receiveMaxSafetyMultiplier);\n }\n }\n\n const quote: CachedMaxSwapQuote = {\n decimals,\n maxTokenAmount: safeMaxAmount,\n maxUsdAmount,\n symbol: max.symbol || token.symbol,\n };\n maxSwapQuoteCacheRef.current[key] = quote;\n return quote;\n };\n\n const getPercentAmountFromMaxQuote = async (\n token: SwapTokenOption,\n pct: number,\n preferUsd: boolean,\n ) => {\n const maxQuote = await resolveMaxSwapQuote(token);\n if (!maxQuote) return undefined;\n\n const ratio = new Decimal(pct).div(100);\n if (preferUsd && maxQuote.maxUsdAmount && maxQuote.maxUsdAmount.gt(0)) {\n return {\n amount: maxQuote.maxUsdAmount\n .mul(ratio)\n .toDecimalPlaces(2, Decimal.ROUND_DOWN)\n .toFixed(),\n mode: \"usd\" as const,\n };\n }\n\n return {\n amount: maxQuote.maxTokenAmount\n .mul(ratio)\n .toDecimalPlaces(Math.max(0, maxQuote.decimals), Decimal.ROUND_DOWN)\n .toFixed(),\n mode: \"token\" as const,\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 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 = (token: SwapTokenOption, tokenAmount: Decimal) => {\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 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(source, targetUsd)\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.toDecimalPlaces(6, Decimal.ROUND_DOWN).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(remainingTokenAmount, 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.toDecimalPlaces(6, Decimal.ROUND_DOWN).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 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 ? token.sourceTokens : [token],\n );\n const seen = new Set();\n return expanded.filter((token) => {\n if (!token.chainId || !token.contractAddress) return false;\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 = 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 = (breakdown.symbol ?? asset.symbol ?? \"\").toUpperCase();\n const assetSymbol = (asset.symbol ?? \"\").toUpperCase();\n const isNativeBalance =\n isNativeTokenAddress(breakdown.contractAddress) ||\n Boolean(nativeSymbol && (breakdownSymbol === nativeSymbol || assetSymbol === nativeSymbol));\n\n if (!isNativeBalance) continue;\n balance = balance.plus(parseFiatNumber(breakdown.balance) ?? new Decimal(0));\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 ) 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: chainMeta?.name ?? breakdown.chain?.name,\n contractAddress,\n decimals: breakdown.decimals ?? asset.decimals ?? 18,\n logo: asset.icon ?? \"\",\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 getExactOutSourceTokens = (\n mode: \"all\" | \"selected\" = exactOutQuoteSourceModeRef.current,\n ) => {\n if (\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n mode === \"selected\" &&\n fromTokens.length > 0\n ) {\n return filterMinimumSourceUsdTokens(getExpandedSourceTokens(fromTokens)).filter(\n hasGasForSource,\n );\n }\n\n return getGasCapableBalanceSourceTokens();\n };\n\n const buildFromSourcesPayload = (tokens: SwapTokenOption[]) => {\n const eligibleTokens = filterMinimumSourceUsdTokens(tokens).filter(\n (token) => token.chainId && token.contractAddress,\n );\n return {\n fromSources: eligibleTokens.map((token) => ({\n chainId: token.chainId!,\n tokenAddress: token.contractAddress as `0x${string}`,\n })),\n };\n };\n\n const buildPredictiveExactOutSources = async (requiredSourceUsd: Decimal) => {\n if (requiredSourceUsd.lte(0)) return [];\n\n const destinationKey = getTokenSelectionKey(toToken);\n const candidates = getExactOutSourceTokens()\n .filter((token) => getTokenSelectionKey(token) !== destinationKey)\n .filter((token) => getTokenBalanceUsd(token).gt(0))\n .sort((a, b) => getTokenBalanceUsd(b).cmp(getTokenBalanceUsd(a)));\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.lte(0)) 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.toDecimalPlaces(6, Decimal.ROUND_DOWN).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 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 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 const selectedSourceTotal =\n exactOutQuoteSourceModeRef.current === \"selected\" && fromTokens.length > 0\n ? fromTokens.reduce(\n (sum, token) => {\n const value = parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd) ? sum.plus(value) : sum;\n },\n new Decimal(0),\n )\n : undefined;\n\n if (selectedSourceTotal && selectedSourceTotal.gt(0)) {\n return selectedSourceTotal;\n }\n\n const allSourceTotal = getGasCapableBalanceSourceTokens().reduce(\n (sum, token) => {\n const value = parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd) ? sum.plus(value) : sum;\n },\n new Decimal(0),\n );\n\n return allSourceTotal.gt(0) ? allSourceTotal : getSwapBalanceTotalUsd();\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(missingTokenAmount.mul(fiatBalance.div(availableTokenAmount)));\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 = (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 = getExactOutRequestedUsd();\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 ||\n missingUsd.lte(0) ||\n 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 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()).div(new Decimal(10).pow(decimals)).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 receiveMaxSafetyMultiplier = new Decimal(\"0.9\");\n const currentSwapEntry =\n currentSwapId !== null\n ? swapHistory.find((entry) => entry.id === currentSwapId)\n : undefined;\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) =>\n entry.id === id ? { ...entry, ...patch } : entry,\n ),\n ),\n );\n };\n\n const patchCurrentSwapHistoryEntry = (patch: Partial) => {\n patchSwapHistoryEntry(currentSwapIdRef.current, patch);\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,\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 ) => {\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 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: activeMode === \"send\" ? recipientAddress : undefined,\n opportunity: selectedOpportunity,\n feeUsd: intentFeeUsd,\n sourceExplorerUrl: null,\n finalExplorerUrl: null,\n intentExplorerUrl: null,\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\",\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(\n 1,\n Math.round((now - startedAt) / 1000),\n ),\n sourceExplorerUrl: explorerUrlsRef.current.sourceExplorerUrl,\n finalExplorerUrl: explorerUrlsRef.current.destinationExplorerUrl,\n ...patch,\n });\n void fetchSwapBalance();\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 handleRefundIntent = async (entry: SwapHistoryEntry) => {\n if (!nexusSDK || !entry.intentId) return;\n patchSwapHistoryEntry(entry.id, { status: \"refund-initiated\" });\n try {\n await nexusSDK.refundIntent(entry.intentId);\n void fetchSwapBalance();\n } catch (error: any) {\n patchSwapHistoryEntry(entry.id, {\n status: \"failed\",\n error: error?.message || \"Refund failed. Please try again.\",\n });\n void fetchSwapBalance();\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 lastSwapIntentRefreshAtRef.current = Date.now();\n cacheDestinationUsdRateFromIntent(intent);\n cachePredictiveBaselineFromIntent(intent);\n setIntentData(intent);\n setIntentToAmount(intent.destination?.amount || undefined);\n setSwapQuoteIssue(null);\n\n if (\n (activeMode === \"send\" ||\n (activeMode === \"deposit\" && swapType === \"exactOut\"))\n ) {\n syncingIntentSourcesRef.current = true;\n setFromTokens((intent.sources ?? []).map(buildIntentSourceToken));\n }\n\n try {\n const bridgeFees = intent.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 bridgeComponentsTotal = bridgeFeeData\n ? [\n executionGasFee,\n parseFiatNumber(bridgeFeeData.protocol),\n parseFiatNumber(bridgeFeeData.solver),\n parseFiatNumber(bridgeFeeData.gasSupplied),\n ].reduce(\n (sum, value) => sum.plus(value ?? new Decimal(0)),\n new Decimal(0),\n )\n : undefined;\n const bridgeTotal =\n typeof bridgeFees === \"string\"\n ? parseFiatNumber(bridgeFees)\n : parseFiatNumber(bridgeFeeData?.total) ??\n (bridgeComponentsTotal && bridgeComponentsTotal.gt(0)\n ? bridgeComponentsTotal\n : undefined);\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 [activeMode, fromTokens, swapType, swapBalance, toToken],\n );\n\n // Register swap intent hook immediately before executing a swap to prevent race conditions across multiple components\n const registerIntentHook = (runId: number) => {\n if (!nexusSDK) return;\n nexusSDK.setOnSwapIntentHook(async ({ intent, allow, deny, refresh }) => {\n if (swapRunIdRef.current !== runId) {\n deny();\n return;\n }\n // Store callbacks so accept/reject buttons can call them\n swapIntentRef.current = { intent, allow, deny, refresh, runId };\n // Populate intent data for preview\n applySwapIntent(intent);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n });\n };\n\n // Deposit-specific\n const [selectedOpportunity, setSelectedOpportunity] = useState<\n DepositOpportunity | undefined\n >(() =>\n activeMode === \"deposit\" && config.opportunities?.length === 1\n ? config.opportunities[0]\n : undefined,\n );\n const [pendingOpportunity, setPendingOpportunity] = useState<\n DepositOpportunity | undefined\n >(undefined);\n const [depositAmountMode, setDepositAmountMode] = useState<\"token\" | \"usd\">(\n \"token\",\n );\n\n const toTokenFromOpportunity = (\n opp: DepositOpportunity,\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() ||\n 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: matchedToken?.decimals ?? citreaToken?.decimals ?? tokenMeta?.decimals ?? 18,\n logo: opp.tokenLogo || matchedToken?.logo || citreaToken?.logo || tokenMeta?.icon,\n chainName: CHAIN_METADATA[opp.chainId]?.name ?? citreaToken?.chainName,\n chainLogo: CHAIN_METADATA[opp.chainId]?.logo ?? citreaToken?.chainLogo,\n };\n };\n\n const getDestinationBalanceFromSwapBalances = (\n token?: SwapTokenOption,\n ) => {\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() === 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?: { token: `0x${string}`; chain: number }) => {\n if (!pair?.token || !pair.chain) return undefined;\n\n const normalizeAddress = (address?: string) => {\n if (!address) return \"\";\n return isNativeTokenAddress(address) ? zeroAddress : address.toLowerCase();\n };\n const targetAddress = normalizeAddress(pair.token);\n\n const balanceToken = deriveTokenOptions(swapBalance ?? []).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((item) => item.id === pair.chain);\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 tokenAddressSymbol ??\n (isNativePrefill ? chainMeta?.nativeCurrency?.symbol : undefined) ??\n \"Token\";\n const tokenMeta = TOKEN_METADATA[tokenSymbol as keyof typeof TOKEN_METADATA];\n\n if (\n !chain &&\n !matchedToken &&\n !citreaToken &&\n !tokenAddressSymbol &&\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 tokenMeta?.decimals ??\n (isNativePrefill ? chainMeta?.nativeCurrency?.decimals : undefined) ??\n 18,\n logo: matchedToken?.logo || citreaToken?.logo || tokenMeta?.icon,\n chainName:\n chain?.name ?? chainMeta?.name ?? citreaToken?.chainName,\n chainLogo:\n chain?.logo ?? chainMeta?.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([{ ...sourceToken, userAmount: config.prefill?.amount ?? \"\" }]);\n setSourceSelectionTouched(true);\n }\n if (destinationToken) {\n setToToken(destinationToken);\n }\n setSwapType(\"exactIn\");\n appliedTokenPrefillRef.current = prefillKey;\n }, [\n activeMode,\n config.prefill?.amount,\n config.prefill?.destination?.chain,\n config.prefill?.destination?.token,\n config.prefill?.source?.chain,\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?.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 = getDestinationBalanceFromSwapBalances(balanceToken);\n if (swapBalanceValue) {\n setDestinationBalance(swapBalanceValue);\n return;\n }\n\n const chainMeta = CHAIN_METADATA[balanceToken.chainId];\n const rpcUrl = chainMeta?.rpcUrls?.[0];\n if (!rpcUrl) return;\n\n let cancelled = false;\n const client = createPublicClient({\n chain: {\n id: balanceToken.chainId,\n name: chainMeta?.name ?? balanceToken.chainName ?? \"Destination Chain\",\n nativeCurrency: chainMeta?.nativeCurrency ?? {\n decimals: 18,\n name: \"Ether\",\n symbol: \"ETH\",\n },\n rpcUrls: {\n default: { http: [rpcUrl] },\n public: { http: [rpcUrl] },\n },\n blockExplorers: chainMeta?.blockExplorerUrls?.[0]\n ? {\n default: {\n name: chainMeta.name,\n url: chainMeta.blockExplorerUrls[0],\n },\n }\n : undefined,\n } as any,\n transport: http(rpcUrl),\n });\n\n const fetchDestinationBalance = async () => {\n try {\n let rawBalance: bigint;\n let decimals = balanceToken.decimals || 18;\n\n if (isNativeTokenAddress(balanceToken.contractAddress)) {\n rawBalance = await client.getBalance({\n address: ownerAddress as `0x${string}`,\n });\n decimals = 18;\n } else {\n const tokenAddress = balanceToken.contractAddress as `0x${string}`;\n const [balanceResult, decimalsResult] = await Promise.all([\n client.readContract({\n abi: erc20Abi,\n address: tokenAddress,\n functionName: \"balanceOf\",\n args: [ownerAddress as `0x${string}`],\n }) as Promise,\n client\n .readContract({\n abi: erc20Abi,\n address: tokenAddress,\n functionName: \"decimals\",\n })\n .catch(() => decimals),\n ]);\n\n rawBalance = balanceResult;\n decimals = Number(decimalsResult) || decimals;\n }\n\n if (!cancelled) {\n setDestinationBalance(\n `${formatReadableTokenBalanceAmount(rawBalance, decimals)} ${balanceToken.symbol}`,\n );\n }\n } catch (error) {\n console.warn(\"Unable to fetch destination token balance\", error);\n }\n };\n\n void fetchDestinationBalance();\n\n return () => {\n cancelled = true;\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\") return;\n if (selectedOpportunity) return;\n if (config.opportunities?.length === 1) {\n const [opp] = config.opportunities;\n setSelectedOpportunity(opp);\n setSwapType(\"exactOut\");\n setToToken(toTokenFromOpportunity(opp));\n }\n }, [activeMode, config.opportunities, selectedOpportunity, supportedChainsAndTokens]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || !selectedOpportunity) return;\n setToToken((current) => ({\n ...toTokenFromOpportunity(selectedOpportunity),\n balance: current?.balance ?? \"0\",\n balanceInFiat: current?.balanceInFiat ?? \"$0.00\",\n }));\n }, [activeMode, selectedOpportunity, supportedChainsAndTokens]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (selectedOpportunity) return;\n if (!config.opportunities || config.opportunities.length <= 1) return;\n setPendingOpportunity((current) => current ?? config.opportunities?.[0]);\n }, [activeMode, config.opportunities, selectedOpportunity]);\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 One 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) return new Decimal(0);\n const fiat = getFiatValue(1, selectedOpportunity.tokenSymbol);\n if (Number.isFinite(fiat) && fiat > 0) {\n return new Decimal(fiat);\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 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?.toDecimalPlaces(toToken?.decimals ?? 18).toFixed() ??\n \"0\";\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 = () => {\n const shouldLoadQuote = Boolean(nexusSDK && canRefreshExactOutQuote());\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\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 =\n destinationUsdNeedingSources.lte(0)\n ? new Decimal(0)\n : cachedSourceUsdRatio && cachedSourceUsdRatio.gt(0)\n ? destinationUsdNeedingSources.mul(cachedSourceUsdRatio)\n : destinationUsdNeedingSources\n .mul(BASIS_POINTS + PREDICTIVE_EXACT_OUT_BUFFER_BPS)\n .div(BASIS_POINTS);\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 fromTokens,\n nexusSDK,\n selectedOpportunity,\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 defaultDepositSourceTokens = useMemo(() => {\n if (activeMode !== \"deposit\" || !swapBalance) return [];\n return deriveTokenOptions(swapBalance)\n .filter(hasMinimumSourceUsdBalance)\n .map((token) => ({\n ...token,\n userAmount: \"\",\n }));\n }, [activeMode, swapBalance]);\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 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 toToken.contractAddress &&\n (breakdownAddress.toLowerCase() === 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 if (!fiatBalance || fiatBalance.lt(minimumSourceUsd)) continue;\n return [\n {\n chainId,\n chainLogo: chainMeta?.logo ?? breakdown.chain?.logo ?? toToken.chainLogo,\n chainName: chainMeta?.name ?? breakdown.chain?.name ?? toToken.chainName,\n contractAddress: breakdown.contractAddress ?? toToken.contractAddress,\n decimals: breakdown.decimals ?? asset.decimals ?? toToken.decimals ?? 18,\n logo: asset.icon ?? 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 },\n ];\n }\n }\n\n return [];\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 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) => getTokenSelectionKey(token) === getTokenSelectionKey(locked),\n ),\n );\n if (missing.length === 0) return current;\n return [...current, ...missing.map((token) => ({ ...token, userAmount: \"\" }))];\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 (\n defaultDepositSourceTokens.length === 0 &&\n lockedDestinationSourceTokens.length === 0\n ) {\n return;\n }\n\n setFromTokens((current) => {\n const lockedKeys = new Set(\n lockedDestinationSourceTokens.map(getTokenSelectionKey),\n );\n const canInitialize =\n current.length === 0 ||\n current.every((token) => lockedKeys.has(getTokenSelectionKey(token)));\n if (!canInitialize) return current;\n\n const next: SwapTokenOption[] = [];\n const seen = new Set();\n for (const token of [\n ...defaultDepositSourceTokens,\n ...lockedDestinationSourceTokens,\n ]) {\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 defaultDepositSourceTokens,\n depositTokenAmountForQuote?.toFixed(),\n lockedDestinationSourceTokens,\n sourceSelectionTouched,\n toToken,\n ]);\n\n // ---------------------------------------------------------------------------\n // Handlers\n // ---------------------------------------------------------------------------\n\n const handleReset = () => {\n clearPendingSwapIntent();\n setAmount(\"\");\n setRecipientAddress(\"\");\n setTxError(null);\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n clearSelectedSources();\n setToToken(undefined);\n setSelectedOpportunity(undefined);\n setPendingOpportunity(undefined);\n setDepositAmountMode(\"token\");\n };\n\n const resetInputsAfterSuccessfulExecution = () => {\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 setToToken(undefined);\n setDepositAmountMode(\"token\");\n };\n\n const handleSelectDepositOpportunity = (opp: DepositOpportunity) => {\n clearPendingSwapIntent();\n setTxError(null);\n setSwapQuoteIssue(null);\n setSelectedOpportunity(opp);\n setPendingOpportunity(opp);\n setSwapType(\"exactOut\");\n setDepositAmountMode(\"token\");\n setAmount(\"\");\n clearSelectedSources();\n setToToken(toTokenFromOpportunity(opp));\n };\n\n const handleClose = () => {\n clearPendingSwapIntent();\n onClose?.();\n };\n\n const handleConnectWallet = async () => {\n if (walletActionPending || nexusLoading || 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 activeConnector = nextConnector;\n }\n\n const connectorProvider = await activeConnector\n ?.getProvider()\n .catch(() => undefined);\n const connectorClientProvider = connectorClient\n ? {\n request: (args: unknown) =>\n connectorClient.request(args as any),\n }\n : undefined;\n const walletClientProvider = walletClient\n ? {\n request: (args: unknown) =>\n 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 ??\n walletClientProvider ??\n windowProvider);\n\n if (!effectiveProvider || typeof effectiveProvider.request !== \"function\") {\n throw new Error(\"Wallet provider is not ready yet.\");\n }\n\n await handleInit(effectiveProvider as EthereumProvider);\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 โ€” SDK will trigger setOnSwapIntentHook for preview */\n const handleEnterPreview = async (\n options: { background?: boolean } = {},\n ) => {\n const { background = false } = options;\n const isExactOutFlow = activeMode === \"deposit\" || activeMode === \"send\";\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 setTxError(null);\n setSwapQuoteIssue(null);\n\n if (\n !background &&\n swapIntentRef.current?.runId === swapRunIdRef.current &&\n intentData &&\n !intentLoading &&\n (activeMode !== \"send\" || Boolean(recipientAddress)) &&\n ((activeMode !== \"deposit\" && activeMode !== \"send\") ||\n (intentData.sources ?? []).length > 0)\n ) {\n swapStepRef.current = \"preview-intent\";\n setSwapStep(\"preview-intent\");\n return;\n }\n\n if (\n !background &&\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (!intentData ||\n !swapIntentRef.current ||\n swapIntentRef.current.runId !== swapRunIdRef.current ||\n (intentData.sources ?? []).length === 0)\n ) {\n setTxError(\"Quote unavailable. Please wait for sources to be selected.\");\n return;\n }\n\n const hasCustomSwapRecipient =\n activeMode === \"swap\" &&\n Boolean(recipientAddress) &&\n (!defaultRecipientAddress ||\n recipientAddress.toLowerCase() !== defaultRecipientAddress.toLowerCase());\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() ===\n ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\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 // Claim ownership of global singleton hook before executing SDK swap\n registerIntentHook(runId);\n\n const getSwapStepListFromEvent = (event: { args: any }) => {\n const args = (event as any).args;\n return Array.isArray(args)\n ? args\n : Array.isArray(args?.steps)\n ? args.steps\n : [];\n };\n\n const handleSwapEvent = (event: { name: string; args: any }) => {\n console.log(\"[NexusOne][SDK swap event]\", event.name, event);\n if (event.name === NEXUS_EVENTS.SWAP_STEPS_LIST) {\n const stepList = getSwapStepListFromEvent(event);\n if (stepList.length > 0) {\n swapStepsListRef.current = stepList as SwapStepType[];\n appendProgressListEvent(event.name, stepList);\n onStepsList(stepList);\n }\n return;\n }\n if (event.name === NEXUS_EVENTS.STEPS_LIST) {\n const args = (event as any).args;\n const stepList = Array.isArray(args)\n ? args\n : Array.isArray(args?.steps)\n ? args.steps\n : [];\n if (stepList.length > 0) {\n appendProgressListEvent(event.name, stepList);\n onStepsList(stepList);\n }\n return;\n }\n if (event.name === NEXUS_EVENTS.STEP_COMPLETE) {\n const step = event.args as BridgeStepType;\n appendProgressEvent(event.name, step, true);\n if (\n (step as any)?.type === \"TRANSACTION_SENT\" ||\n (step as any)?.type === \"TRANSACTION_CONFIRMED\"\n ) {\n markSwapExecutionStarted();\n }\n if ((step as any)?.data?.explorerURL) {\n mergeExplorerUrls({\n destinationExplorerUrl: (step as any).data.explorerURL,\n });\n }\n if ((step as any)?.completed !== false) {\n onStepComplete(step as any);\n }\n return;\n }\n if (event.name === \"SWAP_SKIPPED\") {\n const step =\n event.args && typeof event.args === \"object\"\n ? event.args\n : ({\n completed: true,\n data: event.args,\n type: \"SWAP_SKIPPED\",\n typeID: \"SWAP_SKIPPED\",\n } as unknown as SwapStepType);\n enterSkippedSwapProgress();\n appendProgressEvent(NEXUS_EVENTS.SWAP_STEP_COMPLETE, step, true);\n onStepComplete(step as SwapStepType);\n return;\n }\n if (event.name === NEXUS_EVENTS.SWAP_STEP_COMPLETE) {\n const step = event.args;\n const swapSkipped = isSwapSkippedStepType(getProgressStepType(step));\n if (swapSkipped) {\n enterSkippedSwapProgress();\n }\n appendProgressEvent(event.name, step, true);\n if (\n [\n \"SOURCE_SWAP_BATCH_TX\",\n \"SOURCE_SWAP_HASH\",\n \"BRIDGE_DEPOSIT\",\n \"RFF_ID\",\n \"DESTINATION_SWAP_BATCH_TX\",\n \"DESTINATION_SWAP_HASH\",\n \"SWAP_COMPLETE\",\n \"SWAP_SKIPPED\",\n ].includes(step?.type ?? \"\")\n ) {\n markSwapExecutionStarted();\n }\n if (step?.type === \"SOURCE_SWAP_HASH\" && step.explorerURL) {\n mergeExplorerUrls({ sourceExplorerUrl: step.explorerURL });\n }\n if (step?.type === \"DESTINATION_SWAP_HASH\" && step.explorerURL) {\n mergeExplorerUrls({ destinationExplorerUrl: step.explorerURL });\n }\n if (step?.type === \"BRIDGE_DEPOSIT\" && (step as any).data?.explorerURL) {\n mergeExplorerUrls({\n sourceExplorerUrl: (step as any).data.explorerURL,\n });\n }\n if (step?.type === \"RFF_ID\") {\n const nextIntentId = Number((step as any).data);\n if (Number.isFinite(nextIntentId) && nextIntentId > 0) {\n patchCurrentSwapHistoryEntry({ intentId: nextIntentId });\n }\n }\n if (step?.completed !== false) {\n onStepComplete(step);\n }\n }\n };\n\n try {\n if (!isExactOutFlow) {\n const fromPayload: {\n chainId: number;\n tokenAddress: `0x${string}`;\n amount: 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 = cleanAmount\n .toDecimalPlaces(Math.max(0, token.decimals || 18), Decimal.ROUND_DOWN)\n .toFixed();\n\n fromPayload.push({\n chainId: token.chainId!,\n tokenAddress: token.contractAddress as `0x${string}`,\n amount: nexusSDK.utils.parseUnits(\n safeTokenAmountStr,\n token.decimals || 18,\n ),\n });\n }\n\n if (fromPayload.length === 0) {\n throw new Error(\"No source amount available for swap.\");\n }\n\n resetExplorerUrls();\n // Start exact-in swap โ€” the intent hook will fire and populate preview\n const result = await nexusSDK.swapWithExactIn(\n {\n from: fromPayload,\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n },\n {\n onEvent: (event: any) => {\n if (swapRunIdRef.current !== runId) return;\n handleSwapEvent(event);\n },\n },\n );\n if (!result?.success) {\n throw new Error(result?.error || \"Swap failed\");\n }\n const intentExplorerUrl = result.result.explorerURL || null;\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ?? currentSwapEntry?.intentId;\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n finishCurrentSwapHistoryEntry(\"fulfilled\", {\n intentExplorerUrl,\n intentId,\n finalExplorerUrl:\n explorerUrlsRef.current.destinationExplorerUrl ||\n explorerUrlsRef.current.sourceExplorerUrl,\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 = nexusSDK.utils.parseUnits(\n exactOutAmountString,\n toToken.decimals || 18,\n );\n\n resetExplorerUrls();\n\n const fromSourcesPayload = buildFromSourcesPayload(\n getExactOutSourceTokens(),\n );\n\n const isNative =\n !toToken.contractAddress ||\n toToken.contractAddress.toLowerCase() ===\n \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\" ||\n toToken.contractAddress ===\n \"0x0000000000000000000000000000000000000000\";\n let executeConfig: any;\n if (activeMode === \"deposit\" && !selectedOpportunity?.execute) {\n throw new Error(\n \"Selected deposit opportunity is missing execute parameters.\",\n );\n }\n\n if (activeMode === \"deposit\" && selectedOpportunity?.execute) {\n executeConfig =\n typeof selectedOpportunity.execute === \"function\"\n ? selectedOpportunity.execute(\n amountBigInt,\n (ownerAddress ?? connectedAddress) as `0x${string}`,\n )\n : selectedOpportunity.execute;\n } else if (\n activeMode === \"send\" &&\n resolvedRecipientAddress\n ) {\n if (isNative) {\n executeConfig = {\n to: resolvedRecipientAddress as `0x${string}`,\n value: amountBigInt,\n gas: BigInt(100000),\n };\n } else {\n executeConfig = {\n to: toToken.contractAddress as `0x${string}`,\n data: encodeFunctionData({\n abi: erc20Abi,\n functionName: \"transfer\",\n args: [resolvedRecipientAddress as `0x${string}`, amountBigInt],\n }),\n gas: BigInt(100000),\n };\n }\n }\n\n if (executeConfig) {\n const onEvent = (event: any) => {\n if (swapRunIdRef.current !== runId) return;\n handleSwapEvent(event);\n };\n const sdkWithOptionalTransfer = nexusSDK as any;\n const result =\n activeMode === \"send\" &&\n typeof sdkWithOptionalTransfer.swapAndTransfer === \"function\"\n ? await sdkWithOptionalTransfer.swapAndTransfer(\n {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmount: amountBigInt,\n recipient: resolvedRecipientAddress as `0x${string}`,\n ...fromSourcesPayload,\n },\n { onEvent },\n )\n : await nexusSDK.swapAndExecute(\n {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmount: amountBigInt,\n execute: executeConfig,\n ...fromSourcesPayload,\n },\n { onEvent },\n );\n\n const swapResult = result?.swapResult ?? result?.result ?? null;\n const swapSkipped = Boolean((result as any)?.swapSkipped);\n if (!swapResult && !swapSkipped && activeMode !== \"send\") {\n throw new Error(\"Swap failed\");\n }\n const executeTxHash =\n result?.executeResponse?.txHash ||\n result?.transactionHash ||\n result?.txHash ||\n null;\n const intentExplorerUrl =\n swapResult?.explorerURL || result?.intentExplorerUrl || null;\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ?? currentSwapEntry?.intentId;\n const finalExplorerUrl =\n result?.explorerUrl ||\n result?.executeExplorerUrl ||\n getExplorerTxUrl(toToken.chainId, executeTxHash);\n if (finalExplorerUrl) {\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n patchCurrentSwapHistoryEntry({\n intentExplorerUrl,\n intentId,\n finalExplorerUrl,\n });\n } else {\n const result = await nexusSDK.swapWithExactOut(\n {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmount: amountBigInt,\n ...fromSourcesPayload,\n },\n {\n onEvent: (event: any) => {\n if (swapRunIdRef.current !== runId) return;\n handleSwapEvent(event);\n },\n },\n );\n if (!result?.success) {\n throw new Error(result?.error || \"Swap failed\");\n }\n const intentExplorerUrl = result.result.explorerURL || null;\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ?? currentSwapEntry?.intentId;\n patchCurrentSwapHistoryEntry({ intentExplorerUrl, intentId });\n }\n\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n finishCurrentSwapHistoryEntry(\"fulfilled\");\n resetInputsAfterSuccessfulExecution();\n onComplete?.();\n setSwapStep(\"success\");\n }\n }\n } catch (err: any) {\n console.error(\"Error in handleEnterPreview:\", err);\n if (swapRunIdRef.current !== runId) {\n return;\n }\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n const hasActiveExecution =\n swapStepRef.current === \"progress\" && Boolean(currentSwapIdRef.current);\n const showFailedProgressThenReceipt = (\n error: string,\n patch: Partial = {},\n ) => {\n const failedProgressEvent = progressEventsRef.current.at(-1);\n const fallbackFailedStep =\n activeMode === \"deposit\" || activeMode === \"send\"\n ? ({ type: \"APPROVAL\", typeID: \"AP\" } as BridgeStepType)\n : ({\n type: \"DETERMINING_SWAP\",\n typeID: \"DETERMINING_SWAP\",\n } as unknown as SwapStepType);\n const failedStep =\n failedProgressEvent?.step ?? fallbackFailedStep;\n const autoRefundAvailable =\n isAutoRefundAvailableProgressEvent(failedProgressEvent);\n setFailedProgressStep(failedStep);\n finishCurrentSwapHistoryEntry(\"failed\", {\n error,\n autoRefundAvailable,\n failureMessage: getFailureMessageForProgressStep(\n failedStep,\n 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 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 const errorMessage =\n err?.message ||\n (typeof err === \"string\"\n ? err\n : \"Transaction failed. Please try again or check console.\");\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 useEffect(() => {\n if (activeMode !== \"swap\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = 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 clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n const timer = window.setTimeout(() => {\n void handleEnterPreview({ background: true });\n }, EXACT_OUT_INPUT_DEBOUNCE_MS);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [activeMode, amount, fromTokens, nexusSDK, swapStep, toToken]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = 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 clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n const timer = window.setTimeout(() => {\n void handleEnterPreview({ background: true });\n }, EXACT_OUT_INPUT_DEBOUNCE_MS);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n amount,\n depositAmountMode,\n nexusSDK,\n sourceSelectionRevision,\n selectedOpportunity,\n swapStep,\n toToken,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"send\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = 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 clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n const timer = window.setTimeout(() => {\n void handleEnterPreview({ background: true });\n }, EXACT_OUT_INPUT_DEBOUNCE_MS);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [activeMode, amount, nexusSDK, sourceSelectionRevision, swapStep, toToken]);\n\n const refreshActiveSwapIntent = useCallback(async () => {\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 isPreviewRefresh = swapStepRef.current === \"preview-intent\";\n if (isPreviewRefresh) {\n setPreviewQuoteRefreshing(true);\n } else {\n setQuoteRefreshing(true);\n }\n try {\n const updated = await activeIntent.refresh();\n if (!updated || swapRunIdRef.current !== runId) return;\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 (swapRunIdRef.current === runId) {\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\" || activeMode === \"deposit\" || activeMode === \"send\") &&\n Boolean(intentData && swapIntentRef.current) &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n\n if (!hasRefreshableIntent) return;\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 (\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 intentData,\n intentLoading,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n refreshActiveSwapIntent,\n swapStep,\n ]);\n\n useEffect(() => {\n const hasRefreshableIntent =\n (activeMode === \"swap\" || activeMode === \"deposit\" || activeMode === \"send\") &&\n Boolean(intentData && swapIntentRef.current) &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n\n if (!hasRefreshableIntent) {\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 }, [activeMode, intentData, swapStep]);\n\n /** User accepted swap from the preview โ€” call allow() from the intent hook */\n const handleSwapAccept = () => {\n if (swapIntentRef.current) {\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 swapIntentRef.current.allow();\n // The swap promise in handleEnterPreview will resolve/reject\n }\n };\n\n // ---------------------------------------------------------------------------\n // Header title\n // ---------------------------------------------------------------------------\n const getTitle = () => {\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\") return \"Swap Failed\";\n return \"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\") return \"Deposit Failed\";\n return \"Deposit\";\n }\n if (activeMode === \"send\") {\n if (swapStep === \"progress\") return \"Sendingโ€ฆ\";\n if (swapStep === \"success\") return \"Send Complete\";\n if (swapStep === \"failed\") return \"Send Failed\";\n return \"Send\";\n }\n return \"Nexus One\";\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 !== \"idle\" &&\n swapStep !== \"choose-swap-asset\" &&\n swapStep !== \"choose-receive-asset\" &&\n swapStep !== \"enter-recipient\";\n const handleBack = () => {\n if (swapStep === \"history\") {\n setSwapStep(\"idle\");\n return;\n }\n if (swapStep === \"choose-swap-asset\") {\n closeDrawerToIdle();\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 = (\n val: string,\n panel: \"send\" | \"receive\",\n ) => {\n syncingIntentSourcesRef.current = false;\n setSwapQuoteIssue(null);\n setTxError(null);\n const nextAmount = parseFiatNumber(val);\n const hasSelectedSourceToken = fromTokens.some(\n (token) => token.chainId && token.contractAddress,\n );\n const shouldLoadQuote = Boolean(\n nexusSDK && nextAmount?.gt(0) && toToken && hasSelectedSourceToken,\n );\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\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 One 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 handleDepositAmountChange = (val: string) => {\n syncingIntentSourcesRef.current = false;\n setExactOutQuoteSourceModeValue(\"all\");\n maxPercentRunRef.current += 1;\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setSwapQuoteIssue(null);\n const nextAmount = parseFiatNumber(val);\n const shouldLoadQuote = Boolean(\n nexusSDK && nextAmount?.gt(0) && toToken && selectedOpportunity,\n );\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n } else {\n clearSelectedSources();\n }\n setAmount(val);\n };\n\n const handleSendAmountChange = (val: string) => {\n syncingIntentSourcesRef.current = false;\n setExactOutQuoteSourceModeValue(\"all\");\n maxPercentRunRef.current += 1;\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setSwapQuoteIssue(null);\n setSwapType(\"exactOut\");\n const nextAmount = parseFiatNumber(val);\n const shouldLoadQuote = Boolean(nexusSDK && nextAmount?.gt(0) && toToken);\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n } else {\n clearSelectedSources();\n }\n setAmount(val);\n };\n\n const handleDepositAmountModeToggle = () => {\n syncingIntentSourcesRef.current = false;\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.div(rate).toDecimalPlaces(toToken?.decimals ?? 18);\n setAmount(converted.toFixed());\n }\n clearPendingSwapIntent();\n setDepositAmountMode((current) => (current === \"token\" ? \"usd\" : \"token\"));\n };\n\n const handleDepositPercentSelect = async (pct: number) => {\n if (!toToken) return;\n\n syncingIntentSourcesRef.current = false;\n setTxError(null);\n setSwapQuoteIssue(null);\n const runId = ++maxPercentRunRef.current;\n\n if (pct !== 100) {\n const usdAmount = getTotalBalancePercentUsdAmount(pct);\n const shouldUseMaxQuoteFallback =\n depositAmountMode === \"usd\" && getDepositTokenUsdRate().lte(0);\n const nextAmount =\n depositAmountMode === \"usd\"\n ? usdAmount.toDecimalPlaces(2, Decimal.ROUND_DOWN).toFixed()\n : formatTokenAmountFromUsd(usdAmount, toToken);\n\n if (nextAmount && !shouldUseMaxQuoteFallback) {\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleDepositAmountChange(nextAmount);\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(true);\n setMaxCalculationPercent(pct);\n try {\n await waitForNextPaint();\n const fallback = await getPercentAmountFromMaxQuote(\n toToken,\n pct,\n depositAmountMode === \"usd\",\n );\n if (runId !== maxPercentRunRef.current) return;\n if (!fallback) {\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 setDepositAmountMode(fallback.mode);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleDepositAmountChange(fallback.amount);\n } catch (error: any) {\n if (runId !== maxPercentRunRef.current) return;\n console.error(\"Unable to calculate percentage deposit amount\", error);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n if (isInsufficientSourcesError(error)) {\n setSwapQuoteIssue(buildInsufficientSourcesIssue(error));\n return;\n }\n setTxError(\n error?.message || \"Unable to calculate this percentage for the deposit asset.\",\n );\n }\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(true);\n setMaxCalculationPercent(100);\n try {\n await waitForNextPaint();\n const maxAmount = await getPercentAmountFromMaxQuote(\n toToken,\n 100,\n depositAmountMode === \"usd\",\n );\n if (runId !== maxPercentRunRef.current) return;\n if (!maxAmount) {\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n setTxError(\"No depositable amount is available for this opportunity.\");\n return;\n }\n\n setDepositAmountMode(maxAmount.mode);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleDepositAmountChange(maxAmount.amount);\n } catch (error: any) {\n if (runId !== maxPercentRunRef.current) return;\n console.error(\"Unable to calculate max deposit amount\", error);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n if (isInsufficientSourcesError(error)) {\n setSwapQuoteIssue(buildInsufficientSourcesIssue(error));\n return;\n }\n setTxError(\n error?.message || \"Unable to calculate the max deposit amount.\",\n );\n }\n };\n\n const handleSendPercentSelect = async (pct: number) => {\n if (!toToken) return;\n\n syncingIntentSourcesRef.current = false;\n setTxError(null);\n setSwapQuoteIssue(null);\n const runId = ++maxPercentRunRef.current;\n\n if (pct !== 100) {\n const usdAmount = getTotalBalancePercentUsdAmount(pct);\n const nextAmount = formatTokenAmountFromUsd(usdAmount, toToken);\n\n if (nextAmount) {\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleSendAmountChange(nextAmount);\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(true);\n setMaxCalculationPercent(pct);\n try {\n await waitForNextPaint();\n const fallback = await getPercentAmountFromMaxQuote(toToken, pct, false);\n if (runId !== maxPercentRunRef.current) return;\n if (!fallback) {\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 setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleSendAmountChange(fallback.amount);\n } catch (error: any) {\n if (runId !== maxPercentRunRef.current) return;\n console.error(\"Unable to calculate percentage send amount\", error);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n if (isInsufficientSourcesError(error)) {\n setSwapQuoteIssue(buildInsufficientSourcesIssue(error));\n return;\n }\n setTxError(\n error?.message || \"Unable to calculate this percentage for the send asset.\",\n );\n }\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(true);\n setMaxCalculationPercent(100);\n try {\n await waitForNextPaint();\n const maxAmount = await getPercentAmountFromMaxQuote(toToken, 100, false);\n if (runId !== maxPercentRunRef.current) return;\n if (!maxAmount) {\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n setTxError(\"No transferable amount is available for this asset.\");\n return;\n }\n\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleSendAmountChange(maxAmount.amount);\n } catch (error: any) {\n if (runId !== maxPercentRunRef.current) return;\n console.error(\"Unable to calculate max send amount\", error);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n if (isInsufficientSourcesError(error)) {\n setSwapQuoteIssue(buildInsufficientSourcesIssue(error));\n return;\n }\n setTxError(error?.message || \"Unable to calculate the max send amount.\");\n }\n };\n\n // ---------------------------------------------------------------------------\n // Render\n // ---------------------------------------------------------------------------\n const exactOutInsufficientSourceIssue =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n swapQuoteIssue?.type === \"insufficientSources\"\n ? swapQuoteIssue\n : null;\n const isExactOutRouteLoading =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n swapStep === \"idle\" &&\n swapType === \"exactOut\" &&\n Boolean(toToken && (receiveMaxCalculating || (amount && Number(amount) > 0))) &&\n !exactOutInsufficientSourceIssue &&\n (quoteRefreshing || intentLoading || receiveMaxCalculating);\n const hasCurrentRunnableIntent =\n Boolean(intentData && swapIntentRef.current) &&\n swapIntentRef.current?.runId === swapRunIdRef.current &&\n !intentLoading;\n const hasIntentSources = Boolean((intentData?.sources ?? []).length > 0);\n const isQuoteUnavailableForAutoSourceFlow =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n Boolean(hasPositiveDecimalInput(amount) && toToken) &&\n !quoteRefreshing &&\n !receiveMaxCalculating &&\n !intentLoading &&\n !exactOutInsufficientSourceIssue &&\n (!hasCurrentRunnableIntent || !hasIntentSources);\n const hasPositiveRootAmount = hasPositiveDecimalInput(amount);\n const hasReadySwapQuoteInput = hasReadyExactInSwapInput(fromTokens, toToken);\n const needsWalletConnection = !ownerAddress || !nexusSDK;\n const walletConnectBusy =\n walletActionPending ||\n nexusLoading ||\n isWalletConnectPending ||\n walletStatus === \"connecting\";\n const walletCtaLabel = walletConnectBusy ? \"Connecting...\" : \"Connect Wallet\";\n const isSwapCtaDisabled =\n needsWalletConnection\n ? walletConnectBusy\n : !hasReadySwapQuoteInput ||\n receiveMaxCalculating ||\n quoteRefreshing ||\n Boolean(exactOutInsufficientSourceIssue);\n const isDepositCtaDisabled =\n needsWalletConnection\n ? walletConnectBusy\n : !hasPositiveRootAmount ||\n !toToken ||\n quoteRefreshing ||\n receiveMaxCalculating ||\n isQuoteUnavailableForAutoSourceFlow ||\n Boolean(exactOutInsufficientSourceIssue);\n const sendNeedsRecipient = activeMode === \"send\" && !recipientAddress;\n const isSendCtaDisabled =\n needsWalletConnection\n ? walletConnectBusy\n : !hasPositiveRootAmount ||\n !toToken ||\n hasSameOwnerSendRecipient ||\n receiveMaxCalculating ||\n (!sendNeedsRecipient &&\n (quoteRefreshing || isQuoteUnavailableForAutoSourceFlow)) ||\n Boolean(exactOutInsufficientSourceIssue);\n const quoteCtaLabel = (fallback: string) => {\n if (needsWalletConnection) return walletCtaLabel;\n if (exactOutInsufficientSourceIssue) return \"Insufficient balance\";\n if (receiveMaxCalculating) return \"Calculating...\";\n if (quoteRefreshing) return \"Intent fetching...\";\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 (exactOutInsufficientSourceIssue) return \"Insufficient balance\";\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) => sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0),\n );\n const previewSourceUsdNumber =\n 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 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 resolvedToToken =\n toToken ??\n (activeMode === \"deposit\" && selectedOpportunity\n ? toTokenFromOpportunity(selectedOpportunity)\n : undefined);\n const toTokenWithFetchedBalance =\n resolvedToToken && destinationBalance\n ? { ...resolvedToToken, balance: destinationBalance }\n : resolvedToToken;\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: parseFiatNumber(intentData?.destination?.amount),\n producedUsd: parseFiatNumber(intentData?.destination?.value),\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 = shouldShowPredictiveExactOutDisplay\n ? predictiveExactOutQuote?.sources ?? fromTokens\n : fromTokens;\n const displayFromTokens = (() => {\n if (\n !destinationBalanceDisplayToken ||\n (activeMode !== \"deposit\" && activeMode !== \"send\")\n ) {\n return baseDisplayFromTokens;\n }\n\n const destinationKey = getTokenSelectionKey(destinationBalanceDisplayToken);\n let replacedEmptyDestinationToken = false;\n const tokens = baseDisplayFromTokens.map((token) => {\n const isDestinationToken =\n 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 return replacedEmptyDestinationToken\n ? tokens\n : [...tokens, destinationBalanceDisplayToken];\n })();\n const displayExactOutRouteLoading =\n isExactOutRouteLoading && !shouldShowPredictiveExactOutDisplay;\n const totalSwapBalanceUsd = getSwapBalanceTotalUsd()\n .toDecimalPlaces(2)\n .toFixed();\n const sendAmountUsd =\n amount && toToken\n ? getTokenUsdValue(\n {\n ...toToken,\n userAmount: amount,\n userAmountMode: \"token\",\n },\n amount,\n ).toNumber()\n : 0;\n const isIdleSwapQuoteLoading =\n activeMode === \"swap\" && swapStep === \"idle\" && quoteRefreshing;\n const isReceiveAmountLoading =\n receiveMaxCalculating ||\n (isIdleSwapQuoteLoading && swapType === \"exactIn\" && !idleReceiveQuoteAmount);\n const isReceiveUsdLoading =\n receiveMaxCalculating ||\n (isIdleSwapQuoteLoading && swapType === \"exactIn\" && !idleReceiveQuoteUsd);\n const hasQuoteRefreshCountdown =\n (activeMode === \"swap\" || activeMode === \"deposit\" || activeMode === \"send\") &&\n Boolean(intentData && swapIntentRef.current) &&\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 isDrawerOverlayActive =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n swapStep === \"enter-recipient\" ||\n closingDrawerStep !== null;\n\n return (\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 {/* Protocol chip appended next to Title when Deposit Protocol selected */}\n {isTitleCentered() &&\n activeMode === \"deposit\" &&\n swapStep === \"idle\" &&\n selectedOpportunity && (\n
\n {\n clearPendingSwapIntent();\n setSelectedOpportunity(undefined);\n setToToken(undefined);\n clearSelectedSources();\n setAmount(\"\");\n setDepositAmountMode(\"token\");\n }}\n className=\"flex items-center gap-1 pl-2 pr-1.5 py-1 rounded-[4px] hover:bg-black/5 transition-colors\"\n style={{\n fontFamily: \"var(--font-geist-mono), sans-serif\",\n fontSize: \"10px\",\n fontWeight: 500,\n color: \"var(--foreground-muted, #848483)\",\n background: \"var(--background-tertiary, #F0F0EF)\",\n border: \"none\",\n cursor: \"pointer\",\n }}\n >\n {selectedOpportunity.title || selectedOpportunity.protocol}\n \n \n
\n )}\n \n\n {/* Right side icons */}\n \n {hasQuoteRefreshCountdown && (\n \n )}\n setSwapStep(\"history\")}\n style={{\n alignItems: \"center\",\n backgroundColor: \"#FFFFFE\",\n borderRadius: \"8px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n height: \"32px\",\n justifyContent: \"center\",\n outline: \"1px solid #E8E8E7\",\n width: \"32px\",\n cursor: \"pointer\",\n border: \"none\",\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 />\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 fromTokens={fromTokens}\n toToken={toTokenWithFetchedBalance}\n receiveQuoteUsd={idleReceiveQuoteUsd}\n sourceRouteStatus={\n exactOutInsufficientSourceIssue\n ? \"insufficient\"\n : isExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n sourceRouteMessage={exactOutInsufficientSourceIssue?.message}\n totalBalance={totalSwapBalanceUsd}\n usdValue={amount && usdValue > 0 ? usdValue.toFixed(2) : \"\"}\n swapType={swapType}\n onOpenSourcePicker={(index) => {\n setEditingAssetIndex(index ?? null);\n openDrawerStep(\"choose-swap-asset\");\n }}\n onOpenDestPicker={() => openDrawerStep(\"choose-receive-asset\")}\n onOpenRecipientPicker={handleOpenRecipientEditor}\n recipientAddress={effectiveRecipientAddress}\n defaultRecipientAddress={defaultRecipientAddress}\n onUpdateTokens={setFromTokens}\n />\n\n {txError && !exactOutInsufficientSourceIssue && (\n \n )}\n\n {/* CTA Button */}\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n void handleEnterPreview();\n }}\n disabled={isSwapCtaDisabled}\n style={{\n alignItems: \"center\",\n backgroundColor: exactOutInsufficientSourceIssue\n ? \"#FCEEED\"\n : isSwapCtaDisabled\n\t ? \"#F0F0EF\"\n\t : \"#006BF4\",\n\t border: exactOutInsufficientSourceIssue\n\t ? \"1px solid #F7C4C1\"\n\t : \"none\",\n\t borderRadius: exactOutInsufficientSourceIssue ? \"4px\" : \"8px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"8px\",\n height: \"48px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n\t cursor: isSwapCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {exactOutInsufficientSourceIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n quoteRefreshing ||\n receiveMaxCalculating ? (\n \n ) : null}\n \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 {/* Opportunity list */}\n {config.opportunities &&\n config.opportunities.length > 0 &&\n !selectedOpportunity && (\n <>\n \n\n {/* Done button for opportunity selection */}\n \n {\n const opportunity =\n pendingOpportunity ?? config.opportunities?.[0];\n if (opportunity) {\n handleSelectDepositOpportunity(opportunity);\n setSwapStep(\"idle\");\n }\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: \"#006BF4\",\n borderRadius: \"8px\",\n boxShadow: \"#5555550D 0px 1px 4px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flex: 1,\n height: \"48px\",\n justifyContent: \"center\",\n border: \"none\",\n cursor: \"pointer\",\n }}\n >\n \n Done\n \n \n \n \n )}\n\n {/* After opportunity selected โ€” show deposit form */}\n {(!config.opportunities ||\n config.opportunities.length === 0 ||\n selectedOpportunity) && (\n <>\n openDrawerStep(\"choose-swap-asset\")}\n onSetPercent={handleDepositPercentSelect}\n routeStatus={\n exactOutInsufficientSourceIssue\n ? \"insufficient\"\n : displayExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n routeMessage={exactOutInsufficientSourceIssue?.message}\n isCalculatingMax={receiveMaxCalculating}\n calculatingPercent={maxCalculationPercent}\n isQuoteRefreshing={quoteRefreshing || intentLoading}\n showAutoBadge={!sourceSelectionTouched}\n />\n\n {txError && !exactOutInsufficientSourceIssue && (\n \n )}\n\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n void handleEnterPreview();\n }}\n disabled={isDepositCtaDisabled}\n style={{\n alignItems: \"center\",\n backgroundColor: exactOutInsufficientSourceIssue\n ? \"#FCEEED\"\n : isDepositCtaDisabled\n\t ? \"#F0F0EF\"\n\t : \"#006BF4\",\n\t border: exactOutInsufficientSourceIssue\n\t ? \"1px solid #F7C4C1\"\n\t : \"none\",\n\t borderRadius: exactOutInsufficientSourceIssue ? \"4px\" : \"8px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"8px\",\n height: \"48px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n\t cursor: isDepositCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {exactOutInsufficientSourceIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n quoteRefreshing ||\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 0 ? sendAmountUsd.toFixed(2) : \"\"\n }\n onOpenAssetPicker={() => openDrawerStep(\"choose-receive-asset\")}\n onOpenSourcePicker={() => {\n setEditingAssetIndex(null);\n openDrawerStep(\"choose-swap-asset\");\n }}\n onOpenRecipientPicker={handleOpenRecipientEditor}\n recipientAddress={recipientAddress || \"\"}\n onSetPercent={handleSendPercentSelect}\n routeStatus={\n exactOutInsufficientSourceIssue\n ? \"insufficient\"\n : displayExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n routeMessage={exactOutInsufficientSourceIssue?.message}\n isCalculatingMax={receiveMaxCalculating}\n calculatingPercent={maxCalculationPercent}\n isQuoteRefreshing={quoteRefreshing}\n showAutoBadge={!sourceSelectionTouched}\n />\n\n {txError && !exactOutInsufficientSourceIssue && (\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 disabled={isSendCtaDisabled}\n style={{\n alignItems: \"center\",\n backgroundColor: exactOutInsufficientSourceIssue\n ? \"#FCEEED\"\n : isSendCtaDisabled\n\t ? \"#F0F0EF\"\n\t : \"#006BF4\",\n\t border: exactOutInsufficientSourceIssue\n\t ? \"1px solid #F7C4C1\"\n\t : \"none\",\n\t borderRadius: exactOutInsufficientSourceIssue ? \"4px\" : \"8px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"8px\",\n height: \"48px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n\t cursor: isSendCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {exactOutInsufficientSourceIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n (!sendNeedsRecipient &&\n (quoteRefreshing || 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 />\n \n \n \n \n \n {\n setTxError(null);\n closeDrawerToIdle();\n }}\n aria-label=\"Back\"\n style={{\n alignItems: \"center\",\n backgroundColor: \"#FFFFFE\",\n border: \"1px solid #E8E8E7\",\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 >\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 label={null}\n placeholder=\"Wallet address\"\n hasError={Boolean(txError)}\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 0\n ? sendAmountUsd.toFixed(2)\n : undefined\n }\n selectedTokens={fromTokens}\n editingAssetIndex={editingAssetIndex}\n onSelectionChange={\n activeMode === \"deposit\" || activeMode === \"send\"\n ? (tokens) => {\n setSourceSelectionTouched(true);\n setExactOutQuoteSourceModeValue(\"selected\");\n invalidateExactOutQuoteForRefresh();\n setSourceSelectionRevision((current) => current + 1);\n setFromTokens(\n tokens.map((token) => ({\n ...token,\n userAmount: \"\",\n })),\n );\n }\n : undefined\n }\n onClearSelection={\n activeMode === \"deposit\" || activeMode === \"send\"\n ? () => {\n setSourceSelectionTouched(true);\n setExactOutQuoteSourceModeValue(\"selected\");\n invalidateExactOutQuoteForRefresh();\n setSourceSelectionRevision((current) => current + 1);\n setFromTokens((current) =>\n current.length === 0 ? current : [],\n );\n }\n : undefined\n }\n onToggle={(token) => {\n if (activeMode === \"deposit\" || activeMode === \"send\") {\n setSourceSelectionTouched(true);\n setExactOutQuoteSourceModeValue(\"selected\");\n invalidateExactOutQuoteForRefresh();\n setSourceSelectionRevision((current) => current + 1);\n } else {\n clearPendingSwapIntent();\n }\n setFromTokens((prev) => {\n const isSameSelection = (a: SwapTokenOption, b: SwapTokenOption) => {\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 isDepositOrSendSourcePicker =\n activeMode === \"deposit\" || activeMode === \"send\";\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\" && prev.length === 0\n ? amount\n : \"\",\n });\n\n if (\n isDepositOrSendSourcePicker &&\n token.isUnified &&\n sourceTokens.length > 0\n ) {\n const hasUnifiedSelection = prev.some(isSameUnifiedGroup);\n const areAllChildrenSelected = sourceTokens.every((source) =>\n prev.some((item) => isSameSelection(item, source)),\n );\n const withoutGroup = prev.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) => withDefaultAmount(source)),\n ];\n }\n\n if (isDepositOrSendSourcePicker && !token.isUnified) {\n const unifiedSelection = prev.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 = prev.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 = prev.find((item) =>\n isSameSelection(item, token),\n );\n if (exists) {\n return prev.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 = prev.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 [\n ...next,\n withDefaultAmount(token),\n ];\n });\n }}\n onDone={closeDrawerToIdle}\n onSelect={(token) => {\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 setSourceSelectionTouched(true);\n setExactOutQuoteSourceModeValue(\"selected\");\n invalidateExactOutQuoteForRefresh();\n setSourceSelectionRevision((current) => current + 1);\n setFromTokens([{ ...token, userAmount: amount }]);\n closeDrawerToIdle();\n }\n }}\n onBack={closeDrawerToIdle}\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 (activeMode === \"send\" || activeMode === \"deposit\") {\n setExactOutQuoteSourceModeValue(\"all\");\n if (tokenChanged) {\n clearPendingSwapIntent();\n setAmount(\"\");\n }\n setSwapType(\"exactOut\");\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 onBack={closeDrawerToIdle}\n />\n \n \n )}\n\n \n );\n}\n\nexport default NexusOne;\n", "type": "registry:component", "target": "components/nexus-one/nexus-one.tsx" }, diff --git a/public/r/nexus-provider.json b/public/r/nexus-provider.json index abb472b..5f98f6a 100644 --- a/public/r/nexus-provider.json +++ b/public/r/nexus-provider.json @@ -11,7 +11,7 @@ "files": [ { "path": "registry/nexus-elements/nexus/NexusProvider.tsx", - "content": "\"use client\";\nimport {\n type EthereumProvider,\n type NexusNetwork,\n NexusSDK,\n type OnAllowanceHookData,\n type OnIntentHookData,\n type OnSwapIntentHookData,\n type SupportedChainsAndTokensResult,\n type SupportedChainsResult,\n type UserAsset,\n} from \"@avail-project/nexus-core\";\n\nimport {\n createContext,\n type RefObject,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { useAccountEffect } from \"wagmi\";\nimport {\n DEFAULT_USD_PEGGED_TOKEN_SYMBOLS,\n USD_PEGGED_FALLBACK_RATE,\n buildUsdPeggedSymbolSet,\n fetchCoinGeckoUsdRate,\n fetchCoinbaseUsdRate,\n getCoinbaseSymbolCandidates,\n normalizeTokenSymbol,\n toFinitePositiveNumber,\n} from \"../common/utils/token-pricing\";\n\ninterface NexusContextType {\n nexusSDK: NexusSDK | null;\n bridgableBalance: UserAsset[] | null;\n swapBalance: UserAsset[] | null;\n intent: RefObject;\n allowance: RefObject;\n swapIntent: RefObject;\n exchangeRate: Record | null;\n supportedChainsAndTokens: SupportedChainsAndTokensResult | null;\n swapSupportedChainsAndTokens: SupportedChainsResult | null;\n network?: NexusNetwork;\n loading: boolean;\n handleInit: (provider: EthereumProvider) => Promise;\n fetchBridgableBalance: () => Promise;\n fetchSwapBalance: () => Promise;\n getFiatValue: (amount: number, token: string) => number;\n resolveTokenUsdRate: (tokenSymbol: string) => Promise;\n initializeNexus: (provider: EthereumProvider) => Promise;\n deinitializeNexus: () => Promise;\n attachEventHooks: () => void;\n}\n\nconst NexusContext = createContext(undefined);\n\ntype NexusProviderProps = {\n children: React.ReactNode;\n config?: {\n network?: NexusNetwork;\n debug?: boolean;\n };\n};\n\nconst defaultConfig: Required = {\n network: \"mainnet\",\n debug: true,\n};\n\nconst NexusProvider = ({\n children,\n config = defaultConfig,\n}: NexusProviderProps) => {\n const stableConfig = useMemo(\n () => ({ ...defaultConfig, ...config }),\n [config],\n );\n\n const sdkRef = useRef(null);\n const [sdk, setSdk] = useState(null);\n const [nexusSDK, setNexusSDK] = useState(null);\n const [loading, setLoading] = useState(false);\n const supportedChainsAndTokens =\n useRef(null);\n const swapSupportedChainsAndTokens = useRef(\n null,\n );\n const [bridgableBalance, setBridgableBalance] = useState(\n null,\n );\n const [swapBalance, setSwapBalance] = useState(null);\n const [exchangeRateState, setExchangeRateState] = useState | null>(null);\n const exchangeRate = useRef | null>(null);\n const coinbaseUsdRateCache = useRef>({});\n const coinbaseUsdRateRequests = useRef<\n Record>\n >({});\n const usdPeggedSymbols = useRef>(\n new Set(DEFAULT_USD_PEGGED_TOKEN_SYMBOLS),\n );\n\n const intent = useRef(null);\n const allowance = useRef(null);\n const swapIntent = useRef(null);\n\n useEffect(() => {\n const nextSdk = new NexusSDK({\n ...stableConfig,\n });\n\n sdkRef.current = nextSdk;\n setSdk(nextSdk);\n\n return () => {\n void nextSdk.deinit().catch((error) => {\n console.error(\"Error deinitializing Nexus:\", error);\n });\n if (sdkRef.current === nextSdk) {\n sdkRef.current = null;\n }\n setSdk(null);\n setNexusSDK(null);\n };\n }, [stableConfig]);\n\n const cacheUsdRate = useCallback((tokenSymbol: string, usdRate: number) => {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n const rate = toFinitePositiveNumber(usdRate);\n if (!normalized || !rate) return;\n\n coinbaseUsdRateCache.current[normalized] = rate;\n const currentRates = exchangeRate.current ?? {};\n if (currentRates[normalized] === rate) return;\n\n const nextRates = {\n ...currentRates,\n [normalized]: rate,\n };\n exchangeRate.current = nextRates;\n setExchangeRateState(nextRates);\n }, []);\n\n const getUsdRateFromLocalSources = useCallback((tokenSymbol: string) => {\n const normalizedSymbol = normalizeTokenSymbol(tokenSymbol);\n if (!normalizedSymbol) return 0;\n\n for (const candidate of getCoinbaseSymbolCandidates(normalizedSymbol)) {\n const sdkRate = toFinitePositiveNumber(exchangeRate.current?.[candidate]);\n if (sdkRate) return sdkRate;\n\n const cachedRate = toFinitePositiveNumber(\n coinbaseUsdRateCache.current[candidate],\n );\n if (cachedRate) return cachedRate;\n }\n\n if (usdPeggedSymbols.current.has(normalizedSymbol)) {\n return USD_PEGGED_FALLBACK_RATE;\n }\n\n return 0;\n }, []);\n\n const normalizeUserAssetFiatValues = useCallback(\n (assets: UserAsset[] | null): UserAsset[] | null => {\n if (!assets) return assets;\n\n return assets.map((asset) => {\n let computedAssetUsd = 0;\n\n const breakdown = (asset.breakdown ?? []).map((entry) => {\n const balance = Number.parseFloat(String(entry.balance ?? \"0\"));\n const safeBalance =\n Number.isFinite(balance) && balance > 0 ? balance : 0;\n const existingUsd = Number.parseFloat(\n String(entry.balanceInFiat ?? \"0\"),\n );\n const safeExistingUsd =\n Number.isFinite(existingUsd) && existingUsd >= 0 ? existingUsd : 0;\n\n let normalizedUsd = safeExistingUsd;\n if (safeBalance > 0 && normalizedUsd <= 0) {\n const rate = getUsdRateFromLocalSources(\n entry.symbol ?? asset.symbol,\n );\n if (rate > 0) {\n normalizedUsd = safeBalance * rate;\n }\n }\n\n computedAssetUsd += normalizedUsd;\n return {\n ...entry,\n balanceInFiat: normalizedUsd,\n };\n });\n\n const assetBalance = Number.parseFloat(String(asset.balance ?? \"0\"));\n const safeAssetBalance =\n Number.isFinite(assetBalance) && assetBalance > 0 ? assetBalance : 0;\n const rawAssetUsd = Number.parseFloat(\n String(asset.balanceInFiat ?? \"0\"),\n );\n const safeAssetUsd =\n Number.isFinite(rawAssetUsd) && rawAssetUsd >= 0 ? rawAssetUsd : 0;\n\n let normalizedAssetUsd = safeAssetUsd;\n if (normalizedAssetUsd <= 0) {\n if (computedAssetUsd > 0) {\n normalizedAssetUsd = computedAssetUsd;\n } else if (safeAssetBalance > 0) {\n const rate = getUsdRateFromLocalSources(asset.symbol);\n if (rate > 0) {\n normalizedAssetUsd = safeAssetBalance * rate;\n }\n }\n }\n\n return {\n ...asset,\n balanceInFiat: normalizedAssetUsd,\n breakdown,\n };\n });\n },\n [getUsdRateFromLocalSources],\n );\n\n const resolveTokenUsdRate = useCallback(\n async (tokenSymbol: string) => {\n const normalizedSymbol = normalizeTokenSymbol(tokenSymbol);\n if (!normalizedSymbol) return null;\n\n const sdkRate = toFinitePositiveNumber(\n exchangeRate.current?.[normalizedSymbol],\n );\n if (sdkRate) {\n return sdkRate;\n }\n\n const cachedRate = toFinitePositiveNumber(\n coinbaseUsdRateCache.current[normalizedSymbol],\n );\n if (cachedRate) {\n return cachedRate;\n }\n\n const inFlightRequest = coinbaseUsdRateRequests.current[normalizedSymbol];\n if (inFlightRequest) {\n return inFlightRequest;\n }\n\n const requestPromise = (async (): Promise => {\n for (const candidate of getCoinbaseSymbolCandidates(normalizedSymbol)) {\n const sdkCandidateRate = toFinitePositiveNumber(\n exchangeRate.current?.[candidate],\n );\n if (sdkCandidateRate) {\n cacheUsdRate(normalizedSymbol, sdkCandidateRate);\n return sdkCandidateRate;\n }\n\n const cachedCandidateRate = toFinitePositiveNumber(\n coinbaseUsdRateCache.current[candidate],\n );\n if (cachedCandidateRate) {\n cacheUsdRate(normalizedSymbol, cachedCandidateRate);\n return cachedCandidateRate;\n }\n }\n\n const coinbaseRate = await fetchCoinbaseUsdRate(normalizedSymbol);\n if (coinbaseRate) {\n cacheUsdRate(normalizedSymbol, coinbaseRate);\n return coinbaseRate;\n }\n\n const coinGeckoRate = await fetchCoinGeckoUsdRate(normalizedSymbol);\n if (coinGeckoRate) {\n cacheUsdRate(normalizedSymbol, coinGeckoRate);\n return coinGeckoRate;\n }\n\n if (usdPeggedSymbols.current.has(normalizedSymbol)) {\n cacheUsdRate(normalizedSymbol, USD_PEGGED_FALLBACK_RATE);\n return USD_PEGGED_FALLBACK_RATE;\n }\n\n return null;\n })();\n\n coinbaseUsdRateRequests.current[normalizedSymbol] = requestPromise;\n try {\n return await requestPromise;\n } finally {\n delete coinbaseUsdRateRequests.current[normalizedSymbol];\n }\n },\n [cacheUsdRate],\n );\n\n const setupNexus = useCallback(async () => {\n if (!sdk) return;\n const list = sdk.utils.getSupportedChains(\n config?.network === \"testnet\" ? 0 : undefined,\n );\n supportedChainsAndTokens.current = list ?? null;\n usdPeggedSymbols.current = buildUsdPeggedSymbolSet(list ?? null);\n const swapList = sdk.utils.getSwapSupportedChainsAndTokens();\n swapSupportedChainsAndTokens.current = swapList ?? null;\n const [bridgeAbleBalanceResult, swapBalanceResult, rates] =\n await Promise.allSettled([\n sdk.getBalancesForBridge(),\n sdk.getBalancesForSwap(false),\n sdk.utils.getCoinbaseRates(),\n ]);\n\n if (rates?.status === \"fulfilled\") {\n // Coinbase returns \"units per USD\" (e.g., 1 USD = 0.00028 ETH).\n // Convert to \"USD per unit\" (e.g., 1 ETH = ~$3514) for straightforward UI calculations.\n const usdPerUnit: Record = {};\n\n for (const [symbol, value] of Object.entries(rates.value)) {\n const unitsPerUsd = Number.parseFloat(String(value));\n if (Number.isFinite(unitsPerUsd) && unitsPerUsd > 0) {\n usdPerUnit[normalizeTokenSymbol(symbol)] = 1 / unitsPerUsd;\n }\n }\n exchangeRate.current = usdPerUnit;\n setExchangeRateState(usdPerUnit);\n }\n\n if (bridgeAbleBalanceResult.status === \"fulfilled\") {\n setBridgableBalance(\n normalizeUserAssetFiatValues(bridgeAbleBalanceResult.value),\n );\n }\n\n if (swapBalanceResult.status === \"fulfilled\") {\n setSwapBalance(normalizeUserAssetFiatValues(swapBalanceResult.value));\n }\n }, [sdk, config?.network, normalizeUserAssetFiatValues]);\n\n const initializeNexus = useCallback(\n async (provider: EthereumProvider) => {\n if (!sdk) {\n throw new Error(\"Nexus SDK is not ready\");\n }\n setLoading(true);\n try {\n if (!sdk.isInitialized()) {\n await sdk.initialize(provider);\n }\n setNexusSDK(sdk);\n } catch (error) {\n console.error(\"Error initializing Nexus:\", error);\n throw error;\n } finally {\n setLoading(false);\n }\n },\n [sdk],\n );\n\n const deinitializeNexus = useCallback(async () => {\n try {\n const activeSdk = nexusSDK ?? sdkRef.current;\n if (!activeSdk) return;\n if (activeSdk.isInitialized()) {\n await activeSdk.deinit();\n }\n setNexusSDK(null);\n supportedChainsAndTokens.current = null;\n swapSupportedChainsAndTokens.current = null;\n setBridgableBalance(null);\n setSwapBalance(null);\n exchangeRate.current = null;\n setExchangeRateState(null);\n coinbaseUsdRateCache.current = {};\n coinbaseUsdRateRequests.current = {};\n usdPeggedSymbols.current = new Set(DEFAULT_USD_PEGGED_TOKEN_SYMBOLS);\n intent.current = null;\n swapIntent.current = null;\n allowance.current = null;\n setLoading(false);\n } catch (error) {\n console.error(\"Error deinitializing Nexus:\", error);\n }\n }, [nexusSDK]);\n\n const attachEventHooks = useCallback(() => {\n if (!sdk) return;\n sdk.setOnAllowanceHook((data: OnAllowanceHookData) => {\n /**\n * Useful when you want the user to select, min, max or a custom value\n * Can use this to capture data and then show it on the UI\n * @see - always call data.allow() to progress the flow, otherwise it will stay stuck here.\n * const {allow, sources, deny} = data\n * @example allow(['min', 'max', '0.5']), the array in allow function should match number of sources.\n * You can skip setting this hook if you want, sdk will auto progress if this hook is not attached\n */\n allowance.current = data;\n });\n\n sdk.setOnIntentHook((data: OnIntentHookData) => {\n /**\n * Useful when you want to capture the intent, and display it on the UI (bridge, bridgeAndTransfer, bridgeAndExecute)\n * const {allow, deny, intent, refresh} = data\n * @see - always call data.allow() to progress the flow, otherwise it will stay stuck here.\n * deny() to reject the intent\n * refresh() to refresh the intent, best to call refresh in 15 second intervals\n * data.intent -> details about the intent, useful when wanting to display info on UI\n * You can skip setting this hook if you want, sdk will auto progress if this hook is not attached\n */\n intent.current = data;\n });\n\n sdk.setOnSwapIntentHook((data: OnSwapIntentHookData) => {\n /**\n * Same behaviour and function as setOnIntentHook, except this one is for swaps exclusively\n */\n swapIntent.current = data;\n });\n }, [sdk]);\n\n const handleInit = useCallback(\n async (provider: EthereumProvider) => {\n if (!sdk) {\n throw new Error(\"Nexus SDK is not ready\");\n }\n if (sdk.isInitialized() || loading) {\n return;\n }\n if (!provider || typeof provider.request !== \"function\") {\n throw new Error(\"Invalid EIP-1193 provider\");\n }\n try {\n await initializeNexus(provider);\n if (!sdk.isInitialized()) return;\n await setupNexus();\n attachEventHooks();\n } catch (error) {\n console.error(\"Error during Nexus setup flow:\", error);\n throw error;\n }\n },\n [sdk, loading, initializeNexus, setupNexus, attachEventHooks],\n );\n\n const fetchBridgableBalance = useCallback(async () => {\n try {\n if (!sdk) return;\n const updatedBalance = await sdk.getBalancesForBridge();\n setBridgableBalance(normalizeUserAssetFiatValues(updatedBalance));\n } catch (error) {\n console.error(\"Error fetching bridgable balance:\", error);\n }\n }, [sdk, normalizeUserAssetFiatValues]);\n\n const fetchSwapBalance = useCallback(async () => {\n try {\n if (!sdk) return;\n const updatedBalance = await sdk.getBalancesForSwap(false);\n setSwapBalance(normalizeUserAssetFiatValues(updatedBalance));\n } catch (error) {\n console.error(\"Error fetching swap balance:\", error);\n }\n }, [sdk, normalizeUserAssetFiatValues]);\n\n const getFiatValue = useCallback(\n (amount: number, token: string) => {\n const rate = getUsdRateFromLocalSources(token);\n return rate * amount;\n },\n [getUsdRateFromLocalSources],\n );\n\n // Backfill USD values once rates arrive so downstream selectors/max logic\n // do not treat supported assets as $0 simply due to timing.\n useEffect(() => {\n if (!exchangeRateState) return;\n setSwapBalance((prev) => normalizeUserAssetFiatValues(prev));\n setBridgableBalance((prev) => normalizeUserAssetFiatValues(prev));\n }, [exchangeRateState, normalizeUserAssetFiatValues]);\n\n useAccountEffect({\n onDisconnect() {\n deinitializeNexus();\n },\n });\n\n const value = useMemo(\n () => ({\n nexusSDK,\n initializeNexus,\n deinitializeNexus,\n attachEventHooks,\n intent,\n allowance,\n handleInit,\n supportedChainsAndTokens: supportedChainsAndTokens.current,\n swapSupportedChainsAndTokens: swapSupportedChainsAndTokens.current,\n bridgableBalance,\n swapBalance: swapBalance,\n network: config?.network,\n loading,\n fetchBridgableBalance,\n fetchSwapBalance,\n swapIntent,\n exchangeRate: exchangeRateState,\n getFiatValue,\n resolveTokenUsdRate,\n }),\n [\n nexusSDK,\n initializeNexus,\n deinitializeNexus,\n attachEventHooks,\n handleInit,\n bridgableBalance,\n swapBalance,\n config,\n loading,\n fetchBridgableBalance,\n fetchSwapBalance,\n exchangeRateState,\n getFiatValue,\n resolveTokenUsdRate,\n ],\n );\n return (\n {children}\n );\n};\n\nexport function useNexus() {\n const context = useContext(NexusContext);\n if (!context) {\n throw new Error(\"useNexus must be used within a NexusProvider\");\n }\n return context;\n}\n\nexport default NexusProvider;\n", + "content": "\"use client\";\nimport {\n type EthereumProvider,\n type NexusNetwork,\n NexusSDK,\n type OnAllowanceHookData,\n type OnIntentHookData,\n type OnSwapIntentHookData,\n type SupportedChainsAndTokensResult,\n type SupportedChainsResult,\n type UserAsset,\n} from \"@avail-project/nexus-core\";\n\nimport {\n createContext,\n type RefObject,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { useAccountEffect } from \"wagmi\";\nimport {\n DEFAULT_USD_PEGGED_TOKEN_SYMBOLS,\n USD_PEGGED_FALLBACK_RATE,\n buildUsdPeggedSymbolSet,\n fetchCoinGeckoUsdRate,\n fetchCoinbaseUsdRate,\n getCoinbaseSymbolCandidates,\n normalizeTokenSymbol,\n toFinitePositiveNumber,\n} from \"../common/utils/token-pricing\";\n\ninterface NexusContextType {\n nexusSDK: NexusSDK | null;\n bridgableBalance: UserAsset[] | null;\n swapBalance: UserAsset[] | null;\n intent: RefObject;\n allowance: RefObject;\n swapIntent: RefObject;\n exchangeRate: Record | null;\n supportedChainsAndTokens: SupportedChainsAndTokensResult | null;\n swapSupportedChainsAndTokens: SupportedChainsResult | null;\n network?: NexusNetwork;\n loading: boolean;\n handleInit: (provider: EthereumProvider) => Promise;\n fetchBridgableBalance: () => Promise;\n fetchSwapBalance: () => Promise;\n getFiatValue: (amount: number, token: string) => number;\n resolveTokenUsdRate: (tokenSymbol: string) => Promise;\n initializeNexus: (provider: EthereumProvider) => Promise;\n deinitializeNexus: () => Promise;\n attachEventHooks: () => void;\n}\n\nconst NexusContext = createContext(undefined);\n\ntype NexusProviderProps = {\n children: React.ReactNode;\n config?: {\n network?: NexusNetwork;\n debug?: boolean;\n };\n};\n\nconst defaultConfig: Required = {\n network: \"mainnet\",\n debug: true,\n};\n\nconst NexusProvider = ({\n children,\n config = defaultConfig,\n}: NexusProviderProps) => {\n const stableConfig = useMemo(\n () => ({ ...defaultConfig, ...config }),\n [config],\n );\n\n const sdkRef = useRef(null);\n const [sdk, setSdk] = useState(null);\n const [nexusSDK, setNexusSDK] = useState(null);\n const [loading, setLoading] = useState(false);\n const supportedChainsAndTokens =\n useRef(null);\n const swapSupportedChainsAndTokens = useRef(\n null,\n );\n const [supportedChainsAndTokensState, setSupportedChainsAndTokensState] =\n useState(null);\n const [\n swapSupportedChainsAndTokensState,\n setSwapSupportedChainsAndTokensState,\n ] = useState(null);\n const [bridgableBalance, setBridgableBalance] = useState(\n null,\n );\n const [swapBalance, setSwapBalance] = useState(null);\n const [exchangeRateState, setExchangeRateState] = useState | null>(null);\n const exchangeRate = useRef | null>(null);\n const coinbaseUsdRateCache = useRef>({});\n const coinbaseUsdRateRequests = useRef<\n Record>\n >({});\n const usdPeggedSymbols = useRef>(\n new Set(DEFAULT_USD_PEGGED_TOKEN_SYMBOLS),\n );\n\n const intent = useRef(null);\n const allowance = useRef(null);\n const swapIntent = useRef(null);\n\n useEffect(() => {\n const nextSdk = new NexusSDK({\n ...stableConfig,\n });\n\n sdkRef.current = nextSdk;\n setSdk(nextSdk);\n\n return () => {\n void nextSdk.deinit().catch((error) => {\n console.error(\"Error deinitializing Nexus:\", error);\n });\n if (sdkRef.current === nextSdk) {\n sdkRef.current = null;\n }\n setSdk(null);\n setNexusSDK(null);\n };\n }, [stableConfig]);\n\n const cacheUsdRate = useCallback((tokenSymbol: string, usdRate: number) => {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n const rate = toFinitePositiveNumber(usdRate);\n if (!normalized || !rate) return;\n\n coinbaseUsdRateCache.current[normalized] = rate;\n const currentRates = exchangeRate.current ?? {};\n if (currentRates[normalized] === rate) return;\n\n const nextRates = {\n ...currentRates,\n [normalized]: rate,\n };\n exchangeRate.current = nextRates;\n setExchangeRateState(nextRates);\n }, []);\n\n const getUsdRateFromLocalSources = useCallback((tokenSymbol: string) => {\n const normalizedSymbol = normalizeTokenSymbol(tokenSymbol);\n if (!normalizedSymbol) return 0;\n\n for (const candidate of getCoinbaseSymbolCandidates(normalizedSymbol)) {\n const sdkRate = toFinitePositiveNumber(exchangeRate.current?.[candidate]);\n if (sdkRate) return sdkRate;\n\n const cachedRate = toFinitePositiveNumber(\n coinbaseUsdRateCache.current[candidate],\n );\n if (cachedRate) return cachedRate;\n }\n\n if (usdPeggedSymbols.current.has(normalizedSymbol)) {\n return USD_PEGGED_FALLBACK_RATE;\n }\n\n return 0;\n }, []);\n\n useEffect(() => {\n if (!sdk) return;\n\n let cancelled = false;\n const list = sdk.utils.getSupportedChains(\n stableConfig.network === \"testnet\" ? 0 : undefined,\n );\n const swapList = sdk.utils.getSwapSupportedChainsAndTokens();\n supportedChainsAndTokens.current = list ?? null;\n swapSupportedChainsAndTokens.current = swapList ?? null;\n usdPeggedSymbols.current = buildUsdPeggedSymbolSet(list ?? null);\n setSupportedChainsAndTokensState(list ?? null);\n setSwapSupportedChainsAndTokensState(swapList ?? null);\n\n void sdk.utils\n .getCoinbaseRates()\n .then((rates) => {\n if (cancelled) return;\n const usdPerUnit: Record = {};\n\n for (const [symbol, value] of Object.entries(rates)) {\n const unitsPerUsd = Number.parseFloat(String(value));\n if (Number.isFinite(unitsPerUsd) && unitsPerUsd > 0) {\n usdPerUnit[normalizeTokenSymbol(symbol)] = 1 / unitsPerUsd;\n }\n }\n exchangeRate.current = usdPerUnit;\n setExchangeRateState(usdPerUnit);\n })\n .catch((error) => {\n if (!cancelled) {\n console.warn(\"Unable to preload Nexus rates\", error);\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [sdk, stableConfig.network]);\n\n const normalizeUserAssetFiatValues = useCallback(\n (assets: UserAsset[] | null): UserAsset[] | null => {\n if (!assets) return assets;\n\n return assets.map((asset) => {\n let computedAssetUsd = 0;\n\n const breakdown = (asset.breakdown ?? []).map((entry) => {\n const balance = Number.parseFloat(String(entry.balance ?? \"0\"));\n const safeBalance =\n Number.isFinite(balance) && balance > 0 ? balance : 0;\n const existingUsd = Number.parseFloat(\n String(entry.balanceInFiat ?? \"0\"),\n );\n const safeExistingUsd =\n Number.isFinite(existingUsd) && existingUsd >= 0 ? existingUsd : 0;\n\n let normalizedUsd = safeExistingUsd;\n if (safeBalance > 0 && normalizedUsd <= 0) {\n const rate = getUsdRateFromLocalSources(\n entry.symbol ?? asset.symbol,\n );\n if (rate > 0) {\n normalizedUsd = safeBalance * rate;\n }\n }\n\n computedAssetUsd += normalizedUsd;\n return {\n ...entry,\n balanceInFiat: normalizedUsd,\n };\n });\n\n const assetBalance = Number.parseFloat(String(asset.balance ?? \"0\"));\n const safeAssetBalance =\n Number.isFinite(assetBalance) && assetBalance > 0 ? assetBalance : 0;\n const rawAssetUsd = Number.parseFloat(\n String(asset.balanceInFiat ?? \"0\"),\n );\n const safeAssetUsd =\n Number.isFinite(rawAssetUsd) && rawAssetUsd >= 0 ? rawAssetUsd : 0;\n\n let normalizedAssetUsd = safeAssetUsd;\n if (normalizedAssetUsd <= 0) {\n if (computedAssetUsd > 0) {\n normalizedAssetUsd = computedAssetUsd;\n } else if (safeAssetBalance > 0) {\n const rate = getUsdRateFromLocalSources(asset.symbol);\n if (rate > 0) {\n normalizedAssetUsd = safeAssetBalance * rate;\n }\n }\n }\n\n return {\n ...asset,\n balanceInFiat: normalizedAssetUsd,\n breakdown,\n };\n });\n },\n [getUsdRateFromLocalSources],\n );\n\n const resolveTokenUsdRate = useCallback(\n async (tokenSymbol: string) => {\n const normalizedSymbol = normalizeTokenSymbol(tokenSymbol);\n if (!normalizedSymbol) return null;\n\n const sdkRate = toFinitePositiveNumber(\n exchangeRate.current?.[normalizedSymbol],\n );\n if (sdkRate) {\n return sdkRate;\n }\n\n const cachedRate = toFinitePositiveNumber(\n coinbaseUsdRateCache.current[normalizedSymbol],\n );\n if (cachedRate) {\n return cachedRate;\n }\n\n const inFlightRequest = coinbaseUsdRateRequests.current[normalizedSymbol];\n if (inFlightRequest) {\n return inFlightRequest;\n }\n\n const requestPromise = (async (): Promise => {\n for (const candidate of getCoinbaseSymbolCandidates(normalizedSymbol)) {\n const sdkCandidateRate = toFinitePositiveNumber(\n exchangeRate.current?.[candidate],\n );\n if (sdkCandidateRate) {\n cacheUsdRate(normalizedSymbol, sdkCandidateRate);\n return sdkCandidateRate;\n }\n\n const cachedCandidateRate = toFinitePositiveNumber(\n coinbaseUsdRateCache.current[candidate],\n );\n if (cachedCandidateRate) {\n cacheUsdRate(normalizedSymbol, cachedCandidateRate);\n return cachedCandidateRate;\n }\n }\n\n const coinbaseRate = await fetchCoinbaseUsdRate(normalizedSymbol);\n if (coinbaseRate) {\n cacheUsdRate(normalizedSymbol, coinbaseRate);\n return coinbaseRate;\n }\n\n const coinGeckoRate = await fetchCoinGeckoUsdRate(normalizedSymbol);\n if (coinGeckoRate) {\n cacheUsdRate(normalizedSymbol, coinGeckoRate);\n return coinGeckoRate;\n }\n\n if (usdPeggedSymbols.current.has(normalizedSymbol)) {\n cacheUsdRate(normalizedSymbol, USD_PEGGED_FALLBACK_RATE);\n return USD_PEGGED_FALLBACK_RATE;\n }\n\n return null;\n })();\n\n coinbaseUsdRateRequests.current[normalizedSymbol] = requestPromise;\n try {\n return await requestPromise;\n } finally {\n delete coinbaseUsdRateRequests.current[normalizedSymbol];\n }\n },\n [cacheUsdRate],\n );\n\n const setupNexus = useCallback(async () => {\n if (!sdk) return;\n const list = sdk.utils.getSupportedChains(\n config?.network === \"testnet\" ? 0 : undefined,\n );\n supportedChainsAndTokens.current = list ?? null;\n setSupportedChainsAndTokensState(list ?? null);\n usdPeggedSymbols.current = buildUsdPeggedSymbolSet(list ?? null);\n const swapList = sdk.utils.getSwapSupportedChainsAndTokens();\n swapSupportedChainsAndTokens.current = swapList ?? null;\n setSwapSupportedChainsAndTokensState(swapList ?? null);\n const [bridgeAbleBalanceResult, swapBalanceResult, rates] =\n await Promise.allSettled([\n sdk.getBalancesForBridge(),\n sdk.getBalancesForSwap(false),\n sdk.utils.getCoinbaseRates(),\n ]);\n\n if (rates?.status === \"fulfilled\") {\n // Coinbase returns \"units per USD\" (e.g., 1 USD = 0.00028 ETH).\n // Convert to \"USD per unit\" (e.g., 1 ETH = ~$3514) for straightforward UI calculations.\n const usdPerUnit: Record = {};\n\n for (const [symbol, value] of Object.entries(rates.value)) {\n const unitsPerUsd = Number.parseFloat(String(value));\n if (Number.isFinite(unitsPerUsd) && unitsPerUsd > 0) {\n usdPerUnit[normalizeTokenSymbol(symbol)] = 1 / unitsPerUsd;\n }\n }\n exchangeRate.current = usdPerUnit;\n setExchangeRateState(usdPerUnit);\n }\n\n if (bridgeAbleBalanceResult.status === \"fulfilled\") {\n setBridgableBalance(\n normalizeUserAssetFiatValues(bridgeAbleBalanceResult.value),\n );\n }\n\n if (swapBalanceResult.status === \"fulfilled\") {\n setSwapBalance(normalizeUserAssetFiatValues(swapBalanceResult.value));\n }\n }, [sdk, config?.network, normalizeUserAssetFiatValues]);\n\n const initializeNexus = useCallback(\n async (provider: EthereumProvider) => {\n if (!sdk) {\n throw new Error(\"Nexus SDK is not ready\");\n }\n setLoading(true);\n try {\n if (!sdk.isInitialized()) {\n await sdk.initialize(provider);\n }\n setNexusSDK(sdk);\n } catch (error) {\n console.error(\"Error initializing Nexus:\", error);\n throw error;\n } finally {\n setLoading(false);\n }\n },\n [sdk],\n );\n\n const deinitializeNexus = useCallback(async () => {\n try {\n const activeSdk = nexusSDK ?? sdkRef.current;\n if (!activeSdk) return;\n if (activeSdk.isInitialized()) {\n await activeSdk.deinit();\n }\n setNexusSDK(null);\n setBridgableBalance(null);\n setSwapBalance(null);\n intent.current = null;\n swapIntent.current = null;\n allowance.current = null;\n setLoading(false);\n } catch (error) {\n console.error(\"Error deinitializing Nexus:\", error);\n }\n }, [nexusSDK]);\n\n const attachEventHooks = useCallback(() => {\n if (!sdk) return;\n sdk.setOnAllowanceHook((data: OnAllowanceHookData) => {\n /**\n * Useful when you want the user to select, min, max or a custom value\n * Can use this to capture data and then show it on the UI\n * @see - always call data.allow() to progress the flow, otherwise it will stay stuck here.\n * const {allow, sources, deny} = data\n * @example allow(['min', 'max', '0.5']), the array in allow function should match number of sources.\n * You can skip setting this hook if you want, sdk will auto progress if this hook is not attached\n */\n allowance.current = data;\n });\n\n sdk.setOnIntentHook((data: OnIntentHookData) => {\n /**\n * Useful when you want to capture the intent, and display it on the UI (bridge, bridgeAndTransfer, bridgeAndExecute)\n * const {allow, deny, intent, refresh} = data\n * @see - always call data.allow() to progress the flow, otherwise it will stay stuck here.\n * deny() to reject the intent\n * refresh() to refresh the intent, best to call refresh in 15 second intervals\n * data.intent -> details about the intent, useful when wanting to display info on UI\n * You can skip setting this hook if you want, sdk will auto progress if this hook is not attached\n */\n intent.current = data;\n });\n\n sdk.setOnSwapIntentHook((data: OnSwapIntentHookData) => {\n /**\n * Same behaviour and function as setOnIntentHook, except this one is for swaps exclusively\n */\n swapIntent.current = data;\n });\n }, [sdk]);\n\n const handleInit = useCallback(\n async (provider: EthereumProvider) => {\n if (!sdk) {\n throw new Error(\"Nexus SDK is not ready\");\n }\n if (sdk.isInitialized() || loading) {\n return;\n }\n if (!provider || typeof provider.request !== \"function\") {\n throw new Error(\"Invalid EIP-1193 provider\");\n }\n try {\n await initializeNexus(provider);\n if (!sdk.isInitialized()) return;\n await setupNexus();\n attachEventHooks();\n } catch (error) {\n console.error(\"Error during Nexus setup flow:\", error);\n throw error;\n }\n },\n [sdk, loading, initializeNexus, setupNexus, attachEventHooks],\n );\n\n const fetchBridgableBalance = useCallback(async () => {\n try {\n if (!sdk) return;\n const updatedBalance = await sdk.getBalancesForBridge();\n setBridgableBalance(normalizeUserAssetFiatValues(updatedBalance));\n } catch (error) {\n console.error(\"Error fetching bridgable balance:\", error);\n }\n }, [sdk, normalizeUserAssetFiatValues]);\n\n const fetchSwapBalance = useCallback(async () => {\n try {\n if (!sdk) return;\n const updatedBalance = await sdk.getBalancesForSwap(false);\n setSwapBalance(normalizeUserAssetFiatValues(updatedBalance));\n } catch (error) {\n console.error(\"Error fetching swap balance:\", error);\n }\n }, [sdk, normalizeUserAssetFiatValues]);\n\n const getFiatValue = useCallback(\n (amount: number, token: string) => {\n const rate = getUsdRateFromLocalSources(token);\n return rate * amount;\n },\n [getUsdRateFromLocalSources],\n );\n\n // Backfill USD values once rates arrive so downstream selectors/max logic\n // do not treat supported assets as $0 simply due to timing.\n useEffect(() => {\n if (!exchangeRateState) return;\n setSwapBalance((prev) => normalizeUserAssetFiatValues(prev));\n setBridgableBalance((prev) => normalizeUserAssetFiatValues(prev));\n }, [exchangeRateState, normalizeUserAssetFiatValues]);\n\n useAccountEffect({\n onDisconnect() {\n deinitializeNexus();\n },\n });\n\n const value = useMemo(\n () => ({\n nexusSDK,\n initializeNexus,\n deinitializeNexus,\n attachEventHooks,\n intent,\n allowance,\n handleInit,\n supportedChainsAndTokens: supportedChainsAndTokensState,\n swapSupportedChainsAndTokens: swapSupportedChainsAndTokensState,\n bridgableBalance,\n swapBalance: swapBalance,\n network: config?.network,\n loading,\n fetchBridgableBalance,\n fetchSwapBalance,\n swapIntent,\n exchangeRate: exchangeRateState,\n getFiatValue,\n resolveTokenUsdRate,\n }),\n [\n nexusSDK,\n initializeNexus,\n deinitializeNexus,\n attachEventHooks,\n handleInit,\n bridgableBalance,\n swapBalance,\n config,\n loading,\n fetchBridgableBalance,\n fetchSwapBalance,\n exchangeRateState,\n getFiatValue,\n resolveTokenUsdRate,\n supportedChainsAndTokensState,\n swapSupportedChainsAndTokensState,\n ],\n );\n return (\n {children}\n );\n};\n\nexport function useNexus() {\n const context = useContext(NexusContext);\n if (!context) {\n throw new Error(\"useNexus must be used within a NexusProvider\");\n }\n return context;\n}\n\nexport default NexusProvider;\n", "type": "registry:component", "target": "components/nexus/NexusProvider.tsx" } diff --git a/registry/nexus-elements/nexus-one/components/swap-intent-preview.tsx b/registry/nexus-elements/nexus-one/components/swap-intent-preview.tsx index aa166a1..515147a 100644 --- a/registry/nexus-elements/nexus-one/components/swap-intent-preview.tsx +++ b/registry/nexus-elements/nexus-one/components/swap-intent-preview.tsx @@ -694,7 +694,7 @@ export function SwapIntentPreview({ ) : parseDecimal(fromAmountUsd) : parseDecimal(fromAmountUsd); - const sourceUsdNumber = + const effectiveSourceUsdNumber = displayOnlyDestinationCoverageUsd !== undefined ? (intentSourceUsdNumber ?? new Decimal(0)).plus( displayOnlyDestinationCoverageUsd, @@ -707,9 +707,9 @@ export function SwapIntentPreview({ : (parseDecimal(normalizedIntentDest?.value) ?? parseDecimal(toAmountUsd)) : undefined; const hasFiatQuote = - sourceUsdNumber !== undefined && + effectiveSourceUsdNumber !== undefined && destinationUsdNumber !== undefined && - sourceUsdNumber.gt(0) && + effectiveSourceUsdNumber.gt(0) && destinationUsdNumber.gt(0); const bridgeFees = intentData?.feesAndBuffer?.bridge; @@ -754,12 +754,12 @@ export function SwapIntentPreview({ explicitFeeNumber ?? (hasFiatQuote ? new Decimal(0) : undefined); const priceImpactBaseUsd = hasFiatQuote && feeNumber !== undefined - ? sourceUsdNumber.minus(feeNumber).minus(swapBufferNumber ?? new Decimal(0)) + ? effectiveSourceUsdNumber.minus(feeNumber).minus(swapBufferNumber ?? new Decimal(0)) : undefined; const quoteImpactUsd = hasFiatQuote && feeNumber !== undefined ? Decimal.max( - sourceUsdNumber + effectiveSourceUsdNumber .minus(destinationUsdNumber) .minus(feeNumber) .minus(swapBufferNumber ?? new Decimal(0)), @@ -810,8 +810,8 @@ export function SwapIntentPreview({ const pendingLabel = isLoading ? "Fetching quote" : "Quote unavailable"; const pendingValue = isLoading ? "..." : "--"; const sourceUsd = - sourceUsdNumber !== undefined - ? `${formatAmount(sourceUsdNumber)} USD` + intentSourceUsdNumber !== undefined + ? `${formatAmount(intentSourceUsdNumber)} USD` : pendingValue; const receiveUsd = hasFiatQuote ? `${formatAmount(destinationUsdNumber)} USD` @@ -935,7 +935,9 @@ export function SwapIntentPreview({ })(); const sourceHeaderAmount = singleSourceHeader?.amount || - (sourceUsdNumber !== undefined ? formatAmount(sourceUsdNumber) : pendingValue); + (intentSourceUsdNumber !== undefined + ? formatAmount(intentSourceUsdNumber) + : pendingValue); const sourceHeaderUnit = singleSourceHeader?.symbol || "USD"; const sourceHeaderSubtitle = (() => { if (singleSourceHeader) { diff --git a/registry/nexus-elements/nexus-one/nexus-one.tsx b/registry/nexus-elements/nexus-one/nexus-one.tsx index 01ad70e..49cc265 100644 --- a/registry/nexus-elements/nexus-one/nexus-one.tsx +++ b/registry/nexus-elements/nexus-one/nexus-one.tsx @@ -43,10 +43,18 @@ import { ERROR_CODES, NEXUS_EVENTS, type BridgeStepType, + type EthereumProvider, type SwapStepType, + TOKEN_CONTRACT_ADDRESSES, TOKEN_METADATA, } from "@avail-project/nexus-core"; -import { useWalletClient, usePublicClient } from "wagmi"; +import { + useAccount, + useConnect, + useConnectorClient, + useWalletClient, + usePublicClient, +} from "wagmi"; import { erc20Abi, isAddress, @@ -126,10 +134,29 @@ type CachedIntentUsdRate = { value: string; }; +type PredictiveQuote = { + key: string; + mode: "exactIn" | "exactOut"; + sources?: SwapTokenOption[]; + toAmount?: string; + toUsd?: string; +}; + +type PredictiveQuoteBaseline = { + destinationUsdRate: string; + exactInDestinationAmountPerSourceUsd?: string; + exactOutSourceUsdPerDestinationUsd?: string; + updatedAt: number; +}; + const QUOTE_REFRESH_INTERVAL_MS = 30000; const EXACT_OUT_INPUT_DEBOUNCE_MS = 1000; const DRAWER_CLOSE_MS = 220; const MODAL_HEIGHT_TRANSITION_MS = 260; +const BASIS_POINTS = 10000; +const PREDICTIVE_EXACT_IN_DISCOUNT_BPS = 50; +const PREDICTIVE_EXACT_OUT_BUFFER_BPS = 100; +const PREDICTIVE_QUOTE_DISPLAY_DECIMALS = 8; const SWAP_HISTORY_STORAGE_KEY_PREFIX = "nexus-one-transaction-history-v1"; const waitForNextPaint = () => new Promise((resolve) => { @@ -1498,6 +1525,8 @@ export function NexusOne({ swapSupportedChainsAndTokens, supportedChainsAndTokens, fetchSwapBalance, + handleInit, + loading: nexusLoading, } = useNexus(); // Mode is a single value, not an array @@ -1519,7 +1548,14 @@ export function NexusOne({ } }, [nexusSDK]); + const { connector, status: walletStatus } = useAccount(); + const { + connectors, + connectAsync, + isPending: isWalletConnectPending, + } = useConnect(); const { data: walletClient } = useWalletClient(); + const { data: connectorClient } = useConnectorClient(); const publicClient = usePublicClient(); const walletClientAddress = walletClient?.account?.address; const ownerAddress = @@ -1541,6 +1577,7 @@ export function NexusOne({ null, ); const [txError, setTxError] = useState(null); + const [walletActionPending, setWalletActionPending] = useState(false); const defaultRecipientAddress = ownerAddress ?? ""; const effectiveRecipientAddress = activeMode === "swap" @@ -1654,6 +1691,12 @@ export function NexusOne({ const intentSymbolUsdRateCacheRef = useRef>( {}, ); + const predictiveQuoteCacheRef = useRef>( + {}, + ); + const predictiveQuoteRunRef = useRef(0); + const [predictiveQuote, setPredictiveQuote] = + useState(null); const maxPercentRunRef = useRef(0); const [previewQuoteRefreshing, setPreviewQuoteRefreshing] = useState(false); const [quoteRefreshProgress, setQuoteRefreshProgress] = useState(0); @@ -1895,6 +1938,9 @@ export function NexusOne({ setIntentToAmount(undefined); setIntentFeeUsd(undefined); setIntentData(null); + if (!options.keepQuoteRefreshing) { + setPredictiveQuote(null); + } } }; @@ -2237,6 +2283,174 @@ export function NexusOne({ return tokenAmount.mul(rate); }; + const getExactOutDestinationBalanceCoverage = ({ + requestedAmount, + requestedUsd, + producedAmount, + producedUsd, + token = toToken, + }: { + requestedAmount?: Decimal; + requestedUsd?: Decimal; + producedAmount?: Decimal; + producedUsd?: Decimal; + token?: SwapTokenOption; + }) => { + if ( + (activeMode !== "deposit" && activeMode !== "send") || + !token || + !requestedAmount || + requestedAmount.lte(0) + ) { + return null; + } + + const balanceAmount = + parseFiatNumber(destinationBalance) ?? + parseFiatNumber(token.balance) ?? + new Decimal(0); + if (balanceAmount.lte(0)) return null; + + const externalAmount = + producedAmount && producedAmount.gt(0) ? producedAmount : new Decimal(0); + const uncoveredAmount = Decimal.max( + requestedAmount.minus(externalAmount), + new Decimal(0), + ); + const coveredAmount = Decimal.min(balanceAmount, uncoveredAmount); + if (coveredAmount.lte(0)) return null; + + const requestedRate = + requestedUsd && requestedUsd.gt(0) + ? requestedUsd.div(requestedAmount) + : undefined; + const producedRate = + producedUsd && producedUsd.gt(0) && producedAmount && producedAmount.gt(0) + ? producedUsd.div(producedAmount) + : undefined; + const fallbackRate = getTokenUsdRate(token); + const usdRate = + requestedRate && requestedRate.gt(0) + ? requestedRate + : producedRate && producedRate.gt(0) + ? producedRate + : fallbackRate.gt(0) + ? fallbackRate + : undefined; + + return { + amount: coveredAmount, + usd: usdRate ? coveredAmount.mul(usdRate) : undefined, + }; + }; + + const buildDestinationBalanceDisplayToken = ( + coverage: ReturnType, + token?: SwapTokenOption, + ): SwapTokenOption | null => { + if (!coverage || !token || coverage.amount.lte(0)) return null; + + const amount = coverage.amount + .toDecimalPlaces(Math.max(0, token.decimals ?? 18), Decimal.ROUND_DOWN) + .toFixed(); + const usd = coverage.usd?.toDecimalPlaces(6, Decimal.ROUND_DOWN).toFixed(); + const balanceUsd = coverage.usd + ? `$${coverage.usd.toDecimalPlaces(2, Decimal.ROUND_DOWN).toFixed()}` + : token.balanceInFiat || "$0.00"; + + return { + ...token, + balance: `${amount} ${token.symbol}`, + balanceInFiat: balanceUsd, + userAmount: amount, + userAmountMode: "token", + userAmountUsd: usd, + }; + }; + + const cacheSymbolUsdRate = (symbol: string | undefined, rate: Decimal) => { + const symbolKey = getSymbolUsdRateCacheKey(symbol); + if (!symbolKey || rate.lte(0)) return; + + intentSymbolUsdRateCacheRef.current[symbolKey] = { + amount: "1", + rate: rate.toDecimalPlaces(18).toFixed(), + updatedAt: Date.now(), + value: rate.toFixed(), + }; + }; + + const getPredictiveDestinationKey = (token?: SwapTokenOption) => { + const tokenKey = getTokenUsdRateCacheKey(token); + return tokenKey ? `destination:${tokenKey}` : ""; + }; + + const getPredictiveSourceKey = (token: SwapTokenOption) => + [ + token.chainId ?? "unknown", + (token.contractAddress || zeroAddress).toLowerCase(), + token.symbol.toUpperCase(), + ].join(":"); + + const getPredictiveQuoteCacheKey = ( + mode = activeMode, + type = swapType, + destination = toToken, + sources = fromTokens, + ) => { + const destinationKey = getPredictiveDestinationKey(destination); + if (!destinationKey) return ""; + if (mode !== "swap" || type !== "exactIn") { + return `exactOut:${destinationKey}`; + } + + const sourceKey = getExpandedSourceTokens(sources) + .map(getPredictiveSourceKey) + .sort() + .join("+"); + return sourceKey ? `exactIn:${sourceKey}->${destinationKey}` : ""; + }; + + const getPredictiveDisplayAmount = ( + amount: Decimal, + token?: Pick, + ) => { + const decimals = Math.min( + PREDICTIVE_QUOTE_DISPLAY_DECIMALS, + Math.max(0, token?.decimals ?? 18), + ); + return amount.toDecimalPlaces(decimals, Decimal.ROUND_DOWN).toFixed(); + }; + + const resolveUsdRateForToken = async (token?: SwapTokenOption) => { + if (!token?.symbol) return new Decimal(0); + + const localRate = getTokenUsdRate(token); + if (localRate.gt(0)) return localRate; + + const resolvedRate = await resolveUsdRateForSymbol(token.symbol); + if (resolvedRate.gt(0)) { + cacheSymbolUsdRate(token.symbol, resolvedRate); + } + return resolvedRate; + }; + + const getPredictiveExactInSourceTokens = () => { + const expanded = getExpandedSourceTokens(fromTokens); + if (expanded.length === 0) return []; + + return expanded + .map((token) => { + const userAmount = + token.userAmount || + (expanded.length === 1 && hasPositiveDecimalInput(amount) + ? amount + : ""); + return { ...token, userAmount }; + }) + .filter((token) => hasPositiveDecimalInput(token.userAmount)); + }; + const sortUnifiedSourceTokens = (tokens: SwapTokenOption[]) => [...tokens].sort((a, b) => { const fiatDiff = getTokenBalanceUsd(b).cmp(getTokenBalanceUsd(a)); @@ -2458,6 +2672,44 @@ export function NexusOne({ }; }; + const buildPredictiveExactOutSources = async (requiredSourceUsd: Decimal) => { + if (requiredSourceUsd.lte(0)) return []; + + const destinationKey = getTokenSelectionKey(toToken); + const candidates = getExactOutSourceTokens() + .filter((token) => getTokenSelectionKey(token) !== destinationKey) + .filter((token) => getTokenBalanceUsd(token).gt(0)) + .sort((a, b) => getTokenBalanceUsd(b).cmp(getTokenBalanceUsd(a))); + const sources: SwapTokenOption[] = []; + let remainingUsd = requiredSourceUsd; + + for (const token of candidates) { + if (remainingUsd.lte(0)) break; + + const availableUsd = getTokenBalanceUsd(token); + if (availableUsd.lte(0)) continue; + + const rate = await resolveUsdRateForToken(token); + if (rate.lte(0)) continue; + + const targetUsd = Decimal.min(remainingUsd, availableUsd); + const tokenAmount = targetUsd + .div(rate) + .toDecimalPlaces(Math.max(0, token.decimals || 18), Decimal.ROUND_DOWN); + if (tokenAmount.lte(0)) continue; + + sources.push({ + ...token, + userAmount: tokenAmount.toFixed(), + userAmountMode: "token", + userAmountUsd: targetUsd.toDecimalPlaces(6, Decimal.ROUND_DOWN).toFixed(), + }); + remainingUsd = remainingUsd.minus(targetUsd); + } + + return remainingUsd.gt(0.01) ? [] : sources; + }; + const getErrorText = (error: unknown) => { const err = error as any; const parts = [ @@ -2844,10 +3096,63 @@ export function NexusOne({ } }; + const cachePredictiveBaselineFromIntent = (intent: SwapIntentData) => { + const destinationAmount = parseFiatNumber(intent.destination?.amount); + const destinationValue = parseFiatNumber(intent.destination?.value); + const sourceUsd = (intent.sources ?? []).reduce( + (sum, source) => + sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)), + new Decimal(0), + ); + + if (!destinationAmount || destinationAmount.lte(0)) return; + + const destinationUsdRate = + destinationValue && destinationValue.gt(0) + ? destinationValue.div(destinationAmount) + : getUsdRateForSymbol(intent.destination?.token?.symbol); + if (destinationUsdRate.lte(0)) return; + + cacheSymbolUsdRate(intent.destination?.token?.symbol, destinationUsdRate); + + const key = getPredictiveQuoteCacheKey(); + if (!key) return; + + const baseline: PredictiveQuoteBaseline = { + destinationUsdRate: destinationUsdRate.toDecimalPlaces(18).toFixed(), + updatedAt: Date.now(), + }; + + if (activeMode === "swap" && swapType === "exactIn" && sourceUsd.gt(0)) { + baseline.exactInDestinationAmountPerSourceUsd = destinationAmount + .div(sourceUsd) + .toDecimalPlaces(18) + .toFixed(); + } + + const resolvedDestinationValue = + destinationValue && destinationValue.gt(0) + ? destinationValue + : destinationAmount.mul(destinationUsdRate); + if ( + (activeMode === "deposit" || activeMode === "send") && + resolvedDestinationValue.gt(0) && + sourceUsd.gt(0) + ) { + baseline.exactOutSourceUsdPerDestinationUsd = sourceUsd + .div(resolvedDestinationValue) + .toDecimalPlaces(18) + .toFixed(); + } + + predictiveQuoteCacheRef.current[key] = baseline; + }; + const applySwapIntent = useCallback( (intent: SwapIntentData) => { lastSwapIntentRefreshAtRef.current = Date.now(); cacheDestinationUsdRateFromIntent(intent); + cachePredictiveBaselineFromIntent(intent); setIntentData(intent); setIntentToAmount(intent.destination?.amount || undefined); setSwapQuoteIssue(null); @@ -2904,7 +3209,7 @@ export function NexusOne({ setIntentFeeUsd(undefined); } }, - [activeMode, swapType, swapBalance], + [activeMode, fromTokens, swapType, swapBalance, toToken], ); // Register swap intent hook immediately before executing a swap to prevent race conditions across multiple components @@ -3034,10 +3339,31 @@ export function NexusOne({ address: pair.token, chainId: pair.chain, }); - const tokenSymbol = matchedToken?.symbol ?? citreaToken?.symbol ?? "Token"; + const tokenAddressSymbol = Object.entries( + TOKEN_CONTRACT_ADDRESSES as Record>, + ).find( + ([, addresses]) => + normalizeAddress(addresses[pair.chain]) === targetAddress, + )?.[0]; + const chainMeta = CHAIN_METADATA[pair.chain]; + const isNativePrefill = isNativeTokenAddress(pair.token); + const tokenSymbol = + matchedToken?.symbol ?? + citreaToken?.symbol ?? + tokenAddressSymbol ?? + (isNativePrefill ? chainMeta?.nativeCurrency?.symbol : undefined) ?? + "Token"; const tokenMeta = TOKEN_METADATA[tokenSymbol as keyof typeof TOKEN_METADATA]; - if (!chain && !matchedToken && !citreaToken) return undefined; + if ( + !chain && + !matchedToken && + !citreaToken && + !tokenAddressSymbol && + !isNativePrefill + ) { + return undefined; + } return { chainId: pair.chain, @@ -3046,12 +3372,17 @@ export function NexusOne({ name: matchedToken?.name || citreaToken?.name || tokenSymbol, balance: `0 ${tokenSymbol}`, balanceInFiat: "$0.00", - decimals: matchedToken?.decimals ?? citreaToken?.decimals ?? tokenMeta?.decimals ?? 18, + decimals: + matchedToken?.decimals ?? + citreaToken?.decimals ?? + tokenMeta?.decimals ?? + (isNativePrefill ? chainMeta?.nativeCurrency?.decimals : undefined) ?? + 18, logo: matchedToken?.logo || citreaToken?.logo || tokenMeta?.icon, chainName: - chain?.name ?? CHAIN_METADATA[pair.chain]?.name ?? citreaToken?.chainName, + chain?.name ?? chainMeta?.name ?? citreaToken?.chainName, chainLogo: - chain?.logo ?? CHAIN_METADATA[pair.chain]?.logo ?? citreaToken?.chainLogo, + chain?.logo ?? chainMeta?.logo ?? citreaToken?.chainLogo, } satisfies SwapTokenOption; }, [supportedChainsAndTokens, swapBalance], @@ -3071,6 +3402,7 @@ export function NexusOne({ destinationPrefill ? `destination:${destinationPrefill.chain}:${destinationPrefill.token.toLowerCase()}` : "", + config.prefill?.amount ? `amount:${config.prefill.amount}` : "", ].join("|"); if (appliedTokenPrefillRef.current === prefillKey) return; @@ -3082,7 +3414,7 @@ export function NexusOne({ if (destinationPrefill && !destinationToken) return; if (sourceToken) { - setFromTokens([{ ...sourceToken, userAmount: "" }]); + setFromTokens([{ ...sourceToken, userAmount: config.prefill?.amount ?? "" }]); setSourceSelectionTouched(true); } if (destinationToken) { @@ -3092,6 +3424,7 @@ export function NexusOne({ appliedTokenPrefillRef.current = prefillKey; }, [ activeMode, + config.prefill?.amount, config.prefill?.destination?.chain, config.prefill?.destination?.token, config.prefill?.source?.chain, @@ -3283,7 +3616,6 @@ export function NexusOne({ }, [activeMode, swapType]); useEffect(() => { - if (activeMode !== "deposit" && activeMode !== "send") return; if (!toToken?.symbol) return; if (getFiatValue(1, toToken.symbol) > 0) return; @@ -3363,7 +3695,7 @@ export function NexusOne({ ? Boolean(hasPositiveDecimalInput(amount) && toToken) : false; const invalidateExactOutQuoteForRefresh = () => { - const shouldLoadQuote = canRefreshExactOutQuote(); + const shouldLoadQuote = Boolean(nexusSDK && canRefreshExactOutQuote()); clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote }); if (shouldLoadQuote) { setQuoteRefreshing(true); @@ -3372,6 +3704,253 @@ export function NexusOne({ } return shouldLoadQuote; }; + + useEffect(() => { + if ( + activeMode !== "swap" || + swapStep !== "idle" || + swapType !== "exactIn" + ) { + setPredictiveQuote((current) => + current?.mode === "exactIn" ? null : current, + ); + return; + } + + const sources = getPredictiveExactInSourceTokens(); + const key = getPredictiveQuoteCacheKey(); + if (!toToken || sources.length === 0 || !key) { + setPredictiveQuote((current) => + current?.mode === "exactIn" ? null : current, + ); + return; + } + + const runId = ++predictiveQuoteRunRef.current; + let cancelled = false; + + void (async () => { + const baseline = predictiveQuoteCacheRef.current[key]; + const cachedDestinationRate = parseFiatNumber( + baseline?.destinationUsdRate, + ); + const destinationRate = + cachedDestinationRate && cachedDestinationRate.gt(0) + ? cachedDestinationRate + : await resolveUsdRateForToken(toToken); + + if (cancelled || runId !== predictiveQuoteRunRef.current) return; + if (destinationRate.lte(0)) { + setPredictiveQuote((current) => + current?.mode === "exactIn" ? null : current, + ); + return; + } + + let sourceUsd = new Decimal(0); + for (const source of sources) { + const sourceAmount = + parseFiatNumber(source.userAmount) ?? new Decimal(0); + if (sourceAmount.lte(0)) continue; + + if (source.userAmountMode === "usd") { + sourceUsd = sourceUsd.plus(sourceAmount); + continue; + } + + const sourceRate = await resolveUsdRateForToken(source); + if (cancelled || runId !== predictiveQuoteRunRef.current) return; + if (sourceRate.lte(0)) { + setPredictiveQuote((current) => + current?.mode === "exactIn" ? null : current, + ); + return; + } + sourceUsd = sourceUsd.plus(sourceAmount.mul(sourceRate)); + } + + if (sourceUsd.lte(0)) { + setPredictiveQuote((current) => + current?.mode === "exactIn" ? null : current, + ); + return; + } + + const cachedAmountPerSourceUsd = parseFiatNumber( + baseline?.exactInDestinationAmountPerSourceUsd, + ); + const predictedDestinationAmount = + cachedAmountPerSourceUsd && cachedAmountPerSourceUsd.gt(0) + ? sourceUsd.mul(cachedAmountPerSourceUsd) + : sourceUsd + .mul(BASIS_POINTS - PREDICTIVE_EXACT_IN_DISCOUNT_BPS) + .div(BASIS_POINTS) + .div(destinationRate); + const predictedDestinationUsd = + cachedAmountPerSourceUsd && cachedAmountPerSourceUsd.gt(0) + ? predictedDestinationAmount.mul(destinationRate) + : sourceUsd + .mul(BASIS_POINTS - PREDICTIVE_EXACT_IN_DISCOUNT_BPS) + .div(BASIS_POINTS); + + if ( + cancelled || + runId !== predictiveQuoteRunRef.current || + predictedDestinationAmount.lte(0) + ) { + return; + } + + setPredictiveQuote({ + key, + mode: "exactIn", + toAmount: getPredictiveDisplayAmount( + predictedDestinationAmount, + toToken, + ), + toUsd: predictedDestinationUsd.toDecimalPlaces(6).toFixed(), + }); + })(); + + return () => { + cancelled = true; + }; + }, [ + activeMode, + amount, + fromTokens, + swapStep, + swapType, + toToken?.chainId, + toToken?.contractAddress, + toToken?.decimals, + toToken?.symbol, + ]); + + useEffect(() => { + if ( + (activeMode !== "deposit" && activeMode !== "send") || + swapStep !== "idle" || + swapType !== "exactOut" || + !nexusSDK + ) { + setPredictiveQuote((current) => + current?.mode === "exactOut" ? null : current, + ); + return; + } + + const parsedAmount = parseFiatNumber(amount); + const key = getPredictiveQuoteCacheKey(); + if ( + !toToken || + !parsedAmount || + parsedAmount.lte(0) || + !key || + (activeMode === "deposit" && !selectedOpportunity) + ) { + setPredictiveQuote((current) => + current?.mode === "exactOut" ? null : current, + ); + return; + } + + const runId = ++predictiveQuoteRunRef.current; + let cancelled = false; + + void (async () => { + const baseline = predictiveQuoteCacheRef.current[key]; + const cachedDestinationRate = parseFiatNumber( + baseline?.destinationUsdRate, + ); + const destinationRate = + cachedDestinationRate && cachedDestinationRate.gt(0) + ? cachedDestinationRate + : await resolveUsdRateForToken(toToken); + + if (cancelled || runId !== predictiveQuoteRunRef.current) return; + if (destinationRate.lte(0)) { + setPredictiveQuote((current) => + current?.mode === "exactOut" ? null : current, + ); + return; + } + + const destinationAmount = + activeMode === "deposit" && depositAmountMode === "usd" + ? parsedAmount.div(destinationRate) + : parsedAmount; + const destinationUsd = + activeMode === "deposit" && depositAmountMode === "usd" + ? parsedAmount + : destinationAmount.mul(destinationRate); + const destinationCoverage = getExactOutDestinationBalanceCoverage({ + requestedAmount: destinationAmount, + requestedUsd: destinationUsd, + token: toToken, + }); + const destinationUsdNeedingSources = Decimal.max( + destinationUsd.minus(destinationCoverage?.usd ?? new Decimal(0)), + new Decimal(0), + ); + const cachedSourceUsdRatio = parseFiatNumber( + baseline?.exactOutSourceUsdPerDestinationUsd, + ); + const requiredSourceUsd = + destinationUsdNeedingSources.lte(0) + ? new Decimal(0) + : cachedSourceUsdRatio && cachedSourceUsdRatio.gt(0) + ? destinationUsdNeedingSources.mul(cachedSourceUsdRatio) + : destinationUsdNeedingSources + .mul(BASIS_POINTS + PREDICTIVE_EXACT_OUT_BUFFER_BPS) + .div(BASIS_POINTS); + const sources = requiredSourceUsd.gt(0) + ? await buildPredictiveExactOutSources(requiredSourceUsd) + : []; + + if ( + cancelled || + runId !== predictiveQuoteRunRef.current || + (requiredSourceUsd.gt(0) && sources.length === 0) + ) { + setPredictiveQuote((current) => + current?.mode === "exactOut" ? null : current, + ); + return; + } + + setPredictiveQuote({ + key, + mode: "exactOut", + sources, + toAmount: getPredictiveDisplayAmount(destinationAmount, toToken), + toUsd: destinationUsd.toDecimalPlaces(6).toFixed(), + }); + })(); + + return () => { + cancelled = true; + }; + }, [ + activeMode, + amount, + depositAmountMode, + destinationBalance, + fromTokens, + nexusSDK, + selectedOpportunity, + sourceSelectionRevision, + swapBalance, + swapStep, + swapType, + toToken?.balance, + toToken?.balanceInFiat, + toToken?.chainId, + toToken?.contractAddress, + toToken?.decimals, + toToken?.symbol, + ]); + const defaultDepositSourceTokens = useMemo(() => { if (activeMode !== "deposit" || !swapBalance) return []; return deriveTokenOptions(swapBalance) @@ -3571,6 +4150,62 @@ export function NexusOne({ onClose?.(); }; + const handleConnectWallet = async () => { + if (walletActionPending || nexusLoading || isWalletConnectPending) return; + + setWalletActionPending(true); + setTxError(null); + try { + let activeConnector = connector; + + if (walletStatus !== "connected") { + const nextConnector = connectors[0]; + if (!nextConnector) { + throw new Error("No wallet connector available."); + } + await connectAsync({ connector: nextConnector }); + activeConnector = nextConnector; + } + + const connectorProvider = await activeConnector + ?.getProvider() + .catch(() => undefined); + const connectorClientProvider = connectorClient + ? { + request: (args: unknown) => + connectorClient.request(args as any), + } + : undefined; + const walletClientProvider = walletClient + ? { + request: (args: unknown) => + walletClient.request(args as any), + } + : undefined; + const windowProvider = + typeof window !== "undefined" + ? (window as Window & { ethereum?: EthereumProvider }).ethereum + : undefined; + const effectiveProvider = + connectorProvider && + typeof (connectorProvider as EthereumProvider).request === "function" + ? (connectorProvider as EthereumProvider) + : (connectorClientProvider ?? + walletClientProvider ?? + windowProvider); + + if (!effectiveProvider || typeof effectiveProvider.request !== "function") { + throw new Error("Wallet provider is not ready yet."); + } + + await handleInit(effectiveProvider as EthereumProvider); + } catch (error: any) { + setTxError(error?.message || "Unable to connect wallet."); + } finally { + setWalletActionPending(false); + } + }; + const handleOpenRecipientEditor = () => { if (activeMode === "swap" && !recipientAddress && defaultRecipientAddress) { setRecipientAddress(defaultRecipientAddress); @@ -4210,7 +4845,7 @@ export function NexusOne({ }; useEffect(() => { - if (activeMode !== "swap" || swapStep !== "idle") return; + if (activeMode !== "swap" || swapStep !== "idle" || !nexusSDK) return; if (syncingIntentSourcesRef.current) { syncingIntentSourcesRef.current = false; @@ -4239,10 +4874,10 @@ export function NexusOne({ clearPendingSwapIntent(true, { keepQuoteRefreshing: true }); } }; - }, [activeMode, amount, fromTokens, swapStep, toToken]); + }, [activeMode, amount, fromTokens, nexusSDK, swapStep, toToken]); useEffect(() => { - if (activeMode !== "deposit" || swapStep !== "idle") return; + if (activeMode !== "deposit" || swapStep !== "idle" || !nexusSDK) return; if (syncingIntentSourcesRef.current) { syncingIntentSourcesRef.current = false; @@ -4280,6 +4915,7 @@ export function NexusOne({ activeMode, amount, depositAmountMode, + nexusSDK, sourceSelectionRevision, selectedOpportunity, swapStep, @@ -4287,7 +4923,7 @@ export function NexusOne({ ]); useEffect(() => { - if (activeMode !== "send" || swapStep !== "idle") return; + if (activeMode !== "send" || swapStep !== "idle" || !nexusSDK) return; if (syncingIntentSourcesRef.current) { syncingIntentSourcesRef.current = false; @@ -4316,7 +4952,7 @@ export function NexusOne({ clearPendingSwapIntent(true, { keepQuoteRefreshing: true }); } }; - }, [activeMode, amount, sourceSelectionRevision, swapStep, toToken]); + }, [activeMode, amount, nexusSDK, sourceSelectionRevision, swapStep, toToken]); const refreshActiveSwapIntent = useCallback(async () => { const activeIntent = swapIntentRef.current; @@ -4572,7 +5208,7 @@ export function NexusOne({ (token) => token.chainId && token.contractAddress, ); const shouldLoadQuote = Boolean( - nextAmount?.gt(0) && toToken && hasSelectedSourceToken, + nexusSDK && nextAmount?.gt(0) && toToken && hasSelectedSourceToken, ); clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote }); if (shouldLoadQuote) { @@ -4599,7 +5235,7 @@ export function NexusOne({ setSwapQuoteIssue(null); const nextAmount = parseFiatNumber(val); const shouldLoadQuote = Boolean( - nextAmount?.gt(0) && toToken && selectedOpportunity, + nexusSDK && nextAmount?.gt(0) && toToken && selectedOpportunity, ); clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote }); if (shouldLoadQuote) { @@ -4619,7 +5255,7 @@ export function NexusOne({ setSwapQuoteIssue(null); setSwapType("exactOut"); const nextAmount = parseFiatNumber(val); - const shouldLoadQuote = Boolean(nextAmount?.gt(0) && toToken); + const shouldLoadQuote = Boolean(nexusSDK && nextAmount?.gt(0) && toToken); clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote }); if (shouldLoadQuote) { setQuoteRefreshing(true); @@ -4865,51 +5501,58 @@ export function NexusOne({ (!hasCurrentRunnableIntent || !hasIntentSources); const hasPositiveRootAmount = hasPositiveDecimalInput(amount); const hasReadySwapQuoteInput = hasReadyExactInSwapInput(fromTokens, toToken); + const needsWalletConnection = !ownerAddress || !nexusSDK; + const walletConnectBusy = + walletActionPending || + nexusLoading || + isWalletConnectPending || + walletStatus === "connecting"; + const walletCtaLabel = walletConnectBusy ? "Connecting..." : "Connect Wallet"; const isSwapCtaDisabled = - !hasReadySwapQuoteInput || - receiveMaxCalculating || - quoteRefreshing || - Boolean(exactOutInsufficientSourceIssue); + needsWalletConnection + ? walletConnectBusy + : !hasReadySwapQuoteInput || + receiveMaxCalculating || + quoteRefreshing || + Boolean(exactOutInsufficientSourceIssue); const isDepositCtaDisabled = - !hasPositiveRootAmount || - !toToken || - quoteRefreshing || - receiveMaxCalculating || - isQuoteUnavailableForAutoSourceFlow || - Boolean(exactOutInsufficientSourceIssue); + needsWalletConnection + ? walletConnectBusy + : !hasPositiveRootAmount || + !toToken || + quoteRefreshing || + receiveMaxCalculating || + isQuoteUnavailableForAutoSourceFlow || + Boolean(exactOutInsufficientSourceIssue); const sendNeedsRecipient = activeMode === "send" && !recipientAddress; const isSendCtaDisabled = - !hasPositiveRootAmount || - !toToken || - hasSameOwnerSendRecipient || - receiveMaxCalculating || - (!sendNeedsRecipient && - (quoteRefreshing || isQuoteUnavailableForAutoSourceFlow)) || - Boolean(exactOutInsufficientSourceIssue); - const quoteCtaLabel = (fallback: string) => - exactOutInsufficientSourceIssue - ? "Insufficient balance" - : receiveMaxCalculating - ? "Calculating..." - : quoteRefreshing - ? "Fetching quotes..." - : isQuoteUnavailableForAutoSourceFlow - ? "Quote unavailable" - : !hasPositiveRootAmount - ? "Enter amount" - : fallback; - const sendCtaLabel = - exactOutInsufficientSourceIssue - ? "Insufficient balance" - : !hasPositiveRootAmount - ? "Enter amount" - : !toToken - ? "Select token" - : hasSameOwnerSendRecipient - ? "Change recipient" - : sendNeedsRecipient - ? "Add recipient" - : quoteCtaLabel("Review send"); + needsWalletConnection + ? walletConnectBusy + : !hasPositiveRootAmount || + !toToken || + hasSameOwnerSendRecipient || + receiveMaxCalculating || + (!sendNeedsRecipient && + (quoteRefreshing || isQuoteUnavailableForAutoSourceFlow)) || + Boolean(exactOutInsufficientSourceIssue); + const quoteCtaLabel = (fallback: string) => { + if (needsWalletConnection) return walletCtaLabel; + if (exactOutInsufficientSourceIssue) return "Insufficient balance"; + if (receiveMaxCalculating) return "Calculating..."; + if (quoteRefreshing) return "Intent fetching..."; + if (isQuoteUnavailableForAutoSourceFlow) return "Quote unavailable"; + if (!hasPositiveRootAmount) return "Enter amount"; + return fallback; + }; + const sendCtaLabel = (() => { + if (needsWalletConnection) return walletCtaLabel; + if (exactOutInsufficientSourceIssue) return "Insufficient balance"; + if (!hasPositiveRootAmount) return "Enter amount"; + if (!toToken) return "Select token"; + if (hasSameOwnerSendRecipient) return "Change recipient"; + if (sendNeedsRecipient) return "Add recipient"; + return quoteCtaLabel("Review send"); + })(); const previewIntentSourceUsdNumber = (intentData?.sources ?? []).reduce( (sum, source) => sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)), new Decimal(0), @@ -4970,6 +5613,86 @@ export function NexusOne({ previewDestinationUsdNumber && previewDestinationUsdNumber.gt(0) ? previewDestinationUsdNumber.toDecimalPlaces(6).toFixed() : undefined; + const predictiveExactInQuote = + predictiveQuote?.mode === "exactIn" && + predictiveQuote.key === getPredictiveQuoteCacheKey("swap", "exactIn") + ? predictiveQuote + : null; + const predictiveExactOutQuote = + predictiveQuote?.mode === "exactOut" && + predictiveQuote.key === getPredictiveQuoteCacheKey(activeMode, "exactOut") + ? predictiveQuote + : null; + const resolvedToToken = + toToken ?? + (activeMode === "deposit" && selectedOpportunity + ? toTokenFromOpportunity(selectedOpportunity) + : undefined); + const toTokenWithFetchedBalance = + resolvedToToken && destinationBalance + ? { ...resolvedToToken, balance: destinationBalance } + : resolvedToToken; + const idleReceiveQuoteAmount = + activeMode === "swap" && swapType === "exactIn" + ? intentToAmount ?? predictiveExactInQuote?.toAmount + : undefined; + const idleReceiveQuoteUsd = + activeMode === "swap" && swapType === "exactIn" + ? previewToAmountUsd ?? predictiveExactInQuote?.toUsd + : previewToAmountUsd; + const exactOutDestinationCoverage = getExactOutDestinationBalanceCoverage({ + requestedAmount: previewExactOutDestinationAmount, + requestedUsd: previewExactOutDestinationUsdNumber, + producedAmount: parseFiatNumber(intentData?.destination?.amount), + producedUsd: parseFiatNumber(intentData?.destination?.value), + token: toTokenWithFetchedBalance, + }); + const destinationBalanceDisplayToken = buildDestinationBalanceDisplayToken( + exactOutDestinationCoverage, + toTokenWithFetchedBalance, + ); + const shouldShowPredictiveExactOutDisplay = + (activeMode === "deposit" || activeMode === "send") && + (quoteRefreshing || intentLoading) && + !hasIntentSources && + Boolean( + predictiveExactOutQuote && + ((predictiveExactOutQuote.sources?.length ?? 0) > 0 || + destinationBalanceDisplayToken), + ); + const baseDisplayFromTokens = shouldShowPredictiveExactOutDisplay + ? predictiveExactOutQuote?.sources ?? fromTokens + : fromTokens; + const displayFromTokens = (() => { + if ( + !destinationBalanceDisplayToken || + (activeMode !== "deposit" && activeMode !== "send") + ) { + return baseDisplayFromTokens; + } + + const destinationKey = getTokenSelectionKey(destinationBalanceDisplayToken); + let replacedEmptyDestinationToken = false; + const tokens = baseDisplayFromTokens.map((token) => { + const isDestinationToken = + getTokenSelectionKey(token) === destinationKey; + if ( + isDestinationToken && + !hasPositiveDecimalInput(token.userAmount) && + !hasPositiveDecimalInput(token.userAmountUsd) + ) { + replacedEmptyDestinationToken = true; + return destinationBalanceDisplayToken; + } + return token; + }); + + return replacedEmptyDestinationToken + ? tokens + : [...tokens, destinationBalanceDisplayToken]; + })(); + const displayExactOutRouteLoading = + isExactOutRouteLoading && !shouldShowPredictiveExactOutDisplay; const totalSwapBalanceUsd = getSwapBalanceTotalUsd() .toDecimalPlaces(2) .toFixed(); @@ -4982,25 +5705,16 @@ export function NexusOne({ userAmountMode: "token", }, amount, - ).toNumber() + ).toNumber() : 0; - const resolvedToToken = - toToken ?? - (activeMode === "deposit" && selectedOpportunity - ? toTokenFromOpportunity(selectedOpportunity) - : undefined); - const toTokenWithFetchedBalance = - resolvedToToken && destinationBalance - ? { ...resolvedToToken, balance: destinationBalance } - : resolvedToToken; const isIdleSwapQuoteLoading = activeMode === "swap" && swapStep === "idle" && quoteRefreshing; const isReceiveAmountLoading = receiveMaxCalculating || - (isIdleSwapQuoteLoading && swapType === "exactIn" && !intentToAmount); + (isIdleSwapQuoteLoading && swapType === "exactIn" && !idleReceiveQuoteAmount); const isReceiveUsdLoading = receiveMaxCalculating || - (isIdleSwapQuoteLoading && swapType === "exactIn" && !previewToAmountUsd); + (isIdleSwapQuoteLoading && swapType === "exactIn" && !idleReceiveQuoteUsd); const hasQuoteRefreshCountdown = (activeMode === "swap" || activeMode === "deposit" || activeMode === "send") && Boolean(intentData && swapIntentRef.current) && @@ -5398,7 +6112,7 @@ export function NexusOne({ \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 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}: 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 sourceListRef = useRef(null);\n const sourceRowRefs = useRef>({});\n const previousSourceCountRef = useRef(fromTokens.length);\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 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 = 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 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 (!onUpdateTokens) return;\n const token = fromTokens[index];\n if (!token) return;\n\n let sanitized = sanitizeInput(\n val,\n token.userAmountMode === \"usd\" ? MAX_AMOUNT_DISPLAY_DECIMALS : getTokenInputDecimals(token),\n );\n\n // Enforce max amount validation\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 isUsdMode = token.userAmountMode === \"usd\";\n\n const maxAmt = isUsdMode ? fiatBalance : tokenBalance;\n if (Number(sanitized) > maxAmt) {\n if (isUsdMode) {\n sanitized = maxAmt.toFixed(2);\n } else {\n sanitized = String(token.balance).replace(/[^0-9.]/g, \"\");\n }\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 (!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\"\n ? sourceRouteMessage\n : 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 =\n 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 ? \"#006BF4\"\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 (!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 (!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: \"5px\",\n padding: \"2px 0\",\n color: fromTokens.length > 0 ? \"#006BF4\" : \"#A8A8A6\",\n cursor: fromTokens.length > 0 ? \"pointer\" : \"not-allowed\",\n fontFamily: '\"Geist\", system-ui, sans-serif',\n fontSize: \"12px\",\n fontWeight: 500,\n lineHeight: \"18px\",\n opacity: fromTokens.length > 0 ? 1 : 0.75,\n }}\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: \"6px\",\n opacity: 1,\n position: \"relative\",\n transform: \"translateY(0)\",\n transition:\n \"opacity 0.18s ease, transform 0.18s ease\",\n zIndex:\n tooltip === `asset-send-${index}`\n ? 1000\n : 1,\n }}\n >\n \n \n {showSourceRouteSkeleton ? (\n \n ) : (\n <>\n {token?.userAmountMode === \"usd\" && (\n \n $\n \n )}\n {\n if (token)\n handleTokenAmountChange(index, e.target.value);\n else handleSendInput(e);\n }}\n onFocus={() => setFocusedRow(index)}\n onBlur={() => {\n if (token) handleBlurAmount(index);\n setFocusedRow(null);\n }}\n style={{\n boxSizing: \"border-box\",\n color:\n (token\n ? Boolean(token.userAmount)\n : Boolean(isExactIn && amount))\n ? \"#161615\"\n : \"#9E9E9C\",\n fontFamily:\n '\"Delight-Medium\", \"Delight\", system-ui, sans-serif',\n fontSize: \"32px\",\n fontWeight: 500,\n lineHeight: \"38px\",\n background: \"transparent\",\n border: \"none\",\n outline: \"none\",\n padding: 0,\n width: \"100%\",\n minWidth: 0,\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: \"7px\",\n paddingBottom: \"4px\",\n paddingLeft: token ? \"4px\" : \"8px\",\n paddingRight: \"9px\",\n paddingTop: \"4px\",\n cursor: \"pointer\",\n flexShrink: 0,\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: \"22px\",\n height: \"22px\",\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 {showSourceRouteSkeleton ? (\n \n ) : (\n (() => {\n if (!token)\n return (\n \n โ‰ˆ ${usdValue || \"0.00\"}\n \n );\n const tokenBalance =\n Number(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 ? ` ${token.symbol}` : \"\";\n\n return (\n 0 ? \"pointer\" : \"default\",\n }}\n onClick={() => handleToggleMode(index)}\n >\n \n {approxPrefix}\n {approxValue}\n {approxSuffix}\n \n {price > 0 && }\n \n );\n })()\n )}\n {showSourceRouteSkeleton ? (\n \n ) : token && focusedRow === index ? (\n setTooltip(`asset-send-${index}`)}\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 {formatTokenBalanceLabel(token)}\n \n \n {/* Tooltip */}\n {tooltip === `asset-send-${index}` && (\n
\n
\n Asset Balance\n
\n
\n This is your current asset balance on this chain.\n
\n
\n )}\n \n ) : null}\n \n\n {/* 25% 50% 75% MAX โ€” shown only while the row amount is focused */}\n \n token\n ? handleSendPercentForToken(index, pct, token)\n : handleSendPercent(pct)\n }\n />\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: \"12px\",\n fontWeight: 500,\n gap: \"5px\",\n lineHeight: \"18px\",\n marginTop: \"-2px\",\n padding: 0,\n }}\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 \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 {receiveBalanceLabel}\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": "import React, { useRef, useState, useEffect } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport Decimal from \"decimal.js\";\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\ninterface SwapIdleFormProps {\n amount: string;\n receiveQuoteAmount?: string;\n receiveQuoteUsd?: string;\n isReceiveAmountLoading?: boolean;\n isReceiveUsdLoading?: boolean;\n sourceRouteStatus?: \"loading\" | \"insufficient\";\n sourceRouteMessage?: string;\n onAmountChange: (val: string, panel: \"send\" | \"receive\") => void;\n fromTokens: SwapTokenOption[];\n toToken?: SwapTokenOption;\n totalBalance: string;\n usdValue: string;\n onOpenSourcePicker: (index?: number) => void;\n onOpenDestPicker: () => void;\n onOpenRecipientPicker?: () => void;\n recipientAddress?: string;\n defaultRecipientAddress?: string;\n swapType: \"exactIn\" | \"exactOut\";\n allowOverBalanceAmounts?: boolean;\n onUpdateTokens?: (tokens: SwapTokenOption[]) => void;\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 if (!visible) return null;\n\n return (\n \n {[25, 50, 75, 100].map((pct) => {\n const label = pct === 100 ? maxLabel : `${pct}%`;\n return (\n onSelect(pct)}\n tabIndex={0}\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 = new Set(\n sources.map((source) => source.chainId ?? source.chainName).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 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 setHover(true)}\n onMouseLeave={() => {\n setHover(false);\n setActive(false);\n }}\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 onMouseDown={(event) => {\n event.preventDefault();\n setActive(true);\n }}\n onMouseUp={() => setActive(false)}\n onClick={() => {\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 tabIndex={tabIndex}\n style={{\n alignItems: \"center\",\n backgroundColor: isHighlighted ? \"#E8F0FF\" : \"#F4F4F3\",\n borderRadius: \"7px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flex: \"1 1 0%\",\n justifyContent: \"center\",\n paddingBlock: \"3px\",\n paddingInline: \"7px\",\n border: \"none\",\n cursor: \"pointer\",\n transition: \"background-color 0.2s ease-out\",\n }}\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 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 allowOverBalanceAmounts = false,\n onUpdateTokens,\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 sourceListRef = useRef(null);\n const sourceRowRefs = useRef>({});\n const previousSourceCountRef = useRef(fromTokens.length);\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 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 = 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 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 (!onUpdateTokens) return;\n const token = fromTokens[index];\n if (!token) return;\n\n let sanitized = sanitizeInput(\n val,\n token.userAmountMode === \"usd\" ? MAX_AMOUNT_DISPLAY_DECIMALS : getTokenInputDecimals(token),\n );\n\n // Enforce max amount validation\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 isUsdMode = token.userAmountMode === \"usd\";\n\n const maxAmt = isUsdMode ? fiatBalance : tokenBalance;\n if (!allowOverBalanceAmounts && Number(sanitized) > maxAmt) {\n if (isUsdMode) {\n sanitized = maxAmt.toFixed(2);\n } else {\n sanitized = String(token.balance).replace(/[^0-9.]/g, \"\");\n }\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 (!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\"\n ? sourceRouteMessage\n : 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 =\n 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 ? \"#006BF4\"\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 (!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 (!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: \"5px\",\n padding: \"2px 0\",\n color: fromTokens.length > 0 ? \"#006BF4\" : \"#A8A8A6\",\n cursor: fromTokens.length > 0 ? \"pointer\" : \"not-allowed\",\n fontFamily: '\"Geist\", system-ui, sans-serif',\n fontSize: \"12px\",\n fontWeight: 500,\n lineHeight: \"18px\",\n opacity: fromTokens.length > 0 ? 1 : 0.75,\n }}\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: \"6px\",\n opacity: 1,\n position: \"relative\",\n transform: \"translateY(0)\",\n transition:\n \"opacity 0.18s ease, transform 0.18s ease\",\n zIndex:\n tooltip === `asset-send-${index}`\n ? 1000\n : 1,\n }}\n >\n \n \n {showSourceRouteSkeleton ? (\n \n ) : (\n <>\n {token?.userAmountMode === \"usd\" && (\n \n $\n \n )}\n {\n if (token)\n handleTokenAmountChange(index, e.target.value);\n else handleSendInput(e);\n }}\n onFocus={() => setFocusedRow(index)}\n onBlur={() => {\n if (token) handleBlurAmount(index);\n setFocusedRow(null);\n }}\n style={{\n boxSizing: \"border-box\",\n color:\n (token\n ? Boolean(token.userAmount)\n : Boolean(isExactIn && amount))\n ? \"#161615\"\n : \"#9E9E9C\",\n fontFamily:\n '\"Delight-Medium\", \"Delight\", system-ui, sans-serif',\n fontSize: \"32px\",\n fontWeight: 500,\n lineHeight: \"38px\",\n background: \"transparent\",\n border: \"none\",\n outline: \"none\",\n padding: 0,\n width: \"100%\",\n minWidth: 0,\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: \"7px\",\n paddingBottom: \"4px\",\n paddingLeft: token ? \"4px\" : \"8px\",\n paddingRight: \"9px\",\n paddingTop: \"4px\",\n cursor: \"pointer\",\n flexShrink: 0,\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: \"22px\",\n height: \"22px\",\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 {showSourceRouteSkeleton ? (\n \n ) : (\n (() => {\n if (!token)\n return (\n \n โ‰ˆ ${usdValue || \"0.00\"}\n \n );\n const tokenBalance =\n Number(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 ? ` ${token.symbol}` : \"\";\n\n return (\n 0 ? \"pointer\" : \"default\",\n }}\n onClick={() => handleToggleMode(index)}\n >\n \n {approxPrefix}\n {approxValue}\n {approxSuffix}\n \n {price > 0 && }\n \n );\n })()\n )}\n {showSourceRouteSkeleton ? (\n \n ) : token && focusedRow === index ? (\n setTooltip(`asset-send-${index}`)}\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 {formatTokenBalanceLabel(token)}\n \n \n {/* Tooltip */}\n {tooltip === `asset-send-${index}` && (\n
\n
\n Asset Balance\n
\n
\n This is your current asset balance on this chain.\n
\n
\n )}\n \n ) : null}\n \n\n {/* 25% 50% 75% MAX โ€” shown only while the row amount is focused */}\n \n token\n ? handleSendPercentForToken(index, pct, token)\n : handleSendPercent(pct)\n }\n />\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: \"12px\",\n fontWeight: 500,\n gap: \"5px\",\n lineHeight: \"18px\",\n marginTop: \"-2px\",\n padding: 0,\n }}\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 \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 {receiveBalanceLabel}\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-one/components/swap-idle-form.tsx" }, @@ -97,7 +97,7 @@ }, { "path": "registry/nexus-elements/nexus-one/nexus-one.tsx", - "content": "\"use client\";\n\nimport React, {\n useState,\n useRef,\n useEffect,\n useCallback,\n useLayoutEffect,\n useMemo,\n} from \"react\";\nimport {\n type NexusOneProps,\n type NexusOneMode,\n type SwapType,\n type DepositOpportunity,\n} from \"./types\";\nimport { SwapIdleForm } from \"./components/swap-idle-form\";\nimport { SendIdleForm } from \"./components/send-idle-form\";\nimport { DepositIdleForm } from \"./components/deposit-idle-form\";\nimport { RecipientInput } from \"./components/recipient-input\";\nimport { StatusAlert } from \"./components/status-alerts\";\nimport {\n SwapAssetSelector,\n type SwapTokenOption,\n deriveTokenOptions,\n} from \"./components/swap-asset-selector\";\nimport {\n SwapIntentPreview,\n type SwapIntentData,\n} from \"./components/swap-intent-preview\";\nimport {\n NexusOneProgressScreen,\n type NexusOneProgressEvent,\n} from \"./components/nexus-one-progress-screen\";\nimport { ReceiveAssetSelector, preloadReceiveTokens } from \"./components/receive-asset-selector\";\nimport { OpportunityList } from \"./components/opportunity-list\";\nimport { AlertCircle, ArrowLeft, ChevronDown, Loader2 } from \"lucide-react\";\nimport { useNexus } from \"../nexus/NexusProvider\";\nimport { useTransactionSteps } from \"../common/tx/useTransactionSteps\";\nimport { findCitreaReceiveToken } from \"./utils/citrea-tokens\";\nimport {\n CHAIN_METADATA,\n ERROR_CODES,\n NEXUS_EVENTS,\n type BridgeStepType,\n type EthereumProvider,\n type SwapStepType,\n TOKEN_CONTRACT_ADDRESSES,\n TOKEN_METADATA,\n} from \"@avail-project/nexus-core\";\nimport {\n useAccount,\n useConnect,\n useConnectorClient,\n useWalletClient,\n usePublicClient,\n} from \"wagmi\";\nimport {\n erc20Abi,\n isAddress,\n zeroAddress,\n createPublicClient,\n http,\n encodeFunctionData,\n} from \"viem\";\nimport { normalize } from \"viem/ens\";\nimport { mainnet } from \"viem/chains\";\nimport Decimal from \"decimal.js\";\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 SwapHistoryStatus =\n | \"pending\"\n | \"fulfilled\"\n | \"failed\"\n | \"refund-initiated\";\n\ninterface SwapHistoryEntry {\n id: string;\n mode: NexusOneMode;\n status: SwapHistoryStatus;\n createdAt: number;\n startedAt: number;\n endedAt?: number;\n durationSeconds?: number;\n intentData: SwapIntentData | null;\n fromTokens: SwapTokenOption[];\n toToken?: SwapTokenOption;\n requestedToAmount?: string;\n requestedToValue?: string;\n recipientAddress?: string;\n opportunity?: DepositOpportunity;\n feeUsd?: string;\n intentId?: number;\n intentExplorerUrl?: string | null;\n sourceExplorerUrl?: string | null;\n finalExplorerUrl?: string | null;\n error?: string;\n failureMessage?: string;\n failedStepType?: string;\n autoRefundAvailable?: boolean;\n}\n\ntype SwapQuoteIssue = {\n type: \"insufficientSources\";\n message: string;\n missingUsd?: string;\n};\n\ntype CachedMaxSwapQuote = {\n decimals: number;\n maxTokenAmount: Decimal;\n maxUsdAmount?: Decimal;\n symbol: string;\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 PredictiveQuoteBaseline = {\n destinationUsdRate: string;\n exactInDestinationAmountPerSourceUsd?: string;\n exactOutSourceUsdPerDestinationUsd?: string;\n updatedAt: number;\n};\n\nconst QUOTE_REFRESH_INTERVAL_MS = 30000;\nconst EXACT_OUT_INPUT_DEBOUNCE_MS = 1000;\nconst DRAWER_CLOSE_MS = 220;\nconst MODAL_HEIGHT_TRANSITION_MS = 260;\nconst BASIS_POINTS = 10000;\nconst PREDICTIVE_EXACT_IN_DISCOUNT_BPS = 50;\nconst PREDICTIVE_EXACT_OUT_BUFFER_BPS = 100;\nconst PREDICTIVE_QUOTE_DISPLAY_DECIMALS = 8;\nconst SWAP_HISTORY_STORAGE_KEY_PREFIX = \"nexus-one-transaction-history-v1\";\nconst waitForNextPaint = () =>\n new Promise((resolve) => {\n if (typeof window === \"undefined\" || !window.requestAnimationFrame) {\n resolve();\n return;\n }\n window.requestAnimationFrame(() => {\n window.setTimeout(() => resolve(), 0);\n });\n });\nconst tooltipSurface = \"#FFFFFE\";\nconst tooltipText = \"var(--foreground-primary, #161615)\";\nconst tooltipBorder = \"var(--border-default, #E8E8E7)\";\nconst uiFont = '\"Geist\", var(--font-geist-sans), system-ui, sans-serif';\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`;\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 isSameTokenSelection = (\n a?: SwapTokenOption | null,\n b?: SwapTokenOption | null,\n) => Boolean(a && b && getTokenSelectionKey(a) === getTokenSelectionKey(b));\n\nconst sanitizeOpportunityForHistory = (\n opportunity?: DepositOpportunity,\n): DepositOpportunity | undefined => {\n if (!opportunity) return undefined;\n return {\n id: opportunity.id,\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 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 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 === \"refund-initiated\";\n\nconst isStoredMode = (value: unknown): value is NexusOneMode =>\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 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 = sortSwapHistoryEntries(entries).map(\n sanitizeHistoryEntry,\n );\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(true)}\n onMouseLeave={() => setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onBlur={() => setShowTooltip(false)}\n tabIndex={0}\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 >\n {showTooltip && (\n \n {tooltipLabel}\n \n )}\n \n \n \n \n \n );\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 = 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 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 const max = amount.abs().gte(1) ? 6 : 8;\n return formatDecimalDisplay(amount, { max });\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 hasValidIntentExplorer = (entry: Pick) =>\n Boolean(\n entry.intentExplorerUrl &&\n entry.intentId !== undefined &&\n Number.isFinite(entry.intentId) &&\n entry.intentId > 0,\n );\n\nconst getExplorerTxUrl = (chainId?: number, txHash?: string | null) => {\n if (!chainId || !txHash) return null;\n const chainMeta = CHAIN_METADATA[chainId];\n const baseUrl =\n (chainMeta as any)?.blockExplorerUrls?.[0] ||\n (chainMeta as any)?.blockExplorers?.default?.url;\n return baseUrl ? `${String(baseUrl).replace(/\\/$/, \"\")}/tx/${txHash}` : null;\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 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 size = 34,\n}: {\n tokenLogo?: string;\n chainLogo?: string;\n tokenSymbol?: string;\n chainName?: string;\n size?: number;\n}) {\n return (\n
\n \n {chainLogo && (\n \n )}\n
\n );\n}\n\nfunction TruncatedAddress({\n address,\n color = \"#006BF4\",\n}: {\n address: string;\n color?: string;\n}) {\n const [showTooltip, setShowTooltip] = useState(false);\n const label =\n address.length > 12 ? `${address.slice(0, 6)}...${address.slice(-4)}` : address;\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n tabIndex={0}\n style={{\n color,\n display: \"inline-flex\",\n fontFamily: uiFont,\n fontSize: \"13px\",\n fontWeight: 500,\n lineHeight: \"18px\",\n outline: \"none\",\n position: \"relative\",\n }}\n >\n {label}\n {showTooltip && (\n \n {address}\n \n )}\n \n );\n}\n\nconst getDisplayDestinationSourceRow = (entry: SwapHistoryEntry) => {\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(entry.intentData?.destination.amount);\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(entry.intentData?.destination.value);\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 tokenLogo: entry.toToken.logo,\n chainLogo: entry.toToken.chainLogo,\n symbol: entry.toToken.symbol,\n chainName: entry.toToken.chainName || \"\",\n amount: displayAmount\n .toDecimalPlaces(Math.max(0, entry.toToken.decimals ?? 18), Decimal.ROUND_DOWN)\n .toFixed(),\n value: rate ? displayAmount.mul(rate).toFixed() : entry.toToken.balanceInFiat,\n };\n};\n\nconst getProgressStepType = (\n step?: SwapStepType | BridgeStepType | null,\n) => String((step as any)?.type ?? (step as any)?.typeID ?? \"\").toUpperCase();\n\nconst isBridgeRefundStepType = (type: string) =>\n type.includes(\"RFF_ID\") || type.includes(\"BRIDGE_DEPOSIT\");\n\nconst isSwapSkippedStepType = (type: string) =>\n type.includes(\"SWAP_SKIPPED\");\n\nconst isAutoRefundAvailableProgressEvent = (\n event?: NexusOneProgressEvent,\n) =>\n event?.name === NEXUS_EVENTS.SWAP_STEP_COMPLETE &&\n isBridgeRefundStepType(getProgressStepType(event.step));\n\nconst getFailureMessageForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n mode: NexusOneMode,\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(\"SOURCE_SWAP\") ||\n type.includes(\"COLLECTION\")\n ) {\n return \"Collection Failed\";\n }\n if (\n type.includes(\"DESTINATION_SWAP\") ||\n type.includes(\"FULFIL\")\n ) {\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(\"RFF\") ||\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 getSourceRows = (entry: SwapHistoryEntry) => {\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 tokenLogo: fallback?.logo,\n chainLogo: source.chain.logo || fallback?.chainLogo,\n symbol: source.token.symbol,\n chainName: source.chain.name,\n amount: source.amount,\n value: source.value,\n };\n });\n\n return displayDestinationSourceRow\n ? [displayDestinationSourceRow, ...sourceRows]\n : sourceRows;\n }\n\n const fallbackRows = entry.fromTokens.map((token, index) => ({\n key: `${token.chainId}-${token.contractAddress}-${index}`,\n tokenLogo: token.logo,\n chainLogo: token.chainLogo,\n symbol: token.symbol,\n chainName: token.chainName || \"\",\n amount: token.userAmount || \"0\",\n value: token.balanceInFiat,\n }));\n\n return displayDestinationSourceRow\n ? [displayDestinationSourceRow, ...fallbackRows]\n : fallbackRows;\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 scrollRef.current?.scrollBy({ top: 72, behavior: \"smooth\" })}\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 >\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 isDeposit = entry.mode === \"deposit\";\n const isSend = entry.mode === \"send\";\n const tokenSymbol = destination?.token.symbol || entry.toToken?.symbol || \"\";\n const chainName = destination?.chain.name || entry.toToken?.chainName || \"\";\n const depositVenue =\n entry.opportunity?.title || entry.opportunity?.protocol || chainName;\n const amount = destination?.amount || \"\";\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 = `Intent #${entry.intentId}`;\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 storedFailureMessage =\n !entry.autoRefundAvailable && entry.failureMessage?.includes(\"Refund\")\n ? undefined\n : entry.failureMessage;\n const failureHeadline =\n storedFailureMessage ||\n (isDeposit\n ? \"Deposit failed. Funds are in your wallet\"\n : isSend\n ? \"Send failed. Funds are in your wallet\"\n : defaultSwapFailureHeadline);\n const receiptLocation = isDeposit ? depositVenue : chainName;\n const receiptSummary = receiptLocation ? `on ${receiptLocation}` : \"\";\n\n return (\n
\n \n \n \n \n {isFailed ? \"x\" : \"โœ“\"}\n
\n \n
\n {isFailed\n ? failureHeadline\n : isDeposit\n ? \"You deposited\"\n : isSend\n ? \"You sent\"\n : \"You received\"}\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: \"#006BF4\",\n cursor: \"pointer\",\n display: \"inline-flex\",\n fontFamily: uiFont,\n fontSize: \"12px\",\n gap: \"4px\",\n padding: 0,\n }}\n >\n {showSourceDetails ? \"Hide Details\" : `${sourceCount} asset${sourceCount === 1 ? \"\" : \"s\"}`}\n \n \n \n \n \n
\n \n
\n \n {isSend && 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({\n status,\n}: {\n status: SwapHistoryStatus;\n}) {\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 === \"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 onRefund,\n}: {\n entries: SwapHistoryEntry[];\n now: number;\n onRefund: (entry: SwapHistoryEntry) => void;\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 swap,\n 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 = entry.toToken?.logo;\n const destinationChainLogo =\n destination?.chain.logo || entry.toToken?.chainLogo || \"\";\n const destinationChainName =\n destination?.chain.name || entry.toToken?.chainName || \"\";\n const destinationSymbol = 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 showIntentExplorer = hasValidIntentExplorer(entry);\n const viewUrl = showIntentExplorer\n ? entry.intentExplorerUrl\n : entry.finalExplorerUrl;\n const canShowRefund =\n entry.status === \"failed\" &&\n Boolean(entry.autoRefundAvailable);\n const status = canShowRefund ? \"refund-initiated\" : entry.status;\n const sourceRows = getSourceRows(entry);\n const firstSource = sourceRows[0];\n\n return (\n \n
\n
\n \n
\n
\n {destinationAmount ? formatTokenDisplay(destinationAmount) : \"--\"}\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 onRefund(entry)}\n style={{\n background: \"#006BF4\",\n border: \"none\",\n borderRadius: \"8px\",\n color: \"#FFFFFE\",\n cursor: entry.intentId ? \"pointer\" : \"not-allowed\",\n fontFamily: uiFont,\n fontSize: \"13px\",\n fontWeight: 600,\n opacity: entry.intentId ? 1 : 0.5,\n padding: \"8px 14px\",\n }}\n >\n Refund\n \n \n )}\n\n \n
\n {firstSource && (\n \n )}\n \n โ†’\n \n \n {showIntentExplorer ? (\n \n Intent #{entry.intentId}\n \n ) : entry.finalExplorerUrl ? (\n \n Final transaction\n \n ) : null}\n
\n {viewUrl && (\n \n View โ†—\n \n )}\n \n \n );\n })}\n \n );\n}\n\n// ---------------------------------------------------------------------------\n// NexusOne\n// ---------------------------------------------------------------------------\n\nexport function NexusOne({\n config,\n embed = true,\n connectedAddress,\n onComplete,\n onStart,\n onError,\n onClose,\n}: NexusOneProps) {\n const {\n nexusSDK,\n bridgableBalance,\n swapBalance,\n getFiatValue,\n resolveTokenUsdRate,\n swapSupportedChainsAndTokens,\n supportedChainsAndTokens,\n fetchSwapBalance,\n handleInit,\n loading: nexusLoading,\n } = useNexus();\n\n // Mode is a single value, not an array\n const activeMode = config.mode;\n if (\n activeMode === \"deposit\" &&\n (!config.opportunities || config.opportunities.length === 0)\n ) {\n throw new Error(\n \"NexusOne deposit mode requires config.opportunities with at least one opportunity.\",\n );\n }\n const showCloseButton = !embed && Boolean(onClose);\n\n // Preload receive tokens once SDK is available\n useEffect(() => {\n if (nexusSDK) {\n preloadReceiveTokens();\n }\n }, [nexusSDK]);\n\n const { connector, status: walletStatus } = 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 : walletClientAddress &&\n isAddress(walletClientAddress) &&\n walletClientAddress.toLowerCase() !== zeroAddress\n ? walletClientAddress\n : undefined;\n const historyStorageKey = getSwapHistoryStorageKey(ownerAddress);\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 previousDefaultRecipientRef = useRef(defaultRecipientAddress);\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 [closingDrawerStep, setClosingDrawerStep] =\n useState(null);\n const rootContentRef = useRef(null);\n const [rootContentHeight, setRootContentHeight] = useState(\n null,\n );\n const [hasMeasuredRootContent, setHasMeasuredRootContent] = useState(false);\n const [fromTokens, setFromTokens] = useState([]);\n const [sourceSelectionTouched, setSourceSelectionTouched] = useState(false);\n const [sourceSelectionRevision, setSourceSelectionRevision] = useState(0);\n const [, setExactOutQuoteSourceMode] = useState<\"all\" | \"selected\">(\"all\");\n const exactOutQuoteSourceModeRef = useRef<\"all\" | \"selected\">(\"all\");\n const [toToken, setToToken] = useState(\n undefined,\n );\n const appliedTokenPrefillRef = useRef(null);\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 }, [activeMode, fetchSwapBalance, nexusSDK, swapStep]);\n\n useEffect(() => {\n setSourceSelectionTouched(false);\n setExactOutQuoteSourceModeValue(\"all\");\n }, [activeMode, setExactOutQuoteSourceModeValue]);\n\n useEffect(() => {\n const previousDefault = previousDefaultRecipientRef.current;\n previousDefaultRecipientRef.current = defaultRecipientAddress;\n\n if (activeMode !== \"swap\" || !defaultRecipientAddress) return;\n\n setRecipientAddress((current) => {\n if (\n !current ||\n (previousDefault &&\n current.toLowerCase() === previousDefault.toLowerCase())\n ) {\n return defaultRecipientAddress;\n }\n return current;\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 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 maxSwapQuoteCacheRef = useRef>({});\n const intentDestinationUsdRateCacheRef = useRef<\n Record\n >({});\n const intentSymbolUsdRateCacheRef = useRef>(\n {},\n );\n const predictiveQuoteCacheRef = useRef>(\n {},\n );\n const predictiveQuoteRunRef = useRef(0);\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 [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\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 } | null>(null);\n\n useEffect(() => {\n swapStepRef.current = swapStep;\n }, [swapStep]);\n\n useEffect(() => {\n return () => {\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n }\n };\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 setSwapStep(\"idle\");\n return;\n }\n\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n }\n\n setClosingDrawerStep(swapStep);\n drawerCloseTimerRef.current = setTimeout(() => {\n setSwapStep(\"idle\");\n setClosingDrawerStep(null);\n drawerCloseTimerRef.current = null;\n }, DRAWER_CLOSE_MS);\n }, [swapStep]);\n\n const openDrawerStep = useCallback((nextStep: SwapStep) => {\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n drawerCloseTimerRef.current = null;\n }\n setClosingDrawerStep(null);\n setSwapStep(nextStep);\n }, []);\n\n const syncRootContentHeight = useCallback(() => {\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 setRootContentHeight((previousHeight) =>\n previousHeight === nextHeight ? previousHeight : nextHeight,\n );\n setHasMeasuredRootContent(true);\n }, []);\n\n useLayoutEffect(() => {\n syncRootContentHeight();\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(syncRootContentHeight);\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 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?.icon)\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 = matchedAsset?.icon ?? (isNativeSource ? chainMeta?.logo : \"\");\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: 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: chainMeta?.name ?? source.chain.name,\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 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 setExactOutQuoteSourceModeValue(\"all\");\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 = 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\n const minimumSourceUsd = new Decimal(1);\n const hasMinimumSourceUsdBalance = (\n token: Pick,\n ) => (parseFiatNumber(token.balanceInFiat) ?? new Decimal(0)).gte(minimumSourceUsd);\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 = (intent?: SwapIntentData | null) => {\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 (!amount || !value || amount.lte(0) || value.lte(0) || !chainId || !symbol) {\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(\n (breakdownSum, item) => {\n const value = parseFiatNumber(item.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd)\n ? breakdownSum.plus(value)\n : breakdownSum;\n },\n new Decimal(0),\n ),\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 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 const getTotalBalancePercentUsdAmount = (pct: number) =>\n getSwapBalanceTotalUsd().mul(pct).div(100);\n const formatTokenAmountFromUsd = (\n usdAmount: Decimal,\n token: Pick,\n ) => {\n const rate = getUsdRateForSymbol(token.symbol);\n if (rate.lte(0)) return undefined;\n return usdAmount\n .div(rate)\n .toDecimalPlaces(Math.max(0, token.decimals ?? 18), Decimal.ROUND_DOWN)\n .toFixed();\n };\n\n const getMaxSwapQuoteCacheKey = (token?: SwapTokenOption) => {\n if (!token?.chainId) return \"\";\n return [\n token.chainId,\n (token.contractAddress || zeroAddress).toLowerCase(),\n token.symbol.toUpperCase(),\n ].join(\":\");\n };\n\n const getCachedMaxSwapQuote = (token?: SwapTokenOption) => {\n const key = getMaxSwapQuoteCacheKey(token);\n return key ? maxSwapQuoteCacheRef.current[key] : undefined;\n };\n\n const getCachedDestinationUsdRate = (token?: SwapTokenOption) => {\n const intentCachedRate = getCachedIntentUsdRate(token);\n if (intentCachedRate && intentCachedRate.gt(0)) {\n return intentCachedRate;\n }\n\n const cached = getCachedMaxSwapQuote(token);\n if (\n !cached ||\n !cached.maxUsdAmount ||\n cached.maxUsdAmount.lte(0) ||\n cached.maxTokenAmount.lte(0)\n ) {\n return undefined;\n }\n return cached.maxUsdAmount.div(cached.maxTokenAmount);\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 resolveMaxSwapQuote = async (token: SwapTokenOption) => {\n const key = getMaxSwapQuoteCacheKey(token);\n if (!key) return undefined;\n\n const cached = maxSwapQuoteCacheRef.current[key];\n if (cached) return cached;\n\n const calculateMaxForSwap = nexusSDK?.calculateMaxForSwap;\n if (typeof calculateMaxForSwap !== \"function\" || !token.chainId) {\n return undefined;\n }\n\n const max = await calculateMaxForSwap({\n toChainId: token.chainId,\n toTokenAddress: (token.contractAddress || zeroAddress) as `0x${string}`,\n });\n const decimals = Number.isFinite(Number(max.decimals))\n ? Number(max.decimals)\n : token.decimals || 18;\n const maxAmount =\n parseFiatNumber(max.maxAmount) ??\n (max.maxAmountRaw !== undefined\n ? new Decimal(max.maxAmountRaw.toString()).div(\n new Decimal(10).pow(decimals),\n )\n : undefined);\n\n if (!maxAmount || maxAmount.lte(0)) return undefined;\n\n const safeMaxAmount = maxAmount.mul(receiveMaxSafetyMultiplier);\n const destinationRate = await resolveUsdRateForSymbol(max.symbol || token.symbol);\n let maxUsdAmount =\n destinationRate.gt(0) ? safeMaxAmount.mul(destinationRate) : undefined;\n\n if (!maxUsdAmount || maxUsdAmount.lte(0)) {\n const sourcesUsd = await (max.sources ?? []).reduce(\n async (sumPromise, source) => {\n const sum = await sumPromise;\n const amount = parseFiatNumber(source.amount) ?? new Decimal(0);\n if (amount.lte(0)) return sum;\n\n const sourceRate = await resolveUsdRateForSymbol(source.symbol);\n return sourceRate.gt(0) ? sum.plus(amount.mul(sourceRate)) : sum;\n },\n Promise.resolve(new Decimal(0)),\n );\n\n if (sourcesUsd.gt(0)) {\n maxUsdAmount = sourcesUsd.mul(receiveMaxSafetyMultiplier);\n }\n }\n\n const quote: CachedMaxSwapQuote = {\n decimals,\n maxTokenAmount: safeMaxAmount,\n maxUsdAmount,\n symbol: max.symbol || token.symbol,\n };\n maxSwapQuoteCacheRef.current[key] = quote;\n return quote;\n };\n\n const getPercentAmountFromMaxQuote = async (\n token: SwapTokenOption,\n pct: number,\n preferUsd: boolean,\n ) => {\n const maxQuote = await resolveMaxSwapQuote(token);\n if (!maxQuote) return undefined;\n\n const ratio = new Decimal(pct).div(100);\n if (preferUsd && maxQuote.maxUsdAmount && maxQuote.maxUsdAmount.gt(0)) {\n return {\n amount: maxQuote.maxUsdAmount\n .mul(ratio)\n .toDecimalPlaces(2, Decimal.ROUND_DOWN)\n .toFixed(),\n mode: \"usd\" as const,\n };\n }\n\n return {\n amount: maxQuote.maxTokenAmount\n .mul(ratio)\n .toDecimalPlaces(Math.max(0, maxQuote.decimals), Decimal.ROUND_DOWN)\n .toFixed(),\n mode: \"token\" as const,\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 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 = (token: SwapTokenOption, tokenAmount: Decimal) => {\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 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(source, targetUsd)\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.toDecimalPlaces(6, Decimal.ROUND_DOWN).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(remainingTokenAmount, 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.toDecimalPlaces(6, Decimal.ROUND_DOWN).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 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 ? token.sourceTokens : [token],\n );\n const seen = new Set();\n return expanded.filter((token) => {\n if (!token.chainId || !token.contractAddress) return false;\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 = 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 = (breakdown.symbol ?? asset.symbol ?? \"\").toUpperCase();\n const assetSymbol = (asset.symbol ?? \"\").toUpperCase();\n const isNativeBalance =\n isNativeTokenAddress(breakdown.contractAddress) ||\n Boolean(nativeSymbol && (breakdownSymbol === nativeSymbol || assetSymbol === nativeSymbol));\n\n if (!isNativeBalance) continue;\n balance = balance.plus(parseFiatNumber(breakdown.balance) ?? new Decimal(0));\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 ) 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: chainMeta?.name ?? breakdown.chain?.name,\n contractAddress,\n decimals: breakdown.decimals ?? asset.decimals ?? 18,\n logo: asset.icon ?? \"\",\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 getExactOutSourceTokens = (\n mode: \"all\" | \"selected\" = exactOutQuoteSourceModeRef.current,\n ) => {\n if (\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n mode === \"selected\" &&\n fromTokens.length > 0\n ) {\n return filterMinimumSourceUsdTokens(getExpandedSourceTokens(fromTokens)).filter(\n hasGasForSource,\n );\n }\n\n return getGasCapableBalanceSourceTokens();\n };\n\n const buildFromSourcesPayload = (tokens: SwapTokenOption[]) => {\n const eligibleTokens = filterMinimumSourceUsdTokens(tokens).filter(\n (token) => token.chainId && token.contractAddress,\n );\n return {\n fromSources: eligibleTokens.map((token) => ({\n chainId: token.chainId!,\n tokenAddress: token.contractAddress as `0x${string}`,\n })),\n };\n };\n\n const buildPredictiveExactOutSources = async (requiredSourceUsd: Decimal) => {\n if (requiredSourceUsd.lte(0)) return [];\n\n const destinationKey = getTokenSelectionKey(toToken);\n const candidates = getExactOutSourceTokens()\n .filter((token) => getTokenSelectionKey(token) !== destinationKey)\n .filter((token) => getTokenBalanceUsd(token).gt(0))\n .sort((a, b) => getTokenBalanceUsd(b).cmp(getTokenBalanceUsd(a)));\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.lte(0)) 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.toDecimalPlaces(6, Decimal.ROUND_DOWN).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 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 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 const selectedSourceTotal =\n exactOutQuoteSourceModeRef.current === \"selected\" && fromTokens.length > 0\n ? fromTokens.reduce(\n (sum, token) => {\n const value = parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd) ? sum.plus(value) : sum;\n },\n new Decimal(0),\n )\n : undefined;\n\n if (selectedSourceTotal && selectedSourceTotal.gt(0)) {\n return selectedSourceTotal;\n }\n\n const allSourceTotal = getGasCapableBalanceSourceTokens().reduce(\n (sum, token) => {\n const value = parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd) ? sum.plus(value) : sum;\n },\n new Decimal(0),\n );\n\n return allSourceTotal.gt(0) ? allSourceTotal : getSwapBalanceTotalUsd();\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(missingTokenAmount.mul(fiatBalance.div(availableTokenAmount)));\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 = (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 = getExactOutRequestedUsd();\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 ||\n missingUsd.lte(0) ||\n 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 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()).div(new Decimal(10).pow(decimals)).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 receiveMaxSafetyMultiplier = new Decimal(\"0.9\");\n const currentSwapEntry =\n currentSwapId !== null\n ? swapHistory.find((entry) => entry.id === currentSwapId)\n : undefined;\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) =>\n entry.id === id ? { ...entry, ...patch } : entry,\n ),\n ),\n );\n };\n\n const patchCurrentSwapHistoryEntry = (patch: Partial) => {\n patchSwapHistoryEntry(currentSwapIdRef.current, patch);\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,\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 ) => {\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 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: activeMode === \"send\" ? recipientAddress : undefined,\n opportunity: selectedOpportunity,\n feeUsd: intentFeeUsd,\n sourceExplorerUrl: null,\n finalExplorerUrl: null,\n intentExplorerUrl: null,\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\",\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(\n 1,\n Math.round((now - startedAt) / 1000),\n ),\n sourceExplorerUrl: explorerUrlsRef.current.sourceExplorerUrl,\n finalExplorerUrl: explorerUrlsRef.current.destinationExplorerUrl,\n ...patch,\n });\n void fetchSwapBalance();\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 handleRefundIntent = async (entry: SwapHistoryEntry) => {\n if (!nexusSDK || !entry.intentId) return;\n patchSwapHistoryEntry(entry.id, { status: \"refund-initiated\" });\n try {\n await nexusSDK.refundIntent(entry.intentId);\n void fetchSwapBalance();\n } catch (error: any) {\n patchSwapHistoryEntry(entry.id, {\n status: \"failed\",\n error: error?.message || \"Refund failed. Please try again.\",\n });\n void fetchSwapBalance();\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 lastSwapIntentRefreshAtRef.current = Date.now();\n cacheDestinationUsdRateFromIntent(intent);\n cachePredictiveBaselineFromIntent(intent);\n setIntentData(intent);\n setIntentToAmount(intent.destination?.amount || undefined);\n setSwapQuoteIssue(null);\n\n if (\n (activeMode === \"send\" ||\n (activeMode === \"deposit\" && swapType === \"exactOut\"))\n ) {\n syncingIntentSourcesRef.current = true;\n setFromTokens((intent.sources ?? []).map(buildIntentSourceToken));\n }\n\n try {\n const bridgeFees = intent.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 bridgeComponentsTotal = bridgeFeeData\n ? [\n executionGasFee,\n parseFiatNumber(bridgeFeeData.protocol),\n parseFiatNumber(bridgeFeeData.solver),\n parseFiatNumber(bridgeFeeData.gasSupplied),\n ].reduce(\n (sum, value) => sum.plus(value ?? new Decimal(0)),\n new Decimal(0),\n )\n : undefined;\n const bridgeTotal =\n typeof bridgeFees === \"string\"\n ? parseFiatNumber(bridgeFees)\n : parseFiatNumber(bridgeFeeData?.total) ??\n (bridgeComponentsTotal && bridgeComponentsTotal.gt(0)\n ? bridgeComponentsTotal\n : undefined);\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 [activeMode, fromTokens, swapType, swapBalance, toToken],\n );\n\n // Register swap intent hook immediately before executing a swap to prevent race conditions across multiple components\n const registerIntentHook = (runId: number) => {\n if (!nexusSDK) return;\n nexusSDK.setOnSwapIntentHook(async ({ intent, allow, deny, refresh }) => {\n if (swapRunIdRef.current !== runId) {\n deny();\n return;\n }\n // Store callbacks so accept/reject buttons can call them\n swapIntentRef.current = { intent, allow, deny, refresh, runId };\n // Populate intent data for preview\n applySwapIntent(intent);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n });\n };\n\n // Deposit-specific\n const [selectedOpportunity, setSelectedOpportunity] = useState<\n DepositOpportunity | undefined\n >(() =>\n activeMode === \"deposit\" && config.opportunities?.length === 1\n ? config.opportunities[0]\n : undefined,\n );\n const [pendingOpportunity, setPendingOpportunity] = useState<\n DepositOpportunity | undefined\n >(undefined);\n const [depositAmountMode, setDepositAmountMode] = useState<\"token\" | \"usd\">(\n \"token\",\n );\n\n const toTokenFromOpportunity = (\n opp: DepositOpportunity,\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() ||\n 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: matchedToken?.decimals ?? citreaToken?.decimals ?? tokenMeta?.decimals ?? 18,\n logo: opp.tokenLogo || matchedToken?.logo || citreaToken?.logo || tokenMeta?.icon,\n chainName: CHAIN_METADATA[opp.chainId]?.name ?? citreaToken?.chainName,\n chainLogo: CHAIN_METADATA[opp.chainId]?.logo ?? citreaToken?.chainLogo,\n };\n };\n\n const getDestinationBalanceFromSwapBalances = (\n token?: SwapTokenOption,\n ) => {\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() === 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?: { token: `0x${string}`; chain: number }) => {\n if (!pair?.token || !pair.chain) return undefined;\n\n const normalizeAddress = (address?: string) => {\n if (!address) return \"\";\n return isNativeTokenAddress(address) ? zeroAddress : address.toLowerCase();\n };\n const targetAddress = normalizeAddress(pair.token);\n\n const balanceToken = deriveTokenOptions(swapBalance ?? []).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((item) => item.id === pair.chain);\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 tokenAddressSymbol ??\n (isNativePrefill ? chainMeta?.nativeCurrency?.symbol : undefined) ??\n \"Token\";\n const tokenMeta = TOKEN_METADATA[tokenSymbol as keyof typeof TOKEN_METADATA];\n\n if (\n !chain &&\n !matchedToken &&\n !citreaToken &&\n !tokenAddressSymbol &&\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 tokenMeta?.decimals ??\n (isNativePrefill ? chainMeta?.nativeCurrency?.decimals : undefined) ??\n 18,\n logo: matchedToken?.logo || citreaToken?.logo || tokenMeta?.icon,\n chainName:\n chain?.name ?? chainMeta?.name ?? citreaToken?.chainName,\n chainLogo:\n chain?.logo ?? chainMeta?.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([{ ...sourceToken, userAmount: config.prefill?.amount ?? \"\" }]);\n setSourceSelectionTouched(true);\n }\n if (destinationToken) {\n setToToken(destinationToken);\n }\n setSwapType(\"exactIn\");\n appliedTokenPrefillRef.current = prefillKey;\n }, [\n activeMode,\n config.prefill?.amount,\n config.prefill?.destination?.chain,\n config.prefill?.destination?.token,\n config.prefill?.source?.chain,\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?.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 = getDestinationBalanceFromSwapBalances(balanceToken);\n if (swapBalanceValue) {\n setDestinationBalance(swapBalanceValue);\n return;\n }\n\n const chainMeta = CHAIN_METADATA[balanceToken.chainId];\n const rpcUrl = chainMeta?.rpcUrls?.[0];\n if (!rpcUrl) return;\n\n let cancelled = false;\n const client = createPublicClient({\n chain: {\n id: balanceToken.chainId,\n name: chainMeta?.name ?? balanceToken.chainName ?? \"Destination Chain\",\n nativeCurrency: chainMeta?.nativeCurrency ?? {\n decimals: 18,\n name: \"Ether\",\n symbol: \"ETH\",\n },\n rpcUrls: {\n default: { http: [rpcUrl] },\n public: { http: [rpcUrl] },\n },\n blockExplorers: chainMeta?.blockExplorerUrls?.[0]\n ? {\n default: {\n name: chainMeta.name,\n url: chainMeta.blockExplorerUrls[0],\n },\n }\n : undefined,\n } as any,\n transport: http(rpcUrl),\n });\n\n const fetchDestinationBalance = async () => {\n try {\n let rawBalance: bigint;\n let decimals = balanceToken.decimals || 18;\n\n if (isNativeTokenAddress(balanceToken.contractAddress)) {\n rawBalance = await client.getBalance({\n address: ownerAddress as `0x${string}`,\n });\n decimals = 18;\n } else {\n const tokenAddress = balanceToken.contractAddress as `0x${string}`;\n const [balanceResult, decimalsResult] = await Promise.all([\n client.readContract({\n abi: erc20Abi,\n address: tokenAddress,\n functionName: \"balanceOf\",\n args: [ownerAddress as `0x${string}`],\n }) as Promise,\n client\n .readContract({\n abi: erc20Abi,\n address: tokenAddress,\n functionName: \"decimals\",\n })\n .catch(() => decimals),\n ]);\n\n rawBalance = balanceResult;\n decimals = Number(decimalsResult) || decimals;\n }\n\n if (!cancelled) {\n setDestinationBalance(\n `${formatReadableTokenBalanceAmount(rawBalance, decimals)} ${balanceToken.symbol}`,\n );\n }\n } catch (error) {\n console.warn(\"Unable to fetch destination token balance\", error);\n }\n };\n\n void fetchDestinationBalance();\n\n return () => {\n cancelled = true;\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\") return;\n if (selectedOpportunity) return;\n if (config.opportunities?.length === 1) {\n const [opp] = config.opportunities;\n setSelectedOpportunity(opp);\n setSwapType(\"exactOut\");\n setToToken(toTokenFromOpportunity(opp));\n }\n }, [activeMode, config.opportunities, selectedOpportunity, supportedChainsAndTokens]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || !selectedOpportunity) return;\n setToToken((current) => ({\n ...toTokenFromOpportunity(selectedOpportunity),\n balance: current?.balance ?? \"0\",\n balanceInFiat: current?.balanceInFiat ?? \"$0.00\",\n }));\n }, [activeMode, selectedOpportunity, supportedChainsAndTokens]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (selectedOpportunity) return;\n if (!config.opportunities || config.opportunities.length <= 1) return;\n setPendingOpportunity((current) => current ?? config.opportunities?.[0]);\n }, [activeMode, config.opportunities, selectedOpportunity]);\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 One 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) return new Decimal(0);\n const fiat = getFiatValue(1, selectedOpportunity.tokenSymbol);\n if (Number.isFinite(fiat) && fiat > 0) {\n return new Decimal(fiat);\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 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?.toDecimalPlaces(toToken?.decimals ?? 18).toFixed() ??\n \"0\";\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 = () => {\n const shouldLoadQuote = Boolean(nexusSDK && canRefreshExactOutQuote());\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\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 =\n destinationUsdNeedingSources.lte(0)\n ? new Decimal(0)\n : cachedSourceUsdRatio && cachedSourceUsdRatio.gt(0)\n ? destinationUsdNeedingSources.mul(cachedSourceUsdRatio)\n : destinationUsdNeedingSources\n .mul(BASIS_POINTS + PREDICTIVE_EXACT_OUT_BUFFER_BPS)\n .div(BASIS_POINTS);\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 fromTokens,\n nexusSDK,\n selectedOpportunity,\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 defaultDepositSourceTokens = useMemo(() => {\n if (activeMode !== \"deposit\" || !swapBalance) return [];\n return deriveTokenOptions(swapBalance)\n .filter(hasMinimumSourceUsdBalance)\n .map((token) => ({\n ...token,\n userAmount: \"\",\n }));\n }, [activeMode, swapBalance]);\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 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 toToken.contractAddress &&\n (breakdownAddress.toLowerCase() === 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 if (!fiatBalance || fiatBalance.lt(minimumSourceUsd)) continue;\n return [\n {\n chainId,\n chainLogo: chainMeta?.logo ?? breakdown.chain?.logo ?? toToken.chainLogo,\n chainName: chainMeta?.name ?? breakdown.chain?.name ?? toToken.chainName,\n contractAddress: breakdown.contractAddress ?? toToken.contractAddress,\n decimals: breakdown.decimals ?? asset.decimals ?? toToken.decimals ?? 18,\n logo: asset.icon ?? 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 },\n ];\n }\n }\n\n return [];\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 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) => getTokenSelectionKey(token) === getTokenSelectionKey(locked),\n ),\n );\n if (missing.length === 0) return current;\n return [...current, ...missing.map((token) => ({ ...token, userAmount: \"\" }))];\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 (\n defaultDepositSourceTokens.length === 0 &&\n lockedDestinationSourceTokens.length === 0\n ) {\n return;\n }\n\n setFromTokens((current) => {\n const lockedKeys = new Set(\n lockedDestinationSourceTokens.map(getTokenSelectionKey),\n );\n const canInitialize =\n current.length === 0 ||\n current.every((token) => lockedKeys.has(getTokenSelectionKey(token)));\n if (!canInitialize) return current;\n\n const next: SwapTokenOption[] = [];\n const seen = new Set();\n for (const token of [\n ...defaultDepositSourceTokens,\n ...lockedDestinationSourceTokens,\n ]) {\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 defaultDepositSourceTokens,\n depositTokenAmountForQuote?.toFixed(),\n lockedDestinationSourceTokens,\n sourceSelectionTouched,\n toToken,\n ]);\n\n // ---------------------------------------------------------------------------\n // Handlers\n // ---------------------------------------------------------------------------\n\n const handleReset = () => {\n clearPendingSwapIntent();\n setAmount(\"\");\n setRecipientAddress(\"\");\n setTxError(null);\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n clearSelectedSources();\n setToToken(undefined);\n setSelectedOpportunity(undefined);\n setPendingOpportunity(undefined);\n setDepositAmountMode(\"token\");\n };\n\n const resetInputsAfterSuccessfulExecution = () => {\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 setToToken(undefined);\n setDepositAmountMode(\"token\");\n };\n\n const handleSelectDepositOpportunity = (opp: DepositOpportunity) => {\n clearPendingSwapIntent();\n setTxError(null);\n setSwapQuoteIssue(null);\n setSelectedOpportunity(opp);\n setPendingOpportunity(opp);\n setSwapType(\"exactOut\");\n setDepositAmountMode(\"token\");\n setAmount(\"\");\n clearSelectedSources();\n setToToken(toTokenFromOpportunity(opp));\n };\n\n const handleClose = () => {\n clearPendingSwapIntent();\n onClose?.();\n };\n\n const handleConnectWallet = async () => {\n if (walletActionPending || nexusLoading || 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 activeConnector = nextConnector;\n }\n\n const connectorProvider = await activeConnector\n ?.getProvider()\n .catch(() => undefined);\n const connectorClientProvider = connectorClient\n ? {\n request: (args: unknown) =>\n connectorClient.request(args as any),\n }\n : undefined;\n const walletClientProvider = walletClient\n ? {\n request: (args: unknown) =>\n 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 ??\n walletClientProvider ??\n windowProvider);\n\n if (!effectiveProvider || typeof effectiveProvider.request !== \"function\") {\n throw new Error(\"Wallet provider is not ready yet.\");\n }\n\n await handleInit(effectiveProvider as EthereumProvider);\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 โ€” SDK will trigger setOnSwapIntentHook for preview */\n const handleEnterPreview = async (\n options: { background?: boolean } = {},\n ) => {\n const { background = false } = options;\n const isExactOutFlow = activeMode === \"deposit\" || activeMode === \"send\";\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 setTxError(null);\n setSwapQuoteIssue(null);\n\n if (\n !background &&\n swapIntentRef.current?.runId === swapRunIdRef.current &&\n intentData &&\n !intentLoading &&\n (activeMode !== \"send\" || Boolean(recipientAddress)) &&\n ((activeMode !== \"deposit\" && activeMode !== \"send\") ||\n (intentData.sources ?? []).length > 0)\n ) {\n swapStepRef.current = \"preview-intent\";\n setSwapStep(\"preview-intent\");\n return;\n }\n\n if (\n !background &&\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (!intentData ||\n !swapIntentRef.current ||\n swapIntentRef.current.runId !== swapRunIdRef.current ||\n (intentData.sources ?? []).length === 0)\n ) {\n setTxError(\"Quote unavailable. Please wait for sources to be selected.\");\n return;\n }\n\n const hasCustomSwapRecipient =\n activeMode === \"swap\" &&\n Boolean(recipientAddress) &&\n (!defaultRecipientAddress ||\n recipientAddress.toLowerCase() !== defaultRecipientAddress.toLowerCase());\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() ===\n ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\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 // Claim ownership of global singleton hook before executing SDK swap\n registerIntentHook(runId);\n\n const getSwapStepListFromEvent = (event: { args: any }) => {\n const args = (event as any).args;\n return Array.isArray(args)\n ? args\n : Array.isArray(args?.steps)\n ? args.steps\n : [];\n };\n\n const handleSwapEvent = (event: { name: string; args: any }) => {\n console.log(\"[NexusOne][SDK swap event]\", event.name, event);\n if (event.name === NEXUS_EVENTS.SWAP_STEPS_LIST) {\n const stepList = getSwapStepListFromEvent(event);\n if (stepList.length > 0) {\n swapStepsListRef.current = stepList as SwapStepType[];\n appendProgressListEvent(event.name, stepList);\n onStepsList(stepList);\n }\n return;\n }\n if (event.name === NEXUS_EVENTS.STEPS_LIST) {\n const args = (event as any).args;\n const stepList = Array.isArray(args)\n ? args\n : Array.isArray(args?.steps)\n ? args.steps\n : [];\n if (stepList.length > 0) {\n appendProgressListEvent(event.name, stepList);\n onStepsList(stepList);\n }\n return;\n }\n if (event.name === NEXUS_EVENTS.STEP_COMPLETE) {\n const step = event.args as BridgeStepType;\n appendProgressEvent(event.name, step, true);\n if (\n (step as any)?.type === \"TRANSACTION_SENT\" ||\n (step as any)?.type === \"TRANSACTION_CONFIRMED\"\n ) {\n markSwapExecutionStarted();\n }\n if ((step as any)?.data?.explorerURL) {\n mergeExplorerUrls({\n destinationExplorerUrl: (step as any).data.explorerURL,\n });\n }\n if ((step as any)?.completed !== false) {\n onStepComplete(step as any);\n }\n return;\n }\n if (event.name === \"SWAP_SKIPPED\") {\n const step =\n event.args && typeof event.args === \"object\"\n ? event.args\n : ({\n completed: true,\n data: event.args,\n type: \"SWAP_SKIPPED\",\n typeID: \"SWAP_SKIPPED\",\n } as unknown as SwapStepType);\n enterSkippedSwapProgress();\n appendProgressEvent(NEXUS_EVENTS.SWAP_STEP_COMPLETE, step, true);\n onStepComplete(step as SwapStepType);\n return;\n }\n if (event.name === NEXUS_EVENTS.SWAP_STEP_COMPLETE) {\n const step = event.args;\n const swapSkipped = isSwapSkippedStepType(getProgressStepType(step));\n if (swapSkipped) {\n enterSkippedSwapProgress();\n }\n appendProgressEvent(event.name, step, true);\n if (\n [\n \"SOURCE_SWAP_BATCH_TX\",\n \"SOURCE_SWAP_HASH\",\n \"BRIDGE_DEPOSIT\",\n \"RFF_ID\",\n \"DESTINATION_SWAP_BATCH_TX\",\n \"DESTINATION_SWAP_HASH\",\n \"SWAP_COMPLETE\",\n \"SWAP_SKIPPED\",\n ].includes(step?.type ?? \"\")\n ) {\n markSwapExecutionStarted();\n }\n if (step?.type === \"SOURCE_SWAP_HASH\" && step.explorerURL) {\n mergeExplorerUrls({ sourceExplorerUrl: step.explorerURL });\n }\n if (step?.type === \"DESTINATION_SWAP_HASH\" && step.explorerURL) {\n mergeExplorerUrls({ destinationExplorerUrl: step.explorerURL });\n }\n if (step?.type === \"BRIDGE_DEPOSIT\" && (step as any).data?.explorerURL) {\n mergeExplorerUrls({\n sourceExplorerUrl: (step as any).data.explorerURL,\n });\n }\n if (step?.type === \"RFF_ID\") {\n const nextIntentId = Number((step as any).data);\n if (Number.isFinite(nextIntentId) && nextIntentId > 0) {\n patchCurrentSwapHistoryEntry({ intentId: nextIntentId });\n }\n }\n if (step?.completed !== false) {\n onStepComplete(step);\n }\n }\n };\n\n try {\n if (!isExactOutFlow) {\n const fromPayload: {\n chainId: number;\n tokenAddress: `0x${string}`;\n amount: 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 = cleanAmount\n .toDecimalPlaces(Math.max(0, token.decimals || 18), Decimal.ROUND_DOWN)\n .toFixed();\n\n fromPayload.push({\n chainId: token.chainId!,\n tokenAddress: token.contractAddress as `0x${string}`,\n amount: nexusSDK.utils.parseUnits(\n safeTokenAmountStr,\n token.decimals || 18,\n ),\n });\n }\n\n if (fromPayload.length === 0) {\n throw new Error(\"No source amount available for swap.\");\n }\n\n resetExplorerUrls();\n // Start exact-in swap โ€” the intent hook will fire and populate preview\n const result = await nexusSDK.swapWithExactIn(\n {\n from: fromPayload,\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n },\n {\n onEvent: (event: any) => {\n if (swapRunIdRef.current !== runId) return;\n handleSwapEvent(event);\n },\n },\n );\n if (!result?.success) {\n throw new Error(result?.error || \"Swap failed\");\n }\n const intentExplorerUrl = result.result.explorerURL || null;\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ?? currentSwapEntry?.intentId;\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n finishCurrentSwapHistoryEntry(\"fulfilled\", {\n intentExplorerUrl,\n intentId,\n finalExplorerUrl:\n explorerUrlsRef.current.destinationExplorerUrl ||\n explorerUrlsRef.current.sourceExplorerUrl,\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 = nexusSDK.utils.parseUnits(\n exactOutAmountString,\n toToken.decimals || 18,\n );\n\n resetExplorerUrls();\n\n const fromSourcesPayload = buildFromSourcesPayload(\n getExactOutSourceTokens(),\n );\n\n const isNative =\n !toToken.contractAddress ||\n toToken.contractAddress.toLowerCase() ===\n \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\" ||\n toToken.contractAddress ===\n \"0x0000000000000000000000000000000000000000\";\n let executeConfig: any;\n if (activeMode === \"deposit\" && !selectedOpportunity?.execute) {\n throw new Error(\n \"Selected deposit opportunity is missing execute parameters.\",\n );\n }\n\n if (activeMode === \"deposit\" && selectedOpportunity?.execute) {\n executeConfig =\n typeof selectedOpportunity.execute === \"function\"\n ? selectedOpportunity.execute(\n amountBigInt,\n (ownerAddress ?? connectedAddress) as `0x${string}`,\n )\n : selectedOpportunity.execute;\n } else if (\n activeMode === \"send\" &&\n resolvedRecipientAddress\n ) {\n if (isNative) {\n executeConfig = {\n to: resolvedRecipientAddress as `0x${string}`,\n value: amountBigInt,\n gas: BigInt(100000),\n };\n } else {\n executeConfig = {\n to: toToken.contractAddress as `0x${string}`,\n data: encodeFunctionData({\n abi: erc20Abi,\n functionName: \"transfer\",\n args: [resolvedRecipientAddress as `0x${string}`, amountBigInt],\n }),\n gas: BigInt(100000),\n };\n }\n }\n\n if (executeConfig) {\n const onEvent = (event: any) => {\n if (swapRunIdRef.current !== runId) return;\n handleSwapEvent(event);\n };\n const sdkWithOptionalTransfer = nexusSDK as any;\n const result =\n activeMode === \"send\" &&\n typeof sdkWithOptionalTransfer.swapAndTransfer === \"function\"\n ? await sdkWithOptionalTransfer.swapAndTransfer(\n {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmount: amountBigInt,\n recipient: resolvedRecipientAddress as `0x${string}`,\n ...fromSourcesPayload,\n },\n { onEvent },\n )\n : await nexusSDK.swapAndExecute(\n {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmount: amountBigInt,\n execute: executeConfig,\n ...fromSourcesPayload,\n },\n { onEvent },\n );\n\n const swapResult = result?.swapResult ?? result?.result ?? null;\n const swapSkipped = Boolean((result as any)?.swapSkipped);\n if (!swapResult && !swapSkipped && activeMode !== \"send\") {\n throw new Error(\"Swap failed\");\n }\n const executeTxHash =\n result?.executeResponse?.txHash ||\n result?.transactionHash ||\n result?.txHash ||\n null;\n const intentExplorerUrl =\n swapResult?.explorerURL || result?.intentExplorerUrl || null;\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ?? currentSwapEntry?.intentId;\n const finalExplorerUrl =\n result?.explorerUrl ||\n result?.executeExplorerUrl ||\n getExplorerTxUrl(toToken.chainId, executeTxHash);\n if (finalExplorerUrl) {\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n patchCurrentSwapHistoryEntry({\n intentExplorerUrl,\n intentId,\n finalExplorerUrl,\n });\n } else {\n const result = await nexusSDK.swapWithExactOut(\n {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmount: amountBigInt,\n ...fromSourcesPayload,\n },\n {\n onEvent: (event: any) => {\n if (swapRunIdRef.current !== runId) return;\n handleSwapEvent(event);\n },\n },\n );\n if (!result?.success) {\n throw new Error(result?.error || \"Swap failed\");\n }\n const intentExplorerUrl = result.result.explorerURL || null;\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ?? currentSwapEntry?.intentId;\n patchCurrentSwapHistoryEntry({ intentExplorerUrl, intentId });\n }\n\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n finishCurrentSwapHistoryEntry(\"fulfilled\");\n resetInputsAfterSuccessfulExecution();\n onComplete?.();\n setSwapStep(\"success\");\n }\n }\n } catch (err: any) {\n console.error(\"Error in handleEnterPreview:\", err);\n if (swapRunIdRef.current !== runId) {\n return;\n }\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n const hasActiveExecution =\n swapStepRef.current === \"progress\" && Boolean(currentSwapIdRef.current);\n const showFailedProgressThenReceipt = (\n error: string,\n patch: Partial = {},\n ) => {\n const failedProgressEvent = progressEventsRef.current.at(-1);\n const fallbackFailedStep =\n activeMode === \"deposit\" || activeMode === \"send\"\n ? ({ type: \"APPROVAL\", typeID: \"AP\" } as BridgeStepType)\n : ({\n type: \"DETERMINING_SWAP\",\n typeID: \"DETERMINING_SWAP\",\n } as unknown as SwapStepType);\n const failedStep =\n failedProgressEvent?.step ?? fallbackFailedStep;\n const autoRefundAvailable =\n isAutoRefundAvailableProgressEvent(failedProgressEvent);\n setFailedProgressStep(failedStep);\n finishCurrentSwapHistoryEntry(\"failed\", {\n error,\n autoRefundAvailable,\n failureMessage: getFailureMessageForProgressStep(\n failedStep,\n 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 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 const errorMessage =\n err?.message ||\n (typeof err === \"string\"\n ? err\n : \"Transaction failed. Please try again or check console.\");\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 useEffect(() => {\n if (activeMode !== \"swap\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = 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 clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n const timer = window.setTimeout(() => {\n void handleEnterPreview({ background: true });\n }, EXACT_OUT_INPUT_DEBOUNCE_MS);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [activeMode, amount, fromTokens, nexusSDK, swapStep, toToken]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = 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 clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n const timer = window.setTimeout(() => {\n void handleEnterPreview({ background: true });\n }, EXACT_OUT_INPUT_DEBOUNCE_MS);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n amount,\n depositAmountMode,\n nexusSDK,\n sourceSelectionRevision,\n selectedOpportunity,\n swapStep,\n toToken,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"send\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = 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 clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n const timer = window.setTimeout(() => {\n void handleEnterPreview({ background: true });\n }, EXACT_OUT_INPUT_DEBOUNCE_MS);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [activeMode, amount, nexusSDK, sourceSelectionRevision, swapStep, toToken]);\n\n const refreshActiveSwapIntent = useCallback(async () => {\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 isPreviewRefresh = swapStepRef.current === \"preview-intent\";\n if (isPreviewRefresh) {\n setPreviewQuoteRefreshing(true);\n } else {\n setQuoteRefreshing(true);\n }\n try {\n const updated = await activeIntent.refresh();\n if (!updated || swapRunIdRef.current !== runId) return;\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 (swapRunIdRef.current === runId) {\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\" || activeMode === \"deposit\" || activeMode === \"send\") &&\n Boolean(intentData && swapIntentRef.current) &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n\n if (!hasRefreshableIntent) return;\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 (\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 intentData,\n intentLoading,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n refreshActiveSwapIntent,\n swapStep,\n ]);\n\n useEffect(() => {\n const hasRefreshableIntent =\n (activeMode === \"swap\" || activeMode === \"deposit\" || activeMode === \"send\") &&\n Boolean(intentData && swapIntentRef.current) &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n\n if (!hasRefreshableIntent) {\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 }, [activeMode, intentData, swapStep]);\n\n /** User accepted swap from the preview โ€” call allow() from the intent hook */\n const handleSwapAccept = () => {\n if (swapIntentRef.current) {\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 swapIntentRef.current.allow();\n // The swap promise in handleEnterPreview will resolve/reject\n }\n };\n\n // ---------------------------------------------------------------------------\n // Header title\n // ---------------------------------------------------------------------------\n const getTitle = () => {\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\") return \"Swap Failed\";\n return \"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\") return \"Deposit Failed\";\n return \"Deposit\";\n }\n if (activeMode === \"send\") {\n if (swapStep === \"progress\") return \"Sendingโ€ฆ\";\n if (swapStep === \"success\") return \"Send Complete\";\n if (swapStep === \"failed\") return \"Send Failed\";\n return \"Send\";\n }\n return \"Nexus One\";\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 !== \"idle\" &&\n swapStep !== \"choose-swap-asset\" &&\n swapStep !== \"choose-receive-asset\" &&\n swapStep !== \"enter-recipient\";\n const handleBack = () => {\n if (swapStep === \"history\") {\n setSwapStep(\"idle\");\n return;\n }\n if (swapStep === \"choose-swap-asset\") {\n closeDrawerToIdle();\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 = (\n val: string,\n panel: \"send\" | \"receive\",\n ) => {\n syncingIntentSourcesRef.current = false;\n setSwapQuoteIssue(null);\n setTxError(null);\n const nextAmount = parseFiatNumber(val);\n const hasSelectedSourceToken = fromTokens.some(\n (token) => token.chainId && token.contractAddress,\n );\n const shouldLoadQuote = Boolean(\n nexusSDK && nextAmount?.gt(0) && toToken && hasSelectedSourceToken,\n );\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\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 One 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 handleDepositAmountChange = (val: string) => {\n syncingIntentSourcesRef.current = false;\n setExactOutQuoteSourceModeValue(\"all\");\n maxPercentRunRef.current += 1;\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setSwapQuoteIssue(null);\n const nextAmount = parseFiatNumber(val);\n const shouldLoadQuote = Boolean(\n nexusSDK && nextAmount?.gt(0) && toToken && selectedOpportunity,\n );\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n } else {\n clearSelectedSources();\n }\n setAmount(val);\n };\n\n const handleSendAmountChange = (val: string) => {\n syncingIntentSourcesRef.current = false;\n setExactOutQuoteSourceModeValue(\"all\");\n maxPercentRunRef.current += 1;\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setSwapQuoteIssue(null);\n setSwapType(\"exactOut\");\n const nextAmount = parseFiatNumber(val);\n const shouldLoadQuote = Boolean(nexusSDK && nextAmount?.gt(0) && toToken);\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n } else {\n clearSelectedSources();\n }\n setAmount(val);\n };\n\n const handleDepositAmountModeToggle = () => {\n syncingIntentSourcesRef.current = false;\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.div(rate).toDecimalPlaces(toToken?.decimals ?? 18);\n setAmount(converted.toFixed());\n }\n clearPendingSwapIntent();\n setDepositAmountMode((current) => (current === \"token\" ? \"usd\" : \"token\"));\n };\n\n const handleDepositPercentSelect = async (pct: number) => {\n if (!toToken) return;\n\n syncingIntentSourcesRef.current = false;\n setTxError(null);\n setSwapQuoteIssue(null);\n const runId = ++maxPercentRunRef.current;\n\n if (pct !== 100) {\n const usdAmount = getTotalBalancePercentUsdAmount(pct);\n const shouldUseMaxQuoteFallback =\n depositAmountMode === \"usd\" && getDepositTokenUsdRate().lte(0);\n const nextAmount =\n depositAmountMode === \"usd\"\n ? usdAmount.toDecimalPlaces(2, Decimal.ROUND_DOWN).toFixed()\n : formatTokenAmountFromUsd(usdAmount, toToken);\n\n if (nextAmount && !shouldUseMaxQuoteFallback) {\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleDepositAmountChange(nextAmount);\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(true);\n setMaxCalculationPercent(pct);\n try {\n await waitForNextPaint();\n const fallback = await getPercentAmountFromMaxQuote(\n toToken,\n pct,\n depositAmountMode === \"usd\",\n );\n if (runId !== maxPercentRunRef.current) return;\n if (!fallback) {\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 setDepositAmountMode(fallback.mode);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleDepositAmountChange(fallback.amount);\n } catch (error: any) {\n if (runId !== maxPercentRunRef.current) return;\n console.error(\"Unable to calculate percentage deposit amount\", error);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n if (isInsufficientSourcesError(error)) {\n setSwapQuoteIssue(buildInsufficientSourcesIssue(error));\n return;\n }\n setTxError(\n error?.message || \"Unable to calculate this percentage for the deposit asset.\",\n );\n }\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(true);\n setMaxCalculationPercent(100);\n try {\n await waitForNextPaint();\n const maxAmount = await getPercentAmountFromMaxQuote(\n toToken,\n 100,\n depositAmountMode === \"usd\",\n );\n if (runId !== maxPercentRunRef.current) return;\n if (!maxAmount) {\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n setTxError(\"No depositable amount is available for this opportunity.\");\n return;\n }\n\n setDepositAmountMode(maxAmount.mode);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleDepositAmountChange(maxAmount.amount);\n } catch (error: any) {\n if (runId !== maxPercentRunRef.current) return;\n console.error(\"Unable to calculate max deposit amount\", error);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n if (isInsufficientSourcesError(error)) {\n setSwapQuoteIssue(buildInsufficientSourcesIssue(error));\n return;\n }\n setTxError(\n error?.message || \"Unable to calculate the max deposit amount.\",\n );\n }\n };\n\n const handleSendPercentSelect = async (pct: number) => {\n if (!toToken) return;\n\n syncingIntentSourcesRef.current = false;\n setTxError(null);\n setSwapQuoteIssue(null);\n const runId = ++maxPercentRunRef.current;\n\n if (pct !== 100) {\n const usdAmount = getTotalBalancePercentUsdAmount(pct);\n const nextAmount = formatTokenAmountFromUsd(usdAmount, toToken);\n\n if (nextAmount) {\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleSendAmountChange(nextAmount);\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(true);\n setMaxCalculationPercent(pct);\n try {\n await waitForNextPaint();\n const fallback = await getPercentAmountFromMaxQuote(toToken, pct, false);\n if (runId !== maxPercentRunRef.current) return;\n if (!fallback) {\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 setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleSendAmountChange(fallback.amount);\n } catch (error: any) {\n if (runId !== maxPercentRunRef.current) return;\n console.error(\"Unable to calculate percentage send amount\", error);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n if (isInsufficientSourcesError(error)) {\n setSwapQuoteIssue(buildInsufficientSourcesIssue(error));\n return;\n }\n setTxError(\n error?.message || \"Unable to calculate this percentage for the send asset.\",\n );\n }\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(true);\n setMaxCalculationPercent(100);\n try {\n await waitForNextPaint();\n const maxAmount = await getPercentAmountFromMaxQuote(toToken, 100, false);\n if (runId !== maxPercentRunRef.current) return;\n if (!maxAmount) {\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n setTxError(\"No transferable amount is available for this asset.\");\n return;\n }\n\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleSendAmountChange(maxAmount.amount);\n } catch (error: any) {\n if (runId !== maxPercentRunRef.current) return;\n console.error(\"Unable to calculate max send amount\", error);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n if (isInsufficientSourcesError(error)) {\n setSwapQuoteIssue(buildInsufficientSourcesIssue(error));\n return;\n }\n setTxError(error?.message || \"Unable to calculate the max send amount.\");\n }\n };\n\n // ---------------------------------------------------------------------------\n // Render\n // ---------------------------------------------------------------------------\n const exactOutInsufficientSourceIssue =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n swapQuoteIssue?.type === \"insufficientSources\"\n ? swapQuoteIssue\n : null;\n const isExactOutRouteLoading =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n swapStep === \"idle\" &&\n swapType === \"exactOut\" &&\n Boolean(toToken && (receiveMaxCalculating || (amount && Number(amount) > 0))) &&\n !exactOutInsufficientSourceIssue &&\n (quoteRefreshing || intentLoading || receiveMaxCalculating);\n const hasCurrentRunnableIntent =\n Boolean(intentData && swapIntentRef.current) &&\n swapIntentRef.current?.runId === swapRunIdRef.current &&\n !intentLoading;\n const hasIntentSources = Boolean((intentData?.sources ?? []).length > 0);\n const isQuoteUnavailableForAutoSourceFlow =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n Boolean(hasPositiveDecimalInput(amount) && toToken) &&\n !quoteRefreshing &&\n !receiveMaxCalculating &&\n !intentLoading &&\n !exactOutInsufficientSourceIssue &&\n (!hasCurrentRunnableIntent || !hasIntentSources);\n const hasPositiveRootAmount = hasPositiveDecimalInput(amount);\n const hasReadySwapQuoteInput = hasReadyExactInSwapInput(fromTokens, toToken);\n const needsWalletConnection = !ownerAddress || !nexusSDK;\n const walletConnectBusy =\n walletActionPending ||\n nexusLoading ||\n isWalletConnectPending ||\n walletStatus === \"connecting\";\n const walletCtaLabel = walletConnectBusy ? \"Connecting...\" : \"Connect Wallet\";\n const isSwapCtaDisabled =\n needsWalletConnection\n ? walletConnectBusy\n : !hasReadySwapQuoteInput ||\n receiveMaxCalculating ||\n quoteRefreshing ||\n Boolean(exactOutInsufficientSourceIssue);\n const isDepositCtaDisabled =\n needsWalletConnection\n ? walletConnectBusy\n : !hasPositiveRootAmount ||\n !toToken ||\n quoteRefreshing ||\n receiveMaxCalculating ||\n isQuoteUnavailableForAutoSourceFlow ||\n Boolean(exactOutInsufficientSourceIssue);\n const sendNeedsRecipient = activeMode === \"send\" && !recipientAddress;\n const isSendCtaDisabled =\n needsWalletConnection\n ? walletConnectBusy\n : !hasPositiveRootAmount ||\n !toToken ||\n hasSameOwnerSendRecipient ||\n receiveMaxCalculating ||\n (!sendNeedsRecipient &&\n (quoteRefreshing || isQuoteUnavailableForAutoSourceFlow)) ||\n Boolean(exactOutInsufficientSourceIssue);\n const quoteCtaLabel = (fallback: string) => {\n if (needsWalletConnection) return walletCtaLabel;\n if (exactOutInsufficientSourceIssue) return \"Insufficient balance\";\n if (receiveMaxCalculating) return \"Calculating...\";\n if (quoteRefreshing) return \"Intent fetching...\";\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 (exactOutInsufficientSourceIssue) return \"Insufficient balance\";\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) => sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0),\n );\n const previewSourceUsdNumber =\n 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 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 resolvedToToken =\n toToken ??\n (activeMode === \"deposit\" && selectedOpportunity\n ? toTokenFromOpportunity(selectedOpportunity)\n : undefined);\n const toTokenWithFetchedBalance =\n resolvedToToken && destinationBalance\n ? { ...resolvedToToken, balance: destinationBalance }\n : resolvedToToken;\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: parseFiatNumber(intentData?.destination?.amount),\n producedUsd: parseFiatNumber(intentData?.destination?.value),\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 = shouldShowPredictiveExactOutDisplay\n ? predictiveExactOutQuote?.sources ?? fromTokens\n : fromTokens;\n const displayFromTokens = (() => {\n if (\n !destinationBalanceDisplayToken ||\n (activeMode !== \"deposit\" && activeMode !== \"send\")\n ) {\n return baseDisplayFromTokens;\n }\n\n const destinationKey = getTokenSelectionKey(destinationBalanceDisplayToken);\n let replacedEmptyDestinationToken = false;\n const tokens = baseDisplayFromTokens.map((token) => {\n const isDestinationToken =\n 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 return replacedEmptyDestinationToken\n ? tokens\n : [...tokens, destinationBalanceDisplayToken];\n })();\n const displayExactOutRouteLoading =\n isExactOutRouteLoading && !shouldShowPredictiveExactOutDisplay;\n const totalSwapBalanceUsd = getSwapBalanceTotalUsd()\n .toDecimalPlaces(2)\n .toFixed();\n const sendAmountUsd =\n amount && toToken\n ? getTokenUsdValue(\n {\n ...toToken,\n userAmount: amount,\n userAmountMode: \"token\",\n },\n amount,\n ).toNumber()\n : 0;\n const isIdleSwapQuoteLoading =\n activeMode === \"swap\" && swapStep === \"idle\" && quoteRefreshing;\n const isReceiveAmountLoading =\n receiveMaxCalculating ||\n (isIdleSwapQuoteLoading && swapType === \"exactIn\" && !idleReceiveQuoteAmount);\n const isReceiveUsdLoading =\n receiveMaxCalculating ||\n (isIdleSwapQuoteLoading && swapType === \"exactIn\" && !idleReceiveQuoteUsd);\n const hasQuoteRefreshCountdown =\n (activeMode === \"swap\" || activeMode === \"deposit\" || activeMode === \"send\") &&\n Boolean(intentData && swapIntentRef.current) &&\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 isDrawerOverlayActive =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n swapStep === \"enter-recipient\" ||\n closingDrawerStep !== null;\n\n return (\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 {/* Protocol chip appended next to Title when Deposit Protocol selected */}\n {isTitleCentered() &&\n activeMode === \"deposit\" &&\n swapStep === \"idle\" &&\n selectedOpportunity && (\n
\n {\n clearPendingSwapIntent();\n setSelectedOpportunity(undefined);\n setToToken(undefined);\n clearSelectedSources();\n setAmount(\"\");\n setDepositAmountMode(\"token\");\n }}\n className=\"flex items-center gap-1 pl-2 pr-1.5 py-1 rounded-[4px] hover:bg-black/5 transition-colors\"\n style={{\n fontFamily: \"var(--font-geist-mono), sans-serif\",\n fontSize: \"10px\",\n fontWeight: 500,\n color: \"var(--foreground-muted, #848483)\",\n background: \"var(--background-tertiary, #F0F0EF)\",\n border: \"none\",\n cursor: \"pointer\",\n }}\n >\n {selectedOpportunity.title || selectedOpportunity.protocol}\n \n \n
\n )}\n \n\n {/* Right side icons */}\n \n {hasQuoteRefreshCountdown && (\n \n )}\n setSwapStep(\"history\")}\n style={{\n alignItems: \"center\",\n backgroundColor: \"#FFFFFE\",\n borderRadius: \"8px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n height: \"32px\",\n justifyContent: \"center\",\n outline: \"1px solid #E8E8E7\",\n width: \"32px\",\n cursor: \"pointer\",\n border: \"none\",\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 />\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 fromTokens={fromTokens}\n toToken={toTokenWithFetchedBalance}\n receiveQuoteUsd={idleReceiveQuoteUsd}\n sourceRouteStatus={\n exactOutInsufficientSourceIssue\n ? \"insufficient\"\n : isExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n sourceRouteMessage={exactOutInsufficientSourceIssue?.message}\n totalBalance={totalSwapBalanceUsd}\n usdValue={amount && usdValue > 0 ? usdValue.toFixed(2) : \"\"}\n swapType={swapType}\n onOpenSourcePicker={(index) => {\n setEditingAssetIndex(index ?? null);\n openDrawerStep(\"choose-swap-asset\");\n }}\n onOpenDestPicker={() => openDrawerStep(\"choose-receive-asset\")}\n onOpenRecipientPicker={handleOpenRecipientEditor}\n recipientAddress={effectiveRecipientAddress}\n defaultRecipientAddress={defaultRecipientAddress}\n onUpdateTokens={setFromTokens}\n />\n\n {txError && !exactOutInsufficientSourceIssue && (\n \n )}\n\n {/* CTA Button */}\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n void handleEnterPreview();\n }}\n disabled={isSwapCtaDisabled}\n style={{\n alignItems: \"center\",\n backgroundColor: exactOutInsufficientSourceIssue\n ? \"#FCEEED\"\n : isSwapCtaDisabled\n\t ? \"#F0F0EF\"\n\t : \"#006BF4\",\n\t border: exactOutInsufficientSourceIssue\n\t ? \"1px solid #F7C4C1\"\n\t : \"none\",\n\t borderRadius: exactOutInsufficientSourceIssue ? \"4px\" : \"8px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"8px\",\n height: \"48px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n\t cursor: isSwapCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {exactOutInsufficientSourceIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n quoteRefreshing ||\n receiveMaxCalculating ? (\n \n ) : null}\n \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 {/* Opportunity list */}\n {config.opportunities &&\n config.opportunities.length > 0 &&\n !selectedOpportunity && (\n <>\n \n\n {/* Done button for opportunity selection */}\n \n {\n const opportunity =\n pendingOpportunity ?? config.opportunities?.[0];\n if (opportunity) {\n handleSelectDepositOpportunity(opportunity);\n setSwapStep(\"idle\");\n }\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: \"#006BF4\",\n borderRadius: \"8px\",\n boxShadow: \"#5555550D 0px 1px 4px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flex: 1,\n height: \"48px\",\n justifyContent: \"center\",\n border: \"none\",\n cursor: \"pointer\",\n }}\n >\n \n Done\n \n \n \n \n )}\n\n {/* After opportunity selected โ€” show deposit form */}\n {(!config.opportunities ||\n config.opportunities.length === 0 ||\n selectedOpportunity) && (\n <>\n openDrawerStep(\"choose-swap-asset\")}\n onSetPercent={handleDepositPercentSelect}\n routeStatus={\n exactOutInsufficientSourceIssue\n ? \"insufficient\"\n : displayExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n routeMessage={exactOutInsufficientSourceIssue?.message}\n isCalculatingMax={receiveMaxCalculating}\n calculatingPercent={maxCalculationPercent}\n isQuoteRefreshing={quoteRefreshing || intentLoading}\n showAutoBadge={!sourceSelectionTouched}\n />\n\n {txError && !exactOutInsufficientSourceIssue && (\n \n )}\n\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n void handleEnterPreview();\n }}\n disabled={isDepositCtaDisabled}\n style={{\n alignItems: \"center\",\n backgroundColor: exactOutInsufficientSourceIssue\n ? \"#FCEEED\"\n : isDepositCtaDisabled\n\t ? \"#F0F0EF\"\n\t : \"#006BF4\",\n\t border: exactOutInsufficientSourceIssue\n\t ? \"1px solid #F7C4C1\"\n\t : \"none\",\n\t borderRadius: exactOutInsufficientSourceIssue ? \"4px\" : \"8px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"8px\",\n height: \"48px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n\t cursor: isDepositCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {exactOutInsufficientSourceIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n quoteRefreshing ||\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 0 ? sendAmountUsd.toFixed(2) : \"\"\n }\n onOpenAssetPicker={() => openDrawerStep(\"choose-receive-asset\")}\n onOpenSourcePicker={() => {\n setEditingAssetIndex(null);\n openDrawerStep(\"choose-swap-asset\");\n }}\n onOpenRecipientPicker={handleOpenRecipientEditor}\n recipientAddress={recipientAddress || \"\"}\n onSetPercent={handleSendPercentSelect}\n routeStatus={\n exactOutInsufficientSourceIssue\n ? \"insufficient\"\n : displayExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n routeMessage={exactOutInsufficientSourceIssue?.message}\n isCalculatingMax={receiveMaxCalculating}\n calculatingPercent={maxCalculationPercent}\n isQuoteRefreshing={quoteRefreshing}\n showAutoBadge={!sourceSelectionTouched}\n />\n\n {txError && !exactOutInsufficientSourceIssue && (\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 disabled={isSendCtaDisabled}\n style={{\n alignItems: \"center\",\n backgroundColor: exactOutInsufficientSourceIssue\n ? \"#FCEEED\"\n : isSendCtaDisabled\n\t ? \"#F0F0EF\"\n\t : \"#006BF4\",\n\t border: exactOutInsufficientSourceIssue\n\t ? \"1px solid #F7C4C1\"\n\t : \"none\",\n\t borderRadius: exactOutInsufficientSourceIssue ? \"4px\" : \"8px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"8px\",\n height: \"48px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n\t cursor: isSendCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {exactOutInsufficientSourceIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n (!sendNeedsRecipient &&\n (quoteRefreshing || 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 />\n \n \n \n \n \n {\n setTxError(null);\n closeDrawerToIdle();\n }}\n aria-label=\"Back\"\n style={{\n alignItems: \"center\",\n backgroundColor: \"#FFFFFE\",\n border: \"1px solid #E8E8E7\",\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 >\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 label={null}\n placeholder=\"Wallet address\"\n hasError={Boolean(txError)}\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 0\n ? sendAmountUsd.toFixed(2)\n : undefined\n }\n selectedTokens={fromTokens}\n editingAssetIndex={editingAssetIndex}\n onSelectionChange={\n activeMode === \"deposit\" || activeMode === \"send\"\n ? (tokens) => {\n setSourceSelectionTouched(true);\n setExactOutQuoteSourceModeValue(\"selected\");\n invalidateExactOutQuoteForRefresh();\n setSourceSelectionRevision((current) => current + 1);\n setFromTokens(\n tokens.map((token) => ({\n ...token,\n userAmount: \"\",\n })),\n );\n }\n : undefined\n }\n onClearSelection={\n activeMode === \"deposit\" || activeMode === \"send\"\n ? () => {\n setSourceSelectionTouched(true);\n setExactOutQuoteSourceModeValue(\"selected\");\n invalidateExactOutQuoteForRefresh();\n setSourceSelectionRevision((current) => current + 1);\n setFromTokens((current) =>\n current.length === 0 ? current : [],\n );\n }\n : undefined\n }\n onToggle={(token) => {\n if (activeMode === \"deposit\" || activeMode === \"send\") {\n setSourceSelectionTouched(true);\n setExactOutQuoteSourceModeValue(\"selected\");\n invalidateExactOutQuoteForRefresh();\n setSourceSelectionRevision((current) => current + 1);\n } else {\n clearPendingSwapIntent();\n }\n setFromTokens((prev) => {\n const isSameSelection = (a: SwapTokenOption, b: SwapTokenOption) => {\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 isDepositOrSendSourcePicker =\n activeMode === \"deposit\" || activeMode === \"send\";\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\" && prev.length === 0\n ? amount\n : \"\",\n });\n\n if (\n isDepositOrSendSourcePicker &&\n token.isUnified &&\n sourceTokens.length > 0\n ) {\n const hasUnifiedSelection = prev.some(isSameUnifiedGroup);\n const areAllChildrenSelected = sourceTokens.every((source) =>\n prev.some((item) => isSameSelection(item, source)),\n );\n const withoutGroup = prev.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) => withDefaultAmount(source)),\n ];\n }\n\n if (isDepositOrSendSourcePicker && !token.isUnified) {\n const unifiedSelection = prev.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 = prev.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 = prev.find((item) =>\n isSameSelection(item, token),\n );\n if (exists) {\n return prev.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 = prev.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 [\n ...next,\n withDefaultAmount(token),\n ];\n });\n }}\n onDone={closeDrawerToIdle}\n onSelect={(token) => {\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 setSourceSelectionTouched(true);\n setExactOutQuoteSourceModeValue(\"selected\");\n invalidateExactOutQuoteForRefresh();\n setSourceSelectionRevision((current) => current + 1);\n setFromTokens([{ ...token, userAmount: amount }]);\n closeDrawerToIdle();\n }\n }}\n onBack={closeDrawerToIdle}\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 (activeMode === \"send\" || activeMode === \"deposit\") {\n setExactOutQuoteSourceModeValue(\"all\");\n if (tokenChanged) {\n clearPendingSwapIntent();\n setAmount(\"\");\n }\n setSwapType(\"exactOut\");\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 onBack={closeDrawerToIdle}\n />\n \n \n )}\n\n \n );\n}\n\nexport default NexusOne;\n", + "content": "\"use client\";\n\nimport React, {\n useState,\n useRef,\n useEffect,\n useCallback,\n useLayoutEffect,\n useMemo,\n} from \"react\";\nimport {\n type NexusOneProps,\n type NexusOneMode,\n type SwapType,\n type DepositOpportunity,\n} from \"./types\";\nimport { SwapIdleForm } from \"./components/swap-idle-form\";\nimport { SendIdleForm } from \"./components/send-idle-form\";\nimport { DepositIdleForm } from \"./components/deposit-idle-form\";\nimport { RecipientInput } from \"./components/recipient-input\";\nimport { StatusAlert } from \"./components/status-alerts\";\nimport {\n SwapAssetSelector,\n type SwapTokenOption,\n deriveTokenOptions,\n} from \"./components/swap-asset-selector\";\nimport {\n SwapIntentPreview,\n type SwapIntentData,\n} from \"./components/swap-intent-preview\";\nimport {\n NexusOneProgressScreen,\n type NexusOneProgressEvent,\n} from \"./components/nexus-one-progress-screen\";\nimport { ReceiveAssetSelector, preloadReceiveTokens } from \"./components/receive-asset-selector\";\nimport { OpportunityList } from \"./components/opportunity-list\";\nimport { AlertCircle, ArrowLeft, ChevronDown, Loader2 } from \"lucide-react\";\nimport { useNexus } from \"../nexus/NexusProvider\";\nimport { useTransactionSteps } from \"../common/tx/useTransactionSteps\";\nimport { findCitreaReceiveToken } from \"./utils/citrea-tokens\";\nimport {\n CHAIN_METADATA,\n ERROR_CODES,\n NEXUS_EVENTS,\n type BridgeStepType,\n type EthereumProvider,\n type SwapStepType,\n TOKEN_CONTRACT_ADDRESSES,\n TOKEN_METADATA,\n} from \"@avail-project/nexus-core\";\nimport {\n useAccount,\n useConnect,\n useConnectorClient,\n useWalletClient,\n usePublicClient,\n} from \"wagmi\";\nimport {\n erc20Abi,\n isAddress,\n zeroAddress,\n createPublicClient,\n http,\n encodeFunctionData,\n} from \"viem\";\nimport { normalize } from \"viem/ens\";\nimport { mainnet } from \"viem/chains\";\nimport Decimal from \"decimal.js\";\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 SwapHistoryStatus =\n | \"pending\"\n | \"fulfilled\"\n | \"failed\"\n | \"refund-initiated\";\n\ninterface SwapHistoryEntry {\n id: string;\n mode: NexusOneMode;\n status: SwapHistoryStatus;\n createdAt: number;\n startedAt: number;\n endedAt?: number;\n durationSeconds?: number;\n intentData: SwapIntentData | null;\n fromTokens: SwapTokenOption[];\n toToken?: SwapTokenOption;\n requestedToAmount?: string;\n requestedToValue?: string;\n recipientAddress?: string;\n opportunity?: DepositOpportunity;\n feeUsd?: string;\n intentId?: number;\n intentExplorerUrl?: string | null;\n sourceExplorerUrl?: string | null;\n finalExplorerUrl?: string | null;\n error?: string;\n failureMessage?: string;\n failedStepType?: string;\n autoRefundAvailable?: boolean;\n}\n\ntype SwapQuoteIssue = {\n type: \"insufficientSources\";\n message: string;\n missingUsd?: string;\n};\n\ntype CachedMaxSwapQuote = {\n decimals: number;\n maxTokenAmount: Decimal;\n maxUsdAmount?: Decimal;\n symbol: string;\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 PredictiveQuoteBaseline = {\n destinationUsdRate: string;\n exactInDestinationAmountPerSourceUsd?: string;\n exactOutSourceUsdPerDestinationUsd?: string;\n updatedAt: number;\n};\n\nconst QUOTE_REFRESH_INTERVAL_MS = 30000;\nconst EXACT_OUT_INPUT_DEBOUNCE_MS = 1000;\nconst DRAWER_CLOSE_MS = 220;\nconst MODAL_HEIGHT_TRANSITION_MS = 260;\nconst BASIS_POINTS = 10000;\nconst PREDICTIVE_EXACT_IN_DISCOUNT_BPS = 50;\nconst PREDICTIVE_EXACT_OUT_BUFFER_BPS = 100;\nconst PREDICTIVE_QUOTE_DISPLAY_DECIMALS = 8;\nconst SWAP_HISTORY_STORAGE_KEY_PREFIX = \"nexus-one-transaction-history-v1\";\nconst waitForNextPaint = () =>\n new Promise((resolve) => {\n if (typeof window === \"undefined\" || !window.requestAnimationFrame) {\n resolve();\n return;\n }\n window.requestAnimationFrame(() => {\n window.setTimeout(() => resolve(), 0);\n });\n });\nconst tooltipSurface = \"#FFFFFE\";\nconst tooltipText = \"var(--foreground-primary, #161615)\";\nconst tooltipBorder = \"var(--border-default, #E8E8E7)\";\nconst uiFont = '\"Geist\", var(--font-geist-sans), system-ui, sans-serif';\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`;\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 isSameTokenSelection = (\n a?: SwapTokenOption | null,\n b?: SwapTokenOption | null,\n) => Boolean(a && b && getTokenSelectionKey(a) === getTokenSelectionKey(b));\n\nconst sanitizeOpportunityForHistory = (\n opportunity?: DepositOpportunity,\n): DepositOpportunity | undefined => {\n if (!opportunity) return undefined;\n return {\n id: opportunity.id,\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 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 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 === \"refund-initiated\";\n\nconst isStoredMode = (value: unknown): value is NexusOneMode =>\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 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 = sortSwapHistoryEntries(entries).map(\n sanitizeHistoryEntry,\n );\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(true)}\n onMouseLeave={() => setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onBlur={() => setShowTooltip(false)}\n tabIndex={0}\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 >\n {showTooltip && (\n \n {tooltipLabel}\n \n )}\n \n \n \n \n \n );\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 = 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 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 const max = amount.abs().gte(1) ? 6 : 8;\n return formatDecimalDisplay(amount, { max });\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 hasValidIntentExplorer = (entry: Pick) =>\n Boolean(\n entry.intentExplorerUrl &&\n entry.intentId !== undefined &&\n Number.isFinite(entry.intentId) &&\n entry.intentId > 0,\n );\n\nconst getExplorerTxUrl = (chainId?: number, txHash?: string | null) => {\n if (!chainId || !txHash) return null;\n const chainMeta = CHAIN_METADATA[chainId];\n const baseUrl =\n (chainMeta as any)?.blockExplorerUrls?.[0] ||\n (chainMeta as any)?.blockExplorers?.default?.url;\n return baseUrl ? `${String(baseUrl).replace(/\\/$/, \"\")}/tx/${txHash}` : null;\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 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 size = 34,\n}: {\n tokenLogo?: string;\n chainLogo?: string;\n tokenSymbol?: string;\n chainName?: string;\n size?: number;\n}) {\n return (\n
\n \n {chainLogo && (\n \n )}\n
\n );\n}\n\nfunction TruncatedAddress({\n address,\n color = \"#006BF4\",\n}: {\n address: string;\n color?: string;\n}) {\n const [showTooltip, setShowTooltip] = useState(false);\n const label =\n address.length > 12 ? `${address.slice(0, 6)}...${address.slice(-4)}` : address;\n\n return (\n setShowTooltip(false)}\n onFocus={() => setShowTooltip(true)}\n onMouseEnter={() => setShowTooltip(true)}\n onMouseLeave={() => setShowTooltip(false)}\n tabIndex={0}\n style={{\n color,\n display: \"inline-flex\",\n fontFamily: uiFont,\n fontSize: \"13px\",\n fontWeight: 500,\n lineHeight: \"18px\",\n outline: \"none\",\n position: \"relative\",\n }}\n >\n {label}\n {showTooltip && (\n \n {address}\n \n )}\n \n );\n}\n\nconst getDisplayDestinationSourceRow = (entry: SwapHistoryEntry) => {\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(entry.intentData?.destination.amount);\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(entry.intentData?.destination.value);\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 tokenLogo: entry.toToken.logo,\n chainLogo: entry.toToken.chainLogo,\n symbol: entry.toToken.symbol,\n chainName: entry.toToken.chainName || \"\",\n amount: displayAmount\n .toDecimalPlaces(Math.max(0, entry.toToken.decimals ?? 18), Decimal.ROUND_DOWN)\n .toFixed(),\n value: rate ? displayAmount.mul(rate).toFixed() : entry.toToken.balanceInFiat,\n };\n};\n\nconst getProgressStepType = (\n step?: SwapStepType | BridgeStepType | null,\n) => String((step as any)?.type ?? (step as any)?.typeID ?? \"\").toUpperCase();\n\nconst isBridgeRefundStepType = (type: string) =>\n type.includes(\"RFF_ID\") || type.includes(\"BRIDGE_DEPOSIT\");\n\nconst isSwapSkippedStepType = (type: string) =>\n type.includes(\"SWAP_SKIPPED\");\n\nconst isAutoRefundAvailableProgressEvent = (\n event?: NexusOneProgressEvent,\n) =>\n event?.name === NEXUS_EVENTS.SWAP_STEP_COMPLETE &&\n isBridgeRefundStepType(getProgressStepType(event.step));\n\nconst getFailureMessageForProgressStep = (\n step: SwapStepType | BridgeStepType | null | undefined,\n mode: NexusOneMode,\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(\"SOURCE_SWAP\") ||\n type.includes(\"COLLECTION\")\n ) {\n return \"Collection Failed\";\n }\n if (\n type.includes(\"DESTINATION_SWAP\") ||\n type.includes(\"FULFIL\")\n ) {\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(\"RFF\") ||\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 getSourceRows = (entry: SwapHistoryEntry) => {\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 tokenLogo: fallback?.logo,\n chainLogo: source.chain.logo || fallback?.chainLogo,\n symbol: source.token.symbol,\n chainName: source.chain.name,\n amount: source.amount,\n value: source.value,\n };\n });\n\n return displayDestinationSourceRow\n ? [displayDestinationSourceRow, ...sourceRows]\n : sourceRows;\n }\n\n const fallbackRows = entry.fromTokens.map((token, index) => ({\n key: `${token.chainId}-${token.contractAddress}-${index}`,\n tokenLogo: token.logo,\n chainLogo: token.chainLogo,\n symbol: token.symbol,\n chainName: token.chainName || \"\",\n amount: token.userAmount || \"0\",\n value: token.balanceInFiat,\n }));\n\n return displayDestinationSourceRow\n ? [displayDestinationSourceRow, ...fallbackRows]\n : fallbackRows;\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 scrollRef.current?.scrollBy({ top: 72, behavior: \"smooth\" })}\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 >\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 isDeposit = entry.mode === \"deposit\";\n const isSend = entry.mode === \"send\";\n const tokenSymbol = destination?.token.symbol || entry.toToken?.symbol || \"\";\n const chainName = destination?.chain.name || entry.toToken?.chainName || \"\";\n const depositVenue =\n entry.opportunity?.title || entry.opportunity?.protocol || chainName;\n const amount = destination?.amount || \"\";\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 = `Intent #${entry.intentId}`;\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 storedFailureMessage =\n !entry.autoRefundAvailable && entry.failureMessage?.includes(\"Refund\")\n ? undefined\n : entry.failureMessage;\n const failureHeadline =\n storedFailureMessage ||\n (isDeposit\n ? \"Deposit failed. Funds are in your wallet\"\n : isSend\n ? \"Send failed. Funds are in your wallet\"\n : defaultSwapFailureHeadline);\n const receiptLocation = isDeposit ? depositVenue : chainName;\n const receiptSummary = receiptLocation ? `on ${receiptLocation}` : \"\";\n\n return (\n
\n \n \n \n \n {isFailed ? \"x\" : \"โœ“\"}\n
\n \n
\n {isFailed\n ? failureHeadline\n : isDeposit\n ? \"You deposited\"\n : isSend\n ? \"You sent\"\n : \"You received\"}\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: \"#006BF4\",\n cursor: \"pointer\",\n display: \"inline-flex\",\n fontFamily: uiFont,\n fontSize: \"12px\",\n gap: \"4px\",\n padding: 0,\n }}\n >\n {showSourceDetails ? \"Hide Details\" : `${sourceCount} asset${sourceCount === 1 ? \"\" : \"s\"}`}\n \n \n \n \n \n
\n \n
\n \n {isSend && 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({\n status,\n}: {\n status: SwapHistoryStatus;\n}) {\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 === \"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 onRefund,\n}: {\n entries: SwapHistoryEntry[];\n now: number;\n onRefund: (entry: SwapHistoryEntry) => void;\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 swap,\n 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 = entry.toToken?.logo;\n const destinationChainLogo =\n destination?.chain.logo || entry.toToken?.chainLogo || \"\";\n const destinationChainName =\n destination?.chain.name || entry.toToken?.chainName || \"\";\n const destinationSymbol = 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 showIntentExplorer = hasValidIntentExplorer(entry);\n const viewUrl = showIntentExplorer\n ? entry.intentExplorerUrl\n : entry.finalExplorerUrl;\n const canShowRefund =\n entry.status === \"failed\" &&\n Boolean(entry.autoRefundAvailable);\n const status = canShowRefund ? \"refund-initiated\" : entry.status;\n const sourceRows = getSourceRows(entry);\n const firstSource = sourceRows[0];\n\n return (\n \n
\n
\n \n
\n
\n {destinationAmount ? formatTokenDisplay(destinationAmount) : \"--\"}\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 onRefund(entry)}\n style={{\n background: \"#006BF4\",\n border: \"none\",\n borderRadius: \"8px\",\n color: \"#FFFFFE\",\n cursor: entry.intentId ? \"pointer\" : \"not-allowed\",\n fontFamily: uiFont,\n fontSize: \"13px\",\n fontWeight: 600,\n opacity: entry.intentId ? 1 : 0.5,\n padding: \"8px 14px\",\n }}\n >\n Refund\n \n \n )}\n\n \n
\n {firstSource && (\n \n )}\n \n โ†’\n \n \n {showIntentExplorer ? (\n \n Intent #{entry.intentId}\n \n ) : entry.finalExplorerUrl ? (\n \n Final transaction\n \n ) : null}\n
\n {viewUrl && (\n \n View โ†—\n \n )}\n \n \n );\n })}\n \n );\n}\n\n// ---------------------------------------------------------------------------\n// NexusOne\n// ---------------------------------------------------------------------------\n\nexport function NexusOne({\n config,\n embed = true,\n connectedAddress,\n onComplete,\n onStart,\n onError,\n onClose,\n}: NexusOneProps) {\n const {\n nexusSDK,\n bridgableBalance,\n swapBalance,\n getFiatValue,\n resolveTokenUsdRate,\n swapSupportedChainsAndTokens,\n supportedChainsAndTokens,\n fetchSwapBalance,\n handleInit,\n loading: nexusLoading,\n } = useNexus();\n\n // Mode is a single value, not an array\n const activeMode = config.mode;\n if (\n activeMode === \"deposit\" &&\n (!config.opportunities || config.opportunities.length === 0)\n ) {\n throw new Error(\n \"NexusOne deposit mode requires config.opportunities with at least one opportunity.\",\n );\n }\n const showCloseButton = !embed && Boolean(onClose);\n\n // Preload receive tokens once SDK is available\n useEffect(() => {\n if (nexusSDK) {\n preloadReceiveTokens();\n }\n }, [nexusSDK]);\n\n const { connector, status: walletStatus } = 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 : walletClientAddress &&\n isAddress(walletClientAddress) &&\n walletClientAddress.toLowerCase() !== zeroAddress\n ? walletClientAddress\n : undefined;\n const historyStorageKey = getSwapHistoryStorageKey(ownerAddress);\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 previousDefaultRecipientRef = useRef(defaultRecipientAddress);\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 [closingDrawerStep, setClosingDrawerStep] =\n useState(null);\n const rootContentRef = useRef(null);\n const [rootContentHeight, setRootContentHeight] = useState(\n null,\n );\n const [hasMeasuredRootContent, setHasMeasuredRootContent] = useState(false);\n const [fromTokens, setFromTokens] = useState([]);\n const [sourceSelectionTouched, setSourceSelectionTouched] = useState(false);\n const [sourceSelectionRevision, setSourceSelectionRevision] = useState(0);\n const [, setExactOutQuoteSourceMode] = useState<\"all\" | \"selected\">(\"all\");\n const exactOutQuoteSourceModeRef = useRef<\"all\" | \"selected\">(\"all\");\n const [toToken, setToToken] = useState(\n undefined,\n );\n const appliedTokenPrefillRef = useRef(null);\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 }, [activeMode, fetchSwapBalance, nexusSDK, swapStep]);\n\n useEffect(() => {\n setSourceSelectionTouched(false);\n setExactOutQuoteSourceModeValue(\"all\");\n }, [activeMode, setExactOutQuoteSourceModeValue]);\n\n useEffect(() => {\n const previousDefault = previousDefaultRecipientRef.current;\n previousDefaultRecipientRef.current = defaultRecipientAddress;\n\n if (activeMode !== \"swap\" || !defaultRecipientAddress) return;\n\n setRecipientAddress((current) => {\n if (\n !current ||\n (previousDefault &&\n current.toLowerCase() === previousDefault.toLowerCase())\n ) {\n return defaultRecipientAddress;\n }\n return current;\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 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 maxSwapQuoteCacheRef = useRef>({});\n const intentDestinationUsdRateCacheRef = useRef<\n Record\n >({});\n const intentSymbolUsdRateCacheRef = useRef>(\n {},\n );\n const predictiveQuoteCacheRef = useRef>(\n {},\n );\n const predictiveQuoteRunRef = useRef(0);\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 [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\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 } | null>(null);\n\n useEffect(() => {\n swapStepRef.current = swapStep;\n }, [swapStep]);\n\n useEffect(() => {\n return () => {\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n }\n };\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 setSwapStep(\"idle\");\n return;\n }\n\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n }\n\n setClosingDrawerStep(swapStep);\n drawerCloseTimerRef.current = setTimeout(() => {\n setSwapStep(\"idle\");\n setClosingDrawerStep(null);\n drawerCloseTimerRef.current = null;\n }, DRAWER_CLOSE_MS);\n }, [swapStep]);\n\n const openDrawerStep = useCallback((nextStep: SwapStep) => {\n if (drawerCloseTimerRef.current) {\n clearTimeout(drawerCloseTimerRef.current);\n drawerCloseTimerRef.current = null;\n }\n setClosingDrawerStep(null);\n setSwapStep(nextStep);\n }, []);\n\n const syncRootContentHeight = useCallback(() => {\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 setRootContentHeight((previousHeight) =>\n previousHeight === nextHeight ? previousHeight : nextHeight,\n );\n setHasMeasuredRootContent(true);\n }, []);\n\n useLayoutEffect(() => {\n syncRootContentHeight();\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(syncRootContentHeight);\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 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?.icon)\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 = matchedAsset?.icon ?? (isNativeSource ? chainMeta?.logo : \"\");\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: 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: chainMeta?.name ?? source.chain.name,\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 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 setExactOutQuoteSourceModeValue(\"all\");\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 = 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\n const minimumSourceUsd = new Decimal(1);\n const hasMinimumSourceUsdBalance = (\n token: Pick,\n ) => (parseFiatNumber(token.balanceInFiat) ?? new Decimal(0)).gte(minimumSourceUsd);\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 = (intent?: SwapIntentData | null) => {\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 (!amount || !value || amount.lte(0) || value.lte(0) || !chainId || !symbol) {\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(\n (breakdownSum, item) => {\n const value = parseFiatNumber(item.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd)\n ? breakdownSum.plus(value)\n : breakdownSum;\n },\n new Decimal(0),\n ),\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 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 const getTotalBalancePercentUsdAmount = (pct: number) =>\n getSwapBalanceTotalUsd().mul(pct).div(100);\n const formatTokenAmountFromUsd = (\n usdAmount: Decimal,\n token: Pick,\n ) => {\n const rate = getUsdRateForSymbol(token.symbol);\n if (rate.lte(0)) return undefined;\n return usdAmount\n .div(rate)\n .toDecimalPlaces(Math.max(0, token.decimals ?? 18), Decimal.ROUND_DOWN)\n .toFixed();\n };\n\n const getMaxSwapQuoteCacheKey = (token?: SwapTokenOption) => {\n if (!token?.chainId) return \"\";\n return [\n token.chainId,\n (token.contractAddress || zeroAddress).toLowerCase(),\n token.symbol.toUpperCase(),\n ].join(\":\");\n };\n\n const getCachedMaxSwapQuote = (token?: SwapTokenOption) => {\n const key = getMaxSwapQuoteCacheKey(token);\n return key ? maxSwapQuoteCacheRef.current[key] : undefined;\n };\n\n const getCachedDestinationUsdRate = (token?: SwapTokenOption) => {\n const intentCachedRate = getCachedIntentUsdRate(token);\n if (intentCachedRate && intentCachedRate.gt(0)) {\n return intentCachedRate;\n }\n\n const cached = getCachedMaxSwapQuote(token);\n if (\n !cached ||\n !cached.maxUsdAmount ||\n cached.maxUsdAmount.lte(0) ||\n cached.maxTokenAmount.lte(0)\n ) {\n return undefined;\n }\n return cached.maxUsdAmount.div(cached.maxTokenAmount);\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 resolveMaxSwapQuote = async (token: SwapTokenOption) => {\n const key = getMaxSwapQuoteCacheKey(token);\n if (!key) return undefined;\n\n const cached = maxSwapQuoteCacheRef.current[key];\n if (cached) return cached;\n\n const calculateMaxForSwap = nexusSDK?.calculateMaxForSwap;\n if (typeof calculateMaxForSwap !== \"function\" || !token.chainId) {\n return undefined;\n }\n\n const max = await calculateMaxForSwap({\n toChainId: token.chainId,\n toTokenAddress: (token.contractAddress || zeroAddress) as `0x${string}`,\n });\n const decimals = Number.isFinite(Number(max.decimals))\n ? Number(max.decimals)\n : token.decimals || 18;\n const maxAmount =\n parseFiatNumber(max.maxAmount) ??\n (max.maxAmountRaw !== undefined\n ? new Decimal(max.maxAmountRaw.toString()).div(\n new Decimal(10).pow(decimals),\n )\n : undefined);\n\n if (!maxAmount || maxAmount.lte(0)) return undefined;\n\n const safeMaxAmount = maxAmount.mul(receiveMaxSafetyMultiplier);\n const destinationRate = await resolveUsdRateForSymbol(max.symbol || token.symbol);\n let maxUsdAmount =\n destinationRate.gt(0) ? safeMaxAmount.mul(destinationRate) : undefined;\n\n if (!maxUsdAmount || maxUsdAmount.lte(0)) {\n const sourcesUsd = await (max.sources ?? []).reduce(\n async (sumPromise, source) => {\n const sum = await sumPromise;\n const amount = parseFiatNumber(source.amount) ?? new Decimal(0);\n if (amount.lte(0)) return sum;\n\n const sourceRate = await resolveUsdRateForSymbol(source.symbol);\n return sourceRate.gt(0) ? sum.plus(amount.mul(sourceRate)) : sum;\n },\n Promise.resolve(new Decimal(0)),\n );\n\n if (sourcesUsd.gt(0)) {\n maxUsdAmount = sourcesUsd.mul(receiveMaxSafetyMultiplier);\n }\n }\n\n const quote: CachedMaxSwapQuote = {\n decimals,\n maxTokenAmount: safeMaxAmount,\n maxUsdAmount,\n symbol: max.symbol || token.symbol,\n };\n maxSwapQuoteCacheRef.current[key] = quote;\n return quote;\n };\n\n const getPercentAmountFromMaxQuote = async (\n token: SwapTokenOption,\n pct: number,\n preferUsd: boolean,\n ) => {\n const maxQuote = await resolveMaxSwapQuote(token);\n if (!maxQuote) return undefined;\n\n const ratio = new Decimal(pct).div(100);\n if (preferUsd && maxQuote.maxUsdAmount && maxQuote.maxUsdAmount.gt(0)) {\n return {\n amount: maxQuote.maxUsdAmount\n .mul(ratio)\n .toDecimalPlaces(2, Decimal.ROUND_DOWN)\n .toFixed(),\n mode: \"usd\" as const,\n };\n }\n\n return {\n amount: maxQuote.maxTokenAmount\n .mul(ratio)\n .toDecimalPlaces(Math.max(0, maxQuote.decimals), Decimal.ROUND_DOWN)\n .toFixed(),\n mode: \"token\" as const,\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 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 = (token: SwapTokenOption, tokenAmount: Decimal) => {\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 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(source, targetUsd)\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.toDecimalPlaces(6, Decimal.ROUND_DOWN).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(remainingTokenAmount, 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.toDecimalPlaces(6, Decimal.ROUND_DOWN).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 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 ? token.sourceTokens : [token],\n );\n const seen = new Set();\n return expanded.filter((token) => {\n if (!token.chainId || !token.contractAddress) return false;\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 = 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 = (breakdown.symbol ?? asset.symbol ?? \"\").toUpperCase();\n const assetSymbol = (asset.symbol ?? \"\").toUpperCase();\n const isNativeBalance =\n isNativeTokenAddress(breakdown.contractAddress) ||\n Boolean(nativeSymbol && (breakdownSymbol === nativeSymbol || assetSymbol === nativeSymbol));\n\n if (!isNativeBalance) continue;\n balance = balance.plus(parseFiatNumber(breakdown.balance) ?? new Decimal(0));\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 ) 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: chainMeta?.name ?? breakdown.chain?.name,\n contractAddress,\n decimals: breakdown.decimals ?? asset.decimals ?? 18,\n logo: asset.icon ?? \"\",\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 getExactOutSourceTokens = (\n mode: \"all\" | \"selected\" = exactOutQuoteSourceModeRef.current,\n ) => {\n if (\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n mode === \"selected\" &&\n fromTokens.length > 0\n ) {\n return filterMinimumSourceUsdTokens(getExpandedSourceTokens(fromTokens)).filter(\n hasGasForSource,\n );\n }\n\n return getGasCapableBalanceSourceTokens();\n };\n\n const buildFromSourcesPayload = (tokens: SwapTokenOption[]) => {\n const eligibleTokens = filterMinimumSourceUsdTokens(tokens).filter(\n (token) => token.chainId && token.contractAddress,\n );\n return {\n fromSources: eligibleTokens.map((token) => ({\n chainId: token.chainId!,\n tokenAddress: token.contractAddress as `0x${string}`,\n })),\n };\n };\n\n const buildPredictiveExactOutSources = async (requiredSourceUsd: Decimal) => {\n if (requiredSourceUsd.lte(0)) return [];\n\n const destinationKey = getTokenSelectionKey(toToken);\n const candidates = getExactOutSourceTokens()\n .filter((token) => getTokenSelectionKey(token) !== destinationKey)\n .filter((token) => getTokenBalanceUsd(token).gt(0))\n .sort((a, b) => getTokenBalanceUsd(b).cmp(getTokenBalanceUsd(a)));\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.lte(0)) 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.toDecimalPlaces(6, Decimal.ROUND_DOWN).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 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 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 const selectedSourceTotal =\n exactOutQuoteSourceModeRef.current === \"selected\" && fromTokens.length > 0\n ? fromTokens.reduce(\n (sum, token) => {\n const value = parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd) ? sum.plus(value) : sum;\n },\n new Decimal(0),\n )\n : undefined;\n\n if (selectedSourceTotal && selectedSourceTotal.gt(0)) {\n return selectedSourceTotal;\n }\n\n const allSourceTotal = getGasCapableBalanceSourceTokens().reduce(\n (sum, token) => {\n const value = parseFiatNumber(token.balanceInFiat) ?? new Decimal(0);\n return value.gte(minimumSourceUsd) ? sum.plus(value) : sum;\n },\n new Decimal(0),\n );\n\n return allSourceTotal.gt(0) ? allSourceTotal : getSwapBalanceTotalUsd();\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(missingTokenAmount.mul(fiatBalance.div(availableTokenAmount)));\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 = (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 = getExactOutRequestedUsd();\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 ||\n missingUsd.lte(0) ||\n 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 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()).div(new Decimal(10).pow(decimals)).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 receiveMaxSafetyMultiplier = new Decimal(\"0.9\");\n const currentSwapEntry =\n currentSwapId !== null\n ? swapHistory.find((entry) => entry.id === currentSwapId)\n : undefined;\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) =>\n entry.id === id ? { ...entry, ...patch } : entry,\n ),\n ),\n );\n };\n\n const patchCurrentSwapHistoryEntry = (patch: Partial) => {\n patchSwapHistoryEntry(currentSwapIdRef.current, patch);\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,\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 ) => {\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 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: activeMode === \"send\" ? recipientAddress : undefined,\n opportunity: selectedOpportunity,\n feeUsd: intentFeeUsd,\n sourceExplorerUrl: null,\n finalExplorerUrl: null,\n intentExplorerUrl: null,\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\",\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(\n 1,\n Math.round((now - startedAt) / 1000),\n ),\n sourceExplorerUrl: explorerUrlsRef.current.sourceExplorerUrl,\n finalExplorerUrl: explorerUrlsRef.current.destinationExplorerUrl,\n ...patch,\n });\n void fetchSwapBalance();\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 handleRefundIntent = async (entry: SwapHistoryEntry) => {\n if (!nexusSDK || !entry.intentId) return;\n patchSwapHistoryEntry(entry.id, { status: \"refund-initiated\" });\n try {\n await nexusSDK.refundIntent(entry.intentId);\n void fetchSwapBalance();\n } catch (error: any) {\n patchSwapHistoryEntry(entry.id, {\n status: \"failed\",\n error: error?.message || \"Refund failed. Please try again.\",\n });\n void fetchSwapBalance();\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 lastSwapIntentRefreshAtRef.current = Date.now();\n cacheDestinationUsdRateFromIntent(intent);\n cachePredictiveBaselineFromIntent(intent);\n setIntentData(intent);\n setIntentToAmount(intent.destination?.amount || undefined);\n setSwapQuoteIssue(null);\n\n if (\n (activeMode === \"send\" ||\n (activeMode === \"deposit\" && swapType === \"exactOut\"))\n ) {\n syncingIntentSourcesRef.current = true;\n setFromTokens((intent.sources ?? []).map(buildIntentSourceToken));\n }\n\n try {\n const bridgeFees = intent.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 bridgeComponentsTotal = bridgeFeeData\n ? [\n executionGasFee,\n parseFiatNumber(bridgeFeeData.protocol),\n parseFiatNumber(bridgeFeeData.solver),\n parseFiatNumber(bridgeFeeData.gasSupplied),\n ].reduce(\n (sum, value) => sum.plus(value ?? new Decimal(0)),\n new Decimal(0),\n )\n : undefined;\n const bridgeTotal =\n typeof bridgeFees === \"string\"\n ? parseFiatNumber(bridgeFees)\n : parseFiatNumber(bridgeFeeData?.total) ??\n (bridgeComponentsTotal && bridgeComponentsTotal.gt(0)\n ? bridgeComponentsTotal\n : undefined);\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 [activeMode, fromTokens, swapType, swapBalance, toToken],\n );\n\n // Register swap intent hook immediately before executing a swap to prevent race conditions across multiple components\n const registerIntentHook = (runId: number) => {\n if (!nexusSDK) return;\n nexusSDK.setOnSwapIntentHook(async ({ intent, allow, deny, refresh }) => {\n if (swapRunIdRef.current !== runId) {\n deny();\n return;\n }\n // Store callbacks so accept/reject buttons can call them\n swapIntentRef.current = { intent, allow, deny, refresh, runId };\n // Populate intent data for preview\n applySwapIntent(intent);\n setIntentLoading(false);\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setPreviewQuoteRefreshing(false);\n });\n };\n\n // Deposit-specific\n const [selectedOpportunity, setSelectedOpportunity] = useState<\n DepositOpportunity | undefined\n >(() =>\n activeMode === \"deposit\" && config.opportunities?.length === 1\n ? config.opportunities[0]\n : undefined,\n );\n const [pendingOpportunity, setPendingOpportunity] = useState<\n DepositOpportunity | undefined\n >(undefined);\n const [depositAmountMode, setDepositAmountMode] = useState<\"token\" | \"usd\">(\n \"token\",\n );\n\n const toTokenFromOpportunity = (\n opp: DepositOpportunity,\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() ||\n 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: matchedToken?.decimals ?? citreaToken?.decimals ?? tokenMeta?.decimals ?? 18,\n logo: opp.tokenLogo || matchedToken?.logo || citreaToken?.logo || tokenMeta?.icon,\n chainName: CHAIN_METADATA[opp.chainId]?.name ?? citreaToken?.chainName,\n chainLogo: CHAIN_METADATA[opp.chainId]?.logo ?? citreaToken?.chainLogo,\n };\n };\n\n const getDestinationBalanceFromSwapBalances = (\n token?: SwapTokenOption,\n ) => {\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() === 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?: { token: `0x${string}`; chain: number }) => {\n if (!pair?.token || !pair.chain) return undefined;\n\n const normalizeAddress = (address?: string) => {\n if (!address) return \"\";\n return isNativeTokenAddress(address) ? zeroAddress : address.toLowerCase();\n };\n const targetAddress = normalizeAddress(pair.token);\n\n const balanceToken = deriveTokenOptions(swapBalance ?? []).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((item) => item.id === pair.chain);\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 tokenAddressSymbol ??\n (isNativePrefill ? chainMeta?.nativeCurrency?.symbol : undefined) ??\n \"Token\";\n const tokenMeta = TOKEN_METADATA[tokenSymbol as keyof typeof TOKEN_METADATA];\n\n if (\n !chain &&\n !matchedToken &&\n !citreaToken &&\n !tokenAddressSymbol &&\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 tokenMeta?.decimals ??\n (isNativePrefill ? chainMeta?.nativeCurrency?.decimals : undefined) ??\n 18,\n logo: matchedToken?.logo || citreaToken?.logo || tokenMeta?.icon,\n chainName:\n chain?.name ?? chainMeta?.name ?? citreaToken?.chainName,\n chainLogo:\n chain?.logo ?? chainMeta?.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([{ ...sourceToken, userAmount: config.prefill?.amount ?? \"\" }]);\n setSourceSelectionTouched(true);\n }\n if (destinationToken) {\n setToToken(destinationToken);\n }\n setSwapType(\"exactIn\");\n appliedTokenPrefillRef.current = prefillKey;\n }, [\n activeMode,\n config.prefill?.amount,\n config.prefill?.destination?.chain,\n config.prefill?.destination?.token,\n config.prefill?.source?.chain,\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?.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 = getDestinationBalanceFromSwapBalances(balanceToken);\n if (swapBalanceValue) {\n setDestinationBalance(swapBalanceValue);\n return;\n }\n\n const chainMeta = CHAIN_METADATA[balanceToken.chainId];\n const rpcUrl = chainMeta?.rpcUrls?.[0];\n if (!rpcUrl) return;\n\n let cancelled = false;\n const client = createPublicClient({\n chain: {\n id: balanceToken.chainId,\n name: chainMeta?.name ?? balanceToken.chainName ?? \"Destination Chain\",\n nativeCurrency: chainMeta?.nativeCurrency ?? {\n decimals: 18,\n name: \"Ether\",\n symbol: \"ETH\",\n },\n rpcUrls: {\n default: { http: [rpcUrl] },\n public: { http: [rpcUrl] },\n },\n blockExplorers: chainMeta?.blockExplorerUrls?.[0]\n ? {\n default: {\n name: chainMeta.name,\n url: chainMeta.blockExplorerUrls[0],\n },\n }\n : undefined,\n } as any,\n transport: http(rpcUrl),\n });\n\n const fetchDestinationBalance = async () => {\n try {\n let rawBalance: bigint;\n let decimals = balanceToken.decimals || 18;\n\n if (isNativeTokenAddress(balanceToken.contractAddress)) {\n rawBalance = await client.getBalance({\n address: ownerAddress as `0x${string}`,\n });\n decimals = 18;\n } else {\n const tokenAddress = balanceToken.contractAddress as `0x${string}`;\n const [balanceResult, decimalsResult] = await Promise.all([\n client.readContract({\n abi: erc20Abi,\n address: tokenAddress,\n functionName: \"balanceOf\",\n args: [ownerAddress as `0x${string}`],\n }) as Promise,\n client\n .readContract({\n abi: erc20Abi,\n address: tokenAddress,\n functionName: \"decimals\",\n })\n .catch(() => decimals),\n ]);\n\n rawBalance = balanceResult;\n decimals = Number(decimalsResult) || decimals;\n }\n\n if (!cancelled) {\n setDestinationBalance(\n `${formatReadableTokenBalanceAmount(rawBalance, decimals)} ${balanceToken.symbol}`,\n );\n }\n } catch (error) {\n console.warn(\"Unable to fetch destination token balance\", error);\n }\n };\n\n void fetchDestinationBalance();\n\n return () => {\n cancelled = true;\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\") return;\n if (selectedOpportunity) return;\n if (config.opportunities?.length === 1) {\n const [opp] = config.opportunities;\n setSelectedOpportunity(opp);\n setSwapType(\"exactOut\");\n setToToken(toTokenFromOpportunity(opp));\n }\n }, [activeMode, config.opportunities, selectedOpportunity, supportedChainsAndTokens]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || !selectedOpportunity) return;\n setToToken((current) => ({\n ...toTokenFromOpportunity(selectedOpportunity),\n balance: current?.balance ?? \"0\",\n balanceInFiat: current?.balanceInFiat ?? \"$0.00\",\n }));\n }, [activeMode, selectedOpportunity, supportedChainsAndTokens]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\") return;\n if (selectedOpportunity) return;\n if (!config.opportunities || config.opportunities.length <= 1) return;\n setPendingOpportunity((current) => current ?? config.opportunities?.[0]);\n }, [activeMode, config.opportunities, selectedOpportunity]);\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 One 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) return new Decimal(0);\n const fiat = getFiatValue(1, selectedOpportunity.tokenSymbol);\n if (Number.isFinite(fiat) && fiat > 0) {\n return new Decimal(fiat);\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 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?.toDecimalPlaces(toToken?.decimals ?? 18).toFixed() ??\n \"0\";\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 = () => {\n const shouldLoadQuote = Boolean(nexusSDK && canRefreshExactOutQuote());\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\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 =\n destinationUsdNeedingSources.lte(0)\n ? new Decimal(0)\n : cachedSourceUsdRatio && cachedSourceUsdRatio.gt(0)\n ? destinationUsdNeedingSources.mul(cachedSourceUsdRatio)\n : destinationUsdNeedingSources\n .mul(BASIS_POINTS + PREDICTIVE_EXACT_OUT_BUFFER_BPS)\n .div(BASIS_POINTS);\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 fromTokens,\n nexusSDK,\n selectedOpportunity,\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 defaultDepositSourceTokens = useMemo(() => {\n if (activeMode !== \"deposit\" || !swapBalance) return [];\n return deriveTokenOptions(swapBalance)\n .filter(hasMinimumSourceUsdBalance)\n .map((token) => ({\n ...token,\n userAmount: \"\",\n }));\n }, [activeMode, swapBalance]);\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 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 toToken.contractAddress &&\n (breakdownAddress.toLowerCase() === 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 if (!fiatBalance || fiatBalance.lt(minimumSourceUsd)) continue;\n return [\n {\n chainId,\n chainLogo: chainMeta?.logo ?? breakdown.chain?.logo ?? toToken.chainLogo,\n chainName: chainMeta?.name ?? breakdown.chain?.name ?? toToken.chainName,\n contractAddress: breakdown.contractAddress ?? toToken.contractAddress,\n decimals: breakdown.decimals ?? asset.decimals ?? toToken.decimals ?? 18,\n logo: asset.icon ?? 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 },\n ];\n }\n }\n\n return [];\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 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) => getTokenSelectionKey(token) === getTokenSelectionKey(locked),\n ),\n );\n if (missing.length === 0) return current;\n return [...current, ...missing.map((token) => ({ ...token, userAmount: \"\" }))];\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 (\n defaultDepositSourceTokens.length === 0 &&\n lockedDestinationSourceTokens.length === 0\n ) {\n return;\n }\n\n setFromTokens((current) => {\n const lockedKeys = new Set(\n lockedDestinationSourceTokens.map(getTokenSelectionKey),\n );\n const canInitialize =\n current.length === 0 ||\n current.every((token) => lockedKeys.has(getTokenSelectionKey(token)));\n if (!canInitialize) return current;\n\n const next: SwapTokenOption[] = [];\n const seen = new Set();\n for (const token of [\n ...defaultDepositSourceTokens,\n ...lockedDestinationSourceTokens,\n ]) {\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 defaultDepositSourceTokens,\n depositTokenAmountForQuote?.toFixed(),\n lockedDestinationSourceTokens,\n sourceSelectionTouched,\n toToken,\n ]);\n\n // ---------------------------------------------------------------------------\n // Handlers\n // ---------------------------------------------------------------------------\n\n const handleReset = () => {\n clearPendingSwapIntent();\n setAmount(\"\");\n setRecipientAddress(\"\");\n setTxError(null);\n setSwapStep(\"idle\");\n setCurrentSwapId(null);\n currentSwapIdRef.current = null;\n currentSwapStartedAtRef.current = 0;\n clearSelectedSources();\n setToToken(undefined);\n setSelectedOpportunity(undefined);\n setPendingOpportunity(undefined);\n setDepositAmountMode(\"token\");\n };\n\n const resetInputsAfterSuccessfulExecution = () => {\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 setToToken(undefined);\n setDepositAmountMode(\"token\");\n };\n\n const handleSelectDepositOpportunity = (opp: DepositOpportunity) => {\n clearPendingSwapIntent();\n setTxError(null);\n setSwapQuoteIssue(null);\n setSelectedOpportunity(opp);\n setPendingOpportunity(opp);\n setSwapType(\"exactOut\");\n setDepositAmountMode(\"token\");\n setAmount(\"\");\n clearSelectedSources();\n setToToken(toTokenFromOpportunity(opp));\n };\n\n const handleClose = () => {\n clearPendingSwapIntent();\n onClose?.();\n };\n\n const handleConnectWallet = async () => {\n if (walletActionPending || nexusLoading || 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 activeConnector = nextConnector;\n }\n\n const connectorProvider = await activeConnector\n ?.getProvider()\n .catch(() => undefined);\n const connectorClientProvider = connectorClient\n ? {\n request: (args: unknown) =>\n connectorClient.request(args as any),\n }\n : undefined;\n const walletClientProvider = walletClient\n ? {\n request: (args: unknown) =>\n 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 ??\n walletClientProvider ??\n windowProvider);\n\n if (!effectiveProvider || typeof effectiveProvider.request !== \"function\") {\n throw new Error(\"Wallet provider is not ready yet.\");\n }\n\n await handleInit(effectiveProvider as EthereumProvider);\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 โ€” SDK will trigger setOnSwapIntentHook for preview */\n const handleEnterPreview = async (\n options: { background?: boolean } = {},\n ) => {\n const { background = false } = options;\n const isExactOutFlow = activeMode === \"deposit\" || activeMode === \"send\";\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 setTxError(null);\n setSwapQuoteIssue(null);\n\n if (\n !background &&\n swapIntentRef.current?.runId === swapRunIdRef.current &&\n intentData &&\n !intentLoading &&\n (activeMode !== \"send\" || Boolean(recipientAddress)) &&\n ((activeMode !== \"deposit\" && activeMode !== \"send\") ||\n (intentData.sources ?? []).length > 0)\n ) {\n swapStepRef.current = \"preview-intent\";\n setSwapStep(\"preview-intent\");\n return;\n }\n\n if (\n !background &&\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n (!intentData ||\n !swapIntentRef.current ||\n swapIntentRef.current.runId !== swapRunIdRef.current ||\n (intentData.sources ?? []).length === 0)\n ) {\n setTxError(\"Quote unavailable. Please wait for sources to be selected.\");\n return;\n }\n\n const hasCustomSwapRecipient =\n activeMode === \"swap\" &&\n Boolean(recipientAddress) &&\n (!defaultRecipientAddress ||\n recipientAddress.toLowerCase() !== defaultRecipientAddress.toLowerCase());\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() ===\n ownerAddress.toLowerCase()\n ) {\n setTxError(\"Recipient cannot be the connected wallet.\");\n return;\n }\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 // Claim ownership of global singleton hook before executing SDK swap\n registerIntentHook(runId);\n\n const getSwapStepListFromEvent = (event: { args: any }) => {\n const args = (event as any).args;\n return Array.isArray(args)\n ? args\n : Array.isArray(args?.steps)\n ? args.steps\n : [];\n };\n\n const handleSwapEvent = (event: { name: string; args: any }) => {\n console.log(\"[NexusOne][SDK swap event]\", event.name, event);\n if (event.name === NEXUS_EVENTS.SWAP_STEPS_LIST) {\n const stepList = getSwapStepListFromEvent(event);\n if (stepList.length > 0) {\n swapStepsListRef.current = stepList as SwapStepType[];\n appendProgressListEvent(event.name, stepList);\n onStepsList(stepList);\n }\n return;\n }\n if (event.name === NEXUS_EVENTS.STEPS_LIST) {\n const args = (event as any).args;\n const stepList = Array.isArray(args)\n ? args\n : Array.isArray(args?.steps)\n ? args.steps\n : [];\n if (stepList.length > 0) {\n appendProgressListEvent(event.name, stepList);\n onStepsList(stepList);\n }\n return;\n }\n if (event.name === NEXUS_EVENTS.STEP_COMPLETE) {\n const step = event.args as BridgeStepType;\n appendProgressEvent(event.name, step, true);\n if (\n (step as any)?.type === \"TRANSACTION_SENT\" ||\n (step as any)?.type === \"TRANSACTION_CONFIRMED\"\n ) {\n markSwapExecutionStarted();\n }\n if ((step as any)?.data?.explorerURL) {\n mergeExplorerUrls({\n destinationExplorerUrl: (step as any).data.explorerURL,\n });\n }\n if ((step as any)?.completed !== false) {\n onStepComplete(step as any);\n }\n return;\n }\n if (event.name === \"SWAP_SKIPPED\") {\n const step =\n event.args && typeof event.args === \"object\"\n ? event.args\n : ({\n completed: true,\n data: event.args,\n type: \"SWAP_SKIPPED\",\n typeID: \"SWAP_SKIPPED\",\n } as unknown as SwapStepType);\n enterSkippedSwapProgress();\n appendProgressEvent(NEXUS_EVENTS.SWAP_STEP_COMPLETE, step, true);\n onStepComplete(step as SwapStepType);\n return;\n }\n if (event.name === NEXUS_EVENTS.SWAP_STEP_COMPLETE) {\n const step = event.args;\n const swapSkipped = isSwapSkippedStepType(getProgressStepType(step));\n if (swapSkipped) {\n enterSkippedSwapProgress();\n }\n appendProgressEvent(event.name, step, true);\n if (\n [\n \"SOURCE_SWAP_BATCH_TX\",\n \"SOURCE_SWAP_HASH\",\n \"BRIDGE_DEPOSIT\",\n \"RFF_ID\",\n \"DESTINATION_SWAP_BATCH_TX\",\n \"DESTINATION_SWAP_HASH\",\n \"SWAP_COMPLETE\",\n \"SWAP_SKIPPED\",\n ].includes(step?.type ?? \"\")\n ) {\n markSwapExecutionStarted();\n }\n if (step?.type === \"SOURCE_SWAP_HASH\" && step.explorerURL) {\n mergeExplorerUrls({ sourceExplorerUrl: step.explorerURL });\n }\n if (step?.type === \"DESTINATION_SWAP_HASH\" && step.explorerURL) {\n mergeExplorerUrls({ destinationExplorerUrl: step.explorerURL });\n }\n if (step?.type === \"BRIDGE_DEPOSIT\" && (step as any).data?.explorerURL) {\n mergeExplorerUrls({\n sourceExplorerUrl: (step as any).data.explorerURL,\n });\n }\n if (step?.type === \"RFF_ID\") {\n const nextIntentId = Number((step as any).data);\n if (Number.isFinite(nextIntentId) && nextIntentId > 0) {\n patchCurrentSwapHistoryEntry({ intentId: nextIntentId });\n }\n }\n if (step?.completed !== false) {\n onStepComplete(step);\n }\n }\n };\n\n try {\n if (!isExactOutFlow) {\n const fromPayload: {\n chainId: number;\n tokenAddress: `0x${string}`;\n amount: 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 = cleanAmount\n .toDecimalPlaces(Math.max(0, token.decimals || 18), Decimal.ROUND_DOWN)\n .toFixed();\n\n fromPayload.push({\n chainId: token.chainId!,\n tokenAddress: token.contractAddress as `0x${string}`,\n amount: nexusSDK.utils.parseUnits(\n safeTokenAmountStr,\n token.decimals || 18,\n ),\n });\n }\n\n if (fromPayload.length === 0) {\n throw new Error(\"No source amount available for swap.\");\n }\n\n resetExplorerUrls();\n // Start exact-in swap โ€” the intent hook will fire and populate preview\n const result = await nexusSDK.swapWithExactIn(\n {\n from: fromPayload,\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n },\n {\n onEvent: (event: any) => {\n if (swapRunIdRef.current !== runId) return;\n handleSwapEvent(event);\n },\n },\n );\n if (!result?.success) {\n throw new Error(result?.error || \"Swap failed\");\n }\n const intentExplorerUrl = result.result.explorerURL || null;\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ?? currentSwapEntry?.intentId;\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n finishCurrentSwapHistoryEntry(\"fulfilled\", {\n intentExplorerUrl,\n intentId,\n finalExplorerUrl:\n explorerUrlsRef.current.destinationExplorerUrl ||\n explorerUrlsRef.current.sourceExplorerUrl,\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 = nexusSDK.utils.parseUnits(\n exactOutAmountString,\n toToken.decimals || 18,\n );\n\n resetExplorerUrls();\n\n const fromSourcesPayload = buildFromSourcesPayload(\n getExactOutSourceTokens(),\n );\n\n const isNative =\n !toToken.contractAddress ||\n toToken.contractAddress.toLowerCase() ===\n \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\" ||\n toToken.contractAddress ===\n \"0x0000000000000000000000000000000000000000\";\n let executeConfig: any;\n if (activeMode === \"deposit\" && !selectedOpportunity?.execute) {\n throw new Error(\n \"Selected deposit opportunity is missing execute parameters.\",\n );\n }\n\n if (activeMode === \"deposit\" && selectedOpportunity?.execute) {\n executeConfig =\n typeof selectedOpportunity.execute === \"function\"\n ? selectedOpportunity.execute(\n amountBigInt,\n (ownerAddress ?? connectedAddress) as `0x${string}`,\n )\n : selectedOpportunity.execute;\n } else if (\n activeMode === \"send\" &&\n resolvedRecipientAddress\n ) {\n if (isNative) {\n executeConfig = {\n to: resolvedRecipientAddress as `0x${string}`,\n value: amountBigInt,\n gas: BigInt(100000),\n };\n } else {\n executeConfig = {\n to: toToken.contractAddress as `0x${string}`,\n data: encodeFunctionData({\n abi: erc20Abi,\n functionName: \"transfer\",\n args: [resolvedRecipientAddress as `0x${string}`, amountBigInt],\n }),\n gas: BigInt(100000),\n };\n }\n }\n\n if (executeConfig) {\n const onEvent = (event: any) => {\n if (swapRunIdRef.current !== runId) return;\n handleSwapEvent(event);\n };\n const sdkWithOptionalTransfer = nexusSDK as any;\n const result =\n activeMode === \"send\" &&\n typeof sdkWithOptionalTransfer.swapAndTransfer === \"function\"\n ? await sdkWithOptionalTransfer.swapAndTransfer(\n {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmount: amountBigInt,\n recipient: resolvedRecipientAddress as `0x${string}`,\n ...fromSourcesPayload,\n },\n { onEvent },\n )\n : await nexusSDK.swapAndExecute(\n {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmount: amountBigInt,\n execute: executeConfig,\n ...fromSourcesPayload,\n },\n { onEvent },\n );\n\n const swapResult = result?.swapResult ?? result?.result ?? null;\n const swapSkipped = Boolean((result as any)?.swapSkipped);\n if (!swapResult && !swapSkipped && activeMode !== \"send\") {\n throw new Error(\"Swap failed\");\n }\n const executeTxHash =\n result?.executeResponse?.txHash ||\n result?.transactionHash ||\n result?.txHash ||\n null;\n const intentExplorerUrl =\n swapResult?.explorerURL || result?.intentExplorerUrl || null;\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ?? currentSwapEntry?.intentId;\n const finalExplorerUrl =\n result?.explorerUrl ||\n result?.executeExplorerUrl ||\n getExplorerTxUrl(toToken.chainId, executeTxHash);\n if (finalExplorerUrl) {\n mergeExplorerUrls({ destinationExplorerUrl: finalExplorerUrl });\n }\n patchCurrentSwapHistoryEntry({\n intentExplorerUrl,\n intentId,\n finalExplorerUrl,\n });\n } else {\n const result = await nexusSDK.swapWithExactOut(\n {\n toChainId: toToken.chainId!,\n toTokenAddress: toToken.contractAddress as `0x${string}`,\n toAmount: amountBigInt,\n ...fromSourcesPayload,\n },\n {\n onEvent: (event: any) => {\n if (swapRunIdRef.current !== runId) return;\n handleSwapEvent(event);\n },\n },\n );\n if (!result?.success) {\n throw new Error(result?.error || \"Swap failed\");\n }\n const intentExplorerUrl = result.result.explorerURL || null;\n const intentId =\n extractIntentIdFromUrl(intentExplorerUrl) ?? currentSwapEntry?.intentId;\n patchCurrentSwapHistoryEntry({ intentExplorerUrl, intentId });\n }\n\n if (\n swapRunIdRef.current === runId &&\n swapStepRef.current === \"progress\"\n ) {\n finishCurrentSwapHistoryEntry(\"fulfilled\");\n resetInputsAfterSuccessfulExecution();\n onComplete?.();\n setSwapStep(\"success\");\n }\n }\n } catch (err: any) {\n console.error(\"Error in handleEnterPreview:\", err);\n if (swapRunIdRef.current !== runId) {\n return;\n }\n setQuoteRefreshing(false);\n setIntentLoading(false);\n setReceiveMaxCalculating(false);\n const hasActiveExecution =\n swapStepRef.current === \"progress\" && Boolean(currentSwapIdRef.current);\n const showFailedProgressThenReceipt = (\n error: string,\n patch: Partial = {},\n ) => {\n const failedProgressEvent = progressEventsRef.current.at(-1);\n const fallbackFailedStep =\n activeMode === \"deposit\" || activeMode === \"send\"\n ? ({ type: \"APPROVAL\", typeID: \"AP\" } as BridgeStepType)\n : ({\n type: \"DETERMINING_SWAP\",\n typeID: \"DETERMINING_SWAP\",\n } as unknown as SwapStepType);\n const failedStep =\n failedProgressEvent?.step ?? fallbackFailedStep;\n const autoRefundAvailable =\n isAutoRefundAvailableProgressEvent(failedProgressEvent);\n setFailedProgressStep(failedStep);\n finishCurrentSwapHistoryEntry(\"failed\", {\n error,\n autoRefundAvailable,\n failureMessage: getFailureMessageForProgressStep(\n failedStep,\n 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 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 const errorMessage =\n err?.message ||\n (typeof err === \"string\"\n ? err\n : \"Transaction failed. Please try again or check console.\");\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 useEffect(() => {\n if (activeMode !== \"swap\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = 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 clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n const timer = window.setTimeout(() => {\n void handleEnterPreview({ background: true });\n }, EXACT_OUT_INPUT_DEBOUNCE_MS);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [activeMode, amount, fromTokens, nexusSDK, swapStep, toToken]);\n\n useEffect(() => {\n if (activeMode !== \"deposit\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = 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 clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n const timer = window.setTimeout(() => {\n void handleEnterPreview({ background: true });\n }, EXACT_OUT_INPUT_DEBOUNCE_MS);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [\n activeMode,\n amount,\n depositAmountMode,\n nexusSDK,\n sourceSelectionRevision,\n selectedOpportunity,\n swapStep,\n toToken,\n ]);\n\n useEffect(() => {\n if (activeMode !== \"send\" || swapStep !== \"idle\" || !nexusSDK) return;\n\n if (syncingIntentSourcesRef.current) {\n syncingIntentSourcesRef.current = 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 clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n setQuoteRefreshing(true);\n const timer = window.setTimeout(() => {\n void handleEnterPreview({ background: true });\n }, EXACT_OUT_INPUT_DEBOUNCE_MS);\n\n return () => {\n window.clearTimeout(timer);\n if (syncingIntentSourcesRef.current) return;\n if (swapStepRef.current === \"idle\") {\n clearPendingSwapIntent(true, { keepQuoteRefreshing: true });\n }\n };\n }, [activeMode, amount, nexusSDK, sourceSelectionRevision, swapStep, toToken]);\n\n const refreshActiveSwapIntent = useCallback(async () => {\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 isPreviewRefresh = swapStepRef.current === \"preview-intent\";\n if (isPreviewRefresh) {\n setPreviewQuoteRefreshing(true);\n } else {\n setQuoteRefreshing(true);\n }\n try {\n const updated = await activeIntent.refresh();\n if (!updated || swapRunIdRef.current !== runId) return;\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 (swapRunIdRef.current === runId) {\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\" || activeMode === \"deposit\" || activeMode === \"send\") &&\n Boolean(intentData && swapIntentRef.current) &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n\n if (!hasRefreshableIntent) return;\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 (\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 intentData,\n intentLoading,\n previewQuoteRefreshing,\n quoteRefreshing,\n receiveMaxCalculating,\n refreshActiveSwapIntent,\n swapStep,\n ]);\n\n useEffect(() => {\n const hasRefreshableIntent =\n (activeMode === \"swap\" || activeMode === \"deposit\" || activeMode === \"send\") &&\n Boolean(intentData && swapIntentRef.current) &&\n (swapStep === \"idle\" || swapStep === \"preview-intent\");\n\n if (!hasRefreshableIntent) {\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 }, [activeMode, intentData, swapStep]);\n\n /** User accepted swap from the preview โ€” call allow() from the intent hook */\n const handleSwapAccept = () => {\n if (swapIntentRef.current) {\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 swapIntentRef.current.allow();\n // The swap promise in handleEnterPreview will resolve/reject\n }\n };\n\n // ---------------------------------------------------------------------------\n // Header title\n // ---------------------------------------------------------------------------\n const getTitle = () => {\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\") return \"Swap Failed\";\n return \"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\") return \"Deposit Failed\";\n return \"Deposit\";\n }\n if (activeMode === \"send\") {\n if (swapStep === \"progress\") return \"Sendingโ€ฆ\";\n if (swapStep === \"success\") return \"Send Complete\";\n if (swapStep === \"failed\") return \"Send Failed\";\n return \"Send\";\n }\n return \"Nexus One\";\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 !== \"idle\" &&\n swapStep !== \"choose-swap-asset\" &&\n swapStep !== \"choose-receive-asset\" &&\n swapStep !== \"enter-recipient\";\n const handleBack = () => {\n if (swapStep === \"history\") {\n setSwapStep(\"idle\");\n return;\n }\n if (swapStep === \"choose-swap-asset\") {\n closeDrawerToIdle();\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 = (\n val: string,\n panel: \"send\" | \"receive\",\n ) => {\n syncingIntentSourcesRef.current = false;\n setSwapQuoteIssue(null);\n setTxError(null);\n const nextAmount = parseFiatNumber(val);\n const hasSelectedSourceToken = fromTokens.some(\n (token) => token.chainId && token.contractAddress,\n );\n const shouldLoadQuote = Boolean(\n nexusSDK && nextAmount?.gt(0) && toToken && hasSelectedSourceToken,\n );\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\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 One 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 handleDepositAmountChange = (val: string) => {\n syncingIntentSourcesRef.current = false;\n setExactOutQuoteSourceModeValue(\"all\");\n maxPercentRunRef.current += 1;\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setSwapQuoteIssue(null);\n const nextAmount = parseFiatNumber(val);\n const shouldLoadQuote = Boolean(\n nexusSDK && nextAmount?.gt(0) && toToken && selectedOpportunity,\n );\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n } else {\n clearSelectedSources();\n }\n setAmount(val);\n };\n\n const handleSendAmountChange = (val: string) => {\n syncingIntentSourcesRef.current = false;\n setExactOutQuoteSourceModeValue(\"all\");\n maxPercentRunRef.current += 1;\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setSwapQuoteIssue(null);\n setSwapType(\"exactOut\");\n const nextAmount = parseFiatNumber(val);\n const shouldLoadQuote = Boolean(nexusSDK && nextAmount?.gt(0) && toToken);\n clearPendingSwapIntent(true, { keepQuoteRefreshing: shouldLoadQuote });\n if (shouldLoadQuote) {\n setQuoteRefreshing(true);\n } else {\n clearSelectedSources();\n }\n setAmount(val);\n };\n\n const handleDepositAmountModeToggle = () => {\n syncingIntentSourcesRef.current = false;\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.div(rate).toDecimalPlaces(toToken?.decimals ?? 18);\n setAmount(converted.toFixed());\n }\n clearPendingSwapIntent();\n setDepositAmountMode((current) => (current === \"token\" ? \"usd\" : \"token\"));\n };\n\n const handleDepositPercentSelect = async (pct: number) => {\n if (!toToken) return;\n\n syncingIntentSourcesRef.current = false;\n setTxError(null);\n setSwapQuoteIssue(null);\n const runId = ++maxPercentRunRef.current;\n\n if (pct !== 100) {\n const usdAmount = getTotalBalancePercentUsdAmount(pct);\n const shouldUseMaxQuoteFallback =\n depositAmountMode === \"usd\" && getDepositTokenUsdRate().lte(0);\n const nextAmount =\n depositAmountMode === \"usd\"\n ? usdAmount.toDecimalPlaces(2, Decimal.ROUND_DOWN).toFixed()\n : formatTokenAmountFromUsd(usdAmount, toToken);\n\n if (nextAmount && !shouldUseMaxQuoteFallback) {\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleDepositAmountChange(nextAmount);\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(true);\n setMaxCalculationPercent(pct);\n try {\n await waitForNextPaint();\n const fallback = await getPercentAmountFromMaxQuote(\n toToken,\n pct,\n depositAmountMode === \"usd\",\n );\n if (runId !== maxPercentRunRef.current) return;\n if (!fallback) {\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 setDepositAmountMode(fallback.mode);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleDepositAmountChange(fallback.amount);\n } catch (error: any) {\n if (runId !== maxPercentRunRef.current) return;\n console.error(\"Unable to calculate percentage deposit amount\", error);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n if (isInsufficientSourcesError(error)) {\n setSwapQuoteIssue(buildInsufficientSourcesIssue(error));\n return;\n }\n setTxError(\n error?.message || \"Unable to calculate this percentage for the deposit asset.\",\n );\n }\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(true);\n setMaxCalculationPercent(100);\n try {\n await waitForNextPaint();\n const maxAmount = await getPercentAmountFromMaxQuote(\n toToken,\n 100,\n depositAmountMode === \"usd\",\n );\n if (runId !== maxPercentRunRef.current) return;\n if (!maxAmount) {\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n setTxError(\"No depositable amount is available for this opportunity.\");\n return;\n }\n\n setDepositAmountMode(maxAmount.mode);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleDepositAmountChange(maxAmount.amount);\n } catch (error: any) {\n if (runId !== maxPercentRunRef.current) return;\n console.error(\"Unable to calculate max deposit amount\", error);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n if (isInsufficientSourcesError(error)) {\n setSwapQuoteIssue(buildInsufficientSourcesIssue(error));\n return;\n }\n setTxError(\n error?.message || \"Unable to calculate the max deposit amount.\",\n );\n }\n };\n\n const handleSendPercentSelect = async (pct: number) => {\n if (!toToken) return;\n\n syncingIntentSourcesRef.current = false;\n setTxError(null);\n setSwapQuoteIssue(null);\n const runId = ++maxPercentRunRef.current;\n\n if (pct !== 100) {\n const usdAmount = getTotalBalancePercentUsdAmount(pct);\n const nextAmount = formatTokenAmountFromUsd(usdAmount, toToken);\n\n if (nextAmount) {\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleSendAmountChange(nextAmount);\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(true);\n setMaxCalculationPercent(pct);\n try {\n await waitForNextPaint();\n const fallback = await getPercentAmountFromMaxQuote(toToken, pct, false);\n if (runId !== maxPercentRunRef.current) return;\n if (!fallback) {\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 setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleSendAmountChange(fallback.amount);\n } catch (error: any) {\n if (runId !== maxPercentRunRef.current) return;\n console.error(\"Unable to calculate percentage send amount\", error);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n if (isInsufficientSourcesError(error)) {\n setSwapQuoteIssue(buildInsufficientSourcesIssue(error));\n return;\n }\n setTxError(\n error?.message || \"Unable to calculate this percentage for the send asset.\",\n );\n }\n return;\n }\n\n setQuoteRefreshing(false);\n setReceiveMaxCalculating(true);\n setMaxCalculationPercent(100);\n try {\n await waitForNextPaint();\n const maxAmount = await getPercentAmountFromMaxQuote(toToken, 100, false);\n if (runId !== maxPercentRunRef.current) return;\n if (!maxAmount) {\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n setTxError(\"No transferable amount is available for this asset.\");\n return;\n }\n\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n handleSendAmountChange(maxAmount.amount);\n } catch (error: any) {\n if (runId !== maxPercentRunRef.current) return;\n console.error(\"Unable to calculate max send amount\", error);\n setReceiveMaxCalculating(false);\n setMaxCalculationPercent(null);\n setQuoteRefreshing(false);\n if (isInsufficientSourcesError(error)) {\n setSwapQuoteIssue(buildInsufficientSourcesIssue(error));\n return;\n }\n setTxError(error?.message || \"Unable to calculate the max send amount.\");\n }\n };\n\n // ---------------------------------------------------------------------------\n // Render\n // ---------------------------------------------------------------------------\n const exactOutInsufficientSourceIssue =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n swapQuoteIssue?.type === \"insufficientSources\"\n ? swapQuoteIssue\n : null;\n const isExactOutRouteLoading =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n swapStep === \"idle\" &&\n swapType === \"exactOut\" &&\n Boolean(toToken && (receiveMaxCalculating || (amount && Number(amount) > 0))) &&\n !exactOutInsufficientSourceIssue &&\n (quoteRefreshing || intentLoading || receiveMaxCalculating);\n const hasCurrentRunnableIntent =\n Boolean(intentData && swapIntentRef.current) &&\n swapIntentRef.current?.runId === swapRunIdRef.current &&\n !intentLoading;\n const hasIntentSources = Boolean((intentData?.sources ?? []).length > 0);\n const isQuoteUnavailableForAutoSourceFlow =\n (activeMode === \"deposit\" || activeMode === \"send\") &&\n Boolean(hasPositiveDecimalInput(amount) && toToken) &&\n !quoteRefreshing &&\n !receiveMaxCalculating &&\n !intentLoading &&\n !exactOutInsufficientSourceIssue &&\n (!hasCurrentRunnableIntent || !hasIntentSources);\n const hasPositiveRootAmount = hasPositiveDecimalInput(amount);\n const hasReadySwapQuoteInput = hasReadyExactInSwapInput(fromTokens, toToken);\n const needsWalletConnection = !ownerAddress || !nexusSDK;\n const walletConnectBusy =\n walletActionPending ||\n nexusLoading ||\n isWalletConnectPending ||\n walletStatus === \"connecting\";\n const walletCtaLabel = walletConnectBusy ? \"Connecting...\" : \"Connect Wallet\";\n const isSwapCtaDisabled =\n needsWalletConnection\n ? walletConnectBusy\n : !hasReadySwapQuoteInput ||\n receiveMaxCalculating ||\n quoteRefreshing ||\n Boolean(exactOutInsufficientSourceIssue);\n const isDepositCtaDisabled =\n needsWalletConnection\n ? walletConnectBusy\n : !hasPositiveRootAmount ||\n !toToken ||\n quoteRefreshing ||\n receiveMaxCalculating ||\n isQuoteUnavailableForAutoSourceFlow ||\n Boolean(exactOutInsufficientSourceIssue);\n const sendNeedsRecipient = activeMode === \"send\" && !recipientAddress;\n const isSendCtaDisabled =\n needsWalletConnection\n ? walletConnectBusy\n : !hasPositiveRootAmount ||\n !toToken ||\n hasSameOwnerSendRecipient ||\n receiveMaxCalculating ||\n (!sendNeedsRecipient &&\n (quoteRefreshing || isQuoteUnavailableForAutoSourceFlow)) ||\n Boolean(exactOutInsufficientSourceIssue);\n const quoteCtaLabel = (fallback: string) => {\n if (needsWalletConnection) return walletCtaLabel;\n if (exactOutInsufficientSourceIssue) return \"Insufficient balance\";\n if (receiveMaxCalculating) return \"Calculating...\";\n if (quoteRefreshing) return \"Intent fetching...\";\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 (exactOutInsufficientSourceIssue) return \"Insufficient balance\";\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) => sum.plus(parseFiatNumber((source as any).value) ?? new Decimal(0)),\n new Decimal(0),\n );\n const previewSourceUsdNumber =\n 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 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 resolvedToToken =\n toToken ??\n (activeMode === \"deposit\" && selectedOpportunity\n ? toTokenFromOpportunity(selectedOpportunity)\n : undefined);\n const toTokenWithFetchedBalance =\n resolvedToToken && destinationBalance\n ? { ...resolvedToToken, balance: destinationBalance }\n : resolvedToToken;\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: parseFiatNumber(intentData?.destination?.amount),\n producedUsd: parseFiatNumber(intentData?.destination?.value),\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 = shouldShowPredictiveExactOutDisplay\n ? predictiveExactOutQuote?.sources ?? fromTokens\n : fromTokens;\n const displayFromTokens = (() => {\n if (\n !destinationBalanceDisplayToken ||\n (activeMode !== \"deposit\" && activeMode !== \"send\")\n ) {\n return baseDisplayFromTokens;\n }\n\n const destinationKey = getTokenSelectionKey(destinationBalanceDisplayToken);\n let replacedEmptyDestinationToken = false;\n const tokens = baseDisplayFromTokens.map((token) => {\n const isDestinationToken =\n 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 return replacedEmptyDestinationToken\n ? tokens\n : [...tokens, destinationBalanceDisplayToken];\n })();\n const displayExactOutRouteLoading =\n isExactOutRouteLoading && !shouldShowPredictiveExactOutDisplay;\n const totalSwapBalanceUsd = getSwapBalanceTotalUsd()\n .toDecimalPlaces(2)\n .toFixed();\n const sendAmountUsd =\n amount && toToken\n ? getTokenUsdValue(\n {\n ...toToken,\n userAmount: amount,\n userAmountMode: \"token\",\n },\n amount,\n ).toNumber()\n : 0;\n const isIdleSwapQuoteLoading =\n activeMode === \"swap\" && swapStep === \"idle\" && quoteRefreshing;\n const isReceiveAmountLoading =\n receiveMaxCalculating ||\n (isIdleSwapQuoteLoading && swapType === \"exactIn\" && !idleReceiveQuoteAmount);\n const isReceiveUsdLoading =\n receiveMaxCalculating ||\n (isIdleSwapQuoteLoading && swapType === \"exactIn\" && !idleReceiveQuoteUsd);\n const hasQuoteRefreshCountdown =\n (activeMode === \"swap\" || activeMode === \"deposit\" || activeMode === \"send\") &&\n Boolean(intentData && swapIntentRef.current) &&\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 isDrawerOverlayActive =\n swapStep === \"choose-swap-asset\" ||\n swapStep === \"choose-receive-asset\" ||\n swapStep === \"enter-recipient\" ||\n closingDrawerStep !== null;\n\n return (\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 {/* Protocol chip appended next to Title when Deposit Protocol selected */}\n {isTitleCentered() &&\n activeMode === \"deposit\" &&\n swapStep === \"idle\" &&\n selectedOpportunity && (\n
\n {\n clearPendingSwapIntent();\n setSelectedOpportunity(undefined);\n setToToken(undefined);\n clearSelectedSources();\n setAmount(\"\");\n setDepositAmountMode(\"token\");\n }}\n className=\"flex items-center gap-1 pl-2 pr-1.5 py-1 rounded-[4px] hover:bg-black/5 transition-colors\"\n style={{\n fontFamily: \"var(--font-geist-mono), sans-serif\",\n fontSize: \"10px\",\n fontWeight: 500,\n color: \"var(--foreground-muted, #848483)\",\n background: \"var(--background-tertiary, #F0F0EF)\",\n border: \"none\",\n cursor: \"pointer\",\n }}\n >\n {selectedOpportunity.title || selectedOpportunity.protocol}\n \n \n
\n )}\n \n\n {/* Right side icons */}\n \n {hasQuoteRefreshCountdown && (\n \n )}\n setSwapStep(\"history\")}\n style={{\n alignItems: \"center\",\n backgroundColor: \"#FFFFFE\",\n borderRadius: \"8px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n height: \"32px\",\n justifyContent: \"center\",\n outline: \"1px solid #E8E8E7\",\n width: \"32px\",\n cursor: \"pointer\",\n border: \"none\",\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 />\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 fromTokens={fromTokens}\n toToken={toTokenWithFetchedBalance}\n receiveQuoteUsd={idleReceiveQuoteUsd}\n sourceRouteStatus={\n exactOutInsufficientSourceIssue\n ? \"insufficient\"\n : isExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n sourceRouteMessage={exactOutInsufficientSourceIssue?.message}\n totalBalance={totalSwapBalanceUsd}\n usdValue={amount && usdValue > 0 ? usdValue.toFixed(2) : \"\"}\n swapType={swapType}\n allowOverBalanceAmounts={needsWalletConnection}\n onOpenSourcePicker={(index) => {\n setEditingAssetIndex(index ?? null);\n openDrawerStep(\"choose-swap-asset\");\n }}\n onOpenDestPicker={() => openDrawerStep(\"choose-receive-asset\")}\n onOpenRecipientPicker={handleOpenRecipientEditor}\n recipientAddress={effectiveRecipientAddress}\n defaultRecipientAddress={defaultRecipientAddress}\n onUpdateTokens={setFromTokens}\n />\n\n {txError && !exactOutInsufficientSourceIssue && (\n \n )}\n\n {/* CTA Button */}\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n void handleEnterPreview();\n }}\n disabled={isSwapCtaDisabled}\n style={{\n alignItems: \"center\",\n backgroundColor: exactOutInsufficientSourceIssue\n ? \"#FCEEED\"\n : isSwapCtaDisabled\n\t ? \"#F0F0EF\"\n\t : \"#006BF4\",\n\t border: exactOutInsufficientSourceIssue\n\t ? \"1px solid #F7C4C1\"\n\t : \"none\",\n\t borderRadius: exactOutInsufficientSourceIssue ? \"4px\" : \"8px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"8px\",\n height: \"48px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n\t cursor: isSwapCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {exactOutInsufficientSourceIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n quoteRefreshing ||\n receiveMaxCalculating ? (\n \n ) : null}\n \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 {/* Opportunity list */}\n {config.opportunities &&\n config.opportunities.length > 0 &&\n !selectedOpportunity && (\n <>\n \n\n {/* Done button for opportunity selection */}\n \n {\n const opportunity =\n pendingOpportunity ?? config.opportunities?.[0];\n if (opportunity) {\n handleSelectDepositOpportunity(opportunity);\n setSwapStep(\"idle\");\n }\n }}\n style={{\n alignItems: \"center\",\n backgroundColor: \"#006BF4\",\n borderRadius: \"8px\",\n boxShadow: \"#5555550D 0px 1px 4px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flex: 1,\n height: \"48px\",\n justifyContent: \"center\",\n border: \"none\",\n cursor: \"pointer\",\n }}\n >\n \n Done\n \n \n \n \n )}\n\n {/* After opportunity selected โ€” show deposit form */}\n {(!config.opportunities ||\n config.opportunities.length === 0 ||\n selectedOpportunity) && (\n <>\n openDrawerStep(\"choose-swap-asset\")}\n onSetPercent={handleDepositPercentSelect}\n routeStatus={\n exactOutInsufficientSourceIssue\n ? \"insufficient\"\n : displayExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n routeMessage={exactOutInsufficientSourceIssue?.message}\n isCalculatingMax={receiveMaxCalculating}\n calculatingPercent={maxCalculationPercent}\n isQuoteRefreshing={quoteRefreshing || intentLoading}\n showAutoBadge={!sourceSelectionTouched}\n />\n\n {txError && !exactOutInsufficientSourceIssue && (\n \n )}\n\n \n {\n if (needsWalletConnection) {\n void handleConnectWallet();\n return;\n }\n void handleEnterPreview();\n }}\n disabled={isDepositCtaDisabled}\n style={{\n alignItems: \"center\",\n backgroundColor: exactOutInsufficientSourceIssue\n ? \"#FCEEED\"\n : isDepositCtaDisabled\n\t ? \"#F0F0EF\"\n\t : \"#006BF4\",\n\t border: exactOutInsufficientSourceIssue\n\t ? \"1px solid #F7C4C1\"\n\t : \"none\",\n\t borderRadius: exactOutInsufficientSourceIssue ? \"4px\" : \"8px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"8px\",\n height: \"48px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n\t cursor: isDepositCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {exactOutInsufficientSourceIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n quoteRefreshing ||\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 0 ? sendAmountUsd.toFixed(2) : \"\"\n }\n onOpenAssetPicker={() => openDrawerStep(\"choose-receive-asset\")}\n onOpenSourcePicker={() => {\n setEditingAssetIndex(null);\n openDrawerStep(\"choose-swap-asset\");\n }}\n onOpenRecipientPicker={handleOpenRecipientEditor}\n recipientAddress={recipientAddress || \"\"}\n onSetPercent={handleSendPercentSelect}\n routeStatus={\n exactOutInsufficientSourceIssue\n ? \"insufficient\"\n : displayExactOutRouteLoading\n ? \"loading\"\n : undefined\n }\n routeMessage={exactOutInsufficientSourceIssue?.message}\n isCalculatingMax={receiveMaxCalculating}\n calculatingPercent={maxCalculationPercent}\n isQuoteRefreshing={quoteRefreshing}\n showAutoBadge={!sourceSelectionTouched}\n />\n\n {txError && !exactOutInsufficientSourceIssue && (\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 disabled={isSendCtaDisabled}\n style={{\n alignItems: \"center\",\n backgroundColor: exactOutInsufficientSourceIssue\n ? \"#FCEEED\"\n : isSendCtaDisabled\n\t ? \"#F0F0EF\"\n\t : \"#006BF4\",\n\t border: exactOutInsufficientSourceIssue\n\t ? \"1px solid #F7C4C1\"\n\t : \"none\",\n\t borderRadius: exactOutInsufficientSourceIssue ? \"4px\" : \"8px\",\n boxSizing: \"border-box\",\n display: \"flex\",\n flexShrink: 0,\n gap: \"8px\",\n height: \"48px\",\n justifyContent: \"center\",\n paddingInline: \"16px\",\n\t cursor: isSendCtaDisabled ? \"default\" : \"pointer\",\n width: \"100%\",\n }}\n >\n {exactOutInsufficientSourceIssue ? (\n \n ) : (needsWalletConnection && walletConnectBusy) ||\n (!sendNeedsRecipient &&\n (quoteRefreshing || 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 />\n \n \n \n \n \n {\n setTxError(null);\n closeDrawerToIdle();\n }}\n aria-label=\"Back\"\n style={{\n alignItems: \"center\",\n backgroundColor: \"#FFFFFE\",\n border: \"1px solid #E8E8E7\",\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 >\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 label={null}\n placeholder=\"Wallet address\"\n hasError={Boolean(txError)}\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 0\n ? sendAmountUsd.toFixed(2)\n : undefined\n }\n selectedTokens={fromTokens}\n editingAssetIndex={editingAssetIndex}\n onSelectionChange={\n activeMode === \"deposit\" || activeMode === \"send\"\n ? (tokens) => {\n setSourceSelectionTouched(true);\n setExactOutQuoteSourceModeValue(\"selected\");\n invalidateExactOutQuoteForRefresh();\n setSourceSelectionRevision((current) => current + 1);\n setFromTokens(\n tokens.map((token) => ({\n ...token,\n userAmount: \"\",\n })),\n );\n }\n : undefined\n }\n onClearSelection={\n activeMode === \"deposit\" || activeMode === \"send\"\n ? () => {\n setSourceSelectionTouched(true);\n setExactOutQuoteSourceModeValue(\"selected\");\n invalidateExactOutQuoteForRefresh();\n setSourceSelectionRevision((current) => current + 1);\n setFromTokens((current) =>\n current.length === 0 ? current : [],\n );\n }\n : undefined\n }\n onToggle={(token) => {\n if (activeMode === \"deposit\" || activeMode === \"send\") {\n setSourceSelectionTouched(true);\n setExactOutQuoteSourceModeValue(\"selected\");\n invalidateExactOutQuoteForRefresh();\n setSourceSelectionRevision((current) => current + 1);\n } else {\n clearPendingSwapIntent();\n }\n setFromTokens((prev) => {\n const isSameSelection = (a: SwapTokenOption, b: SwapTokenOption) => {\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 isDepositOrSendSourcePicker =\n activeMode === \"deposit\" || activeMode === \"send\";\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\" && prev.length === 0\n ? amount\n : \"\",\n });\n\n if (\n isDepositOrSendSourcePicker &&\n token.isUnified &&\n sourceTokens.length > 0\n ) {\n const hasUnifiedSelection = prev.some(isSameUnifiedGroup);\n const areAllChildrenSelected = sourceTokens.every((source) =>\n prev.some((item) => isSameSelection(item, source)),\n );\n const withoutGroup = prev.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) => withDefaultAmount(source)),\n ];\n }\n\n if (isDepositOrSendSourcePicker && !token.isUnified) {\n const unifiedSelection = prev.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 = prev.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 = prev.find((item) =>\n isSameSelection(item, token),\n );\n if (exists) {\n return prev.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 = prev.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 [\n ...next,\n withDefaultAmount(token),\n ];\n });\n }}\n onDone={closeDrawerToIdle}\n onSelect={(token) => {\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 setSourceSelectionTouched(true);\n setExactOutQuoteSourceModeValue(\"selected\");\n invalidateExactOutQuoteForRefresh();\n setSourceSelectionRevision((current) => current + 1);\n setFromTokens([{ ...token, userAmount: amount }]);\n closeDrawerToIdle();\n }\n }}\n onBack={closeDrawerToIdle}\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 (activeMode === \"send\" || activeMode === \"deposit\") {\n setExactOutQuoteSourceModeValue(\"all\");\n if (tokenChanged) {\n clearPendingSwapIntent();\n setAmount(\"\");\n }\n setSwapType(\"exactOut\");\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 onBack={closeDrawerToIdle}\n />\n \n \n )}\n\n \n );\n}\n\nexport default NexusOne;\n", "type": "registry:component", "target": "components/nexus-one/nexus-one.tsx" }, diff --git a/registry/nexus-elements/nexus-one/components/swap-idle-form.tsx b/registry/nexus-elements/nexus-one/components/swap-idle-form.tsx index 10d9bb9..abf2e3e 100644 --- a/registry/nexus-elements/nexus-one/components/swap-idle-form.tsx +++ b/registry/nexus-elements/nexus-one/components/swap-idle-form.tsx @@ -31,6 +31,7 @@ interface SwapIdleFormProps { recipientAddress?: string; defaultRecipientAddress?: string; swapType: "exactIn" | "exactOut"; + allowOverBalanceAmounts?: boolean; onUpdateTokens?: (tokens: SwapTokenOption[]) => void; } @@ -587,6 +588,7 @@ export function SwapIdleForm({ recipientAddress, defaultRecipientAddress, swapType, + allowOverBalanceAmounts = false, onUpdateTokens, }: SwapIdleFormProps) { const [focusedPanel, setFocusedPanel] = useState<"send" | "receive" | null>( @@ -684,7 +686,7 @@ export function SwapIdleForm({ const isUsdMode = token.userAmountMode === "usd"; const maxAmt = isUsdMode ? fiatBalance : tokenBalance; - if (Number(sanitized) > maxAmt) { + if (!allowOverBalanceAmounts && Number(sanitized) > maxAmt) { if (isUsdMode) { sanitized = maxAmt.toFixed(2); } else { diff --git a/registry/nexus-elements/nexus-one/nexus-one.tsx b/registry/nexus-elements/nexus-one/nexus-one.tsx index 49cc265..020dfe4 100644 --- a/registry/nexus-elements/nexus-one/nexus-one.tsx +++ b/registry/nexus-elements/nexus-one/nexus-one.tsx @@ -6133,6 +6133,7 @@ export function NexusOne({ totalBalance={totalSwapBalanceUsd} usdValue={amount && usdValue > 0 ? usdValue.toFixed(2) : ""} swapType={swapType} + allowOverBalanceAmounts={needsWalletConnection} onOpenSourcePicker={(index) => { setEditingAssetIndex(index ?? null); openDrawerStep("choose-swap-asset"); From 1daf2bf343fdc5c4cabd84a013b37bbb85e1acb3 Mon Sep 17 00:00:00 2001 From: shrinathprabhu Date: Tue, 26 May 2026 19:09:17 +0530 Subject: [PATCH 4/6] Update nexus sdk --- package.json | 2 +- pnpm-lock.yaml | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 9147da1..9370c29 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "postinstall": "fumadocs-mdx" }, "dependencies": { - "@avail-project/nexus-core": "github:availproject/nexus-sdk#e1cca4979977bd00e930be9d9ff7c75f17d3a9fd", + "@avail-project/nexus-core": "github:availproject/nexus-sdk#adfb0928feef77fb68ed76aaf3d6911e5c17bc78", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-collapsible": "^1.1.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7c4414..34b53d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,8 +14,8 @@ importers: .: dependencies: '@avail-project/nexus-core': - specifier: github:availproject/nexus-sdk#e1cca4979977bd00e930be9d9ff7c75f17d3a9fd - version: https://codeload.github.com/availproject/nexus-sdk/tar.gz/e1cca4979977bd00e930be9d9ff7c75f17d3a9fd(@opentelemetry/api@1.9.0)(bufferutil@4.1.0)(google-protobuf@3.21.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + specifier: github:availproject/nexus-sdk#adfb0928feef77fb68ed76aaf3d6911e5c17bc78 + version: https://codeload.github.com/availproject/nexus-sdk/tar.gz/adfb0928feef77fb68ed76aaf3d6911e5c17bc78(@opentelemetry/api@1.9.0)(bufferutil@4.1.0)(google-protobuf@3.21.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@radix-ui/react-accordion': specifier: ^1.2.12 version: 1.2.12(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.2.2(react@19.2.2))(react@19.2.2) @@ -201,8 +201,8 @@ packages: msgpackr: ^1.11.4 viem: ^2.31.7 - '@avail-project/nexus-core@https://codeload.github.com/availproject/nexus-sdk/tar.gz/e1cca4979977bd00e930be9d9ff7c75f17d3a9fd': - resolution: {gitHosted: true, integrity: sha512-42RzziIHsC9KV/goOO4F6X2XXvWsKCwLUUMxDwAyh9lkO6QVE7DQzR3nElhQF+94utHbZCBXyGjRODXOHtnTHA==, tarball: https://codeload.github.com/availproject/nexus-sdk/tar.gz/e1cca4979977bd00e930be9d9ff7c75f17d3a9fd} + '@avail-project/nexus-core@https://codeload.github.com/availproject/nexus-sdk/tar.gz/adfb0928feef77fb68ed76aaf3d6911e5c17bc78': + resolution: {gitHosted: true, tarball: https://codeload.github.com/availproject/nexus-sdk/tar.gz/adfb0928feef77fb68ed76aaf3d6911e5c17bc78} version: 1.4.0 engines: {node: '>=18.0.0', npm: '>=9.0.0'} @@ -1404,7 +1404,7 @@ packages: '@paulmillr/qr@0.2.1': resolution: {integrity: sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ==} - deprecated: 'The package is now available as "qr": npm install qr' + deprecated: 'Switch to "qr" (new package name) for security updates: npm install qr' '@posthog/core@1.10.0': resolution: {integrity: sha512-Xk3JQ+cdychsvftrV3G9ZrN9W329lbyFW0pGJXFGKFQf8qr4upw2SgNg9BVorjSrfhoXZRnJGt/uNF4nGFBL5A==} @@ -6961,7 +6961,7 @@ snapshots: transitivePeerDependencies: - google-protobuf - '@avail-project/nexus-core@https://codeload.github.com/availproject/nexus-sdk/tar.gz/e1cca4979977bd00e930be9d9ff7c75f17d3a9fd(@opentelemetry/api@1.9.0)(bufferutil@4.1.0)(google-protobuf@3.21.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@avail-project/nexus-core@https://codeload.github.com/availproject/nexus-sdk/tar.gz/adfb0928feef77fb68ed76aaf3d6911e5c17bc78(@opentelemetry/api@1.9.0)(bufferutil@4.1.0)(google-protobuf@3.21.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@avail-project/ca-common': 2.3.0(@cosmjs/proto-signing@0.34.1)(@cosmjs/stargate@0.34.1(bufferutil@4.1.0)(utf-8-validate@5.0.10))(axios@1.15.0)(decimal.js@10.6.0)(google-protobuf@3.21.4)(long@5.3.2)(msgpackr@1.11.8)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@cosmjs/proto-signing': 0.34.1 @@ -9662,7 +9662,7 @@ snapshots: dependencies: '@babel/runtime': 7.28.6 '@noble/curves': 1.9.7 - '@noble/hashes': 1.4.0 + '@noble/hashes': 1.8.0 '@solana/buffer-layout': 4.0.1 '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) agentkeepalive: 4.6.0 @@ -13788,10 +13788,10 @@ snapshots: ox@0.6.7(typescript@5.9.3)(zod@3.25.76): dependencies: '@adraffy/ens-normalize': 1.11.1 - '@noble/curves': 1.8.1 - '@noble/hashes': 1.7.1 - '@scure/bip32': 1.6.2 - '@scure/bip39': 1.5.4 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 abitype: 1.0.8(typescript@5.9.3)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: From c3fbca5af3361433ef760100e83feb9fe23ea2d2 Mon Sep 17 00:00:00 2001 From: shrinathprabhu Date: Thu, 28 May 2026 17:40:30 +0530 Subject: [PATCH 5/6] Update nexus sdk --- package.json | 2 +- pnpm-lock.yaml | 365 +------------------------------------------- pnpm-workspace.yaml | 1 + 3 files changed, 7 insertions(+), 361 deletions(-) diff --git a/package.json b/package.json index 9370c29..d3ae332 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "postinstall": "fumadocs-mdx" }, "dependencies": { - "@avail-project/nexus-core": "github:availproject/nexus-sdk#adfb0928feef77fb68ed76aaf3d6911e5c17bc78", + "@avail-project/nexus-core": "1.5.0", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-collapsible": "^1.1.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34b53d2..da30710 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,8 +14,8 @@ importers: .: dependencies: '@avail-project/nexus-core': - specifier: github:availproject/nexus-sdk#adfb0928feef77fb68ed76aaf3d6911e5c17bc78 - version: https://codeload.github.com/availproject/nexus-sdk/tar.gz/adfb0928feef77fb68ed76aaf3d6911e5c17bc78(@opentelemetry/api@1.9.0)(bufferutil@4.1.0)(google-protobuf@3.21.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + specifier: 1.5.0 + version: 1.5.0(@opentelemetry/api@1.9.0)(bufferutil@4.1.0)(google-protobuf@3.21.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@radix-ui/react-accordion': specifier: ^1.2.12 version: 1.2.12(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.2.2(react@19.2.2))(react@19.2.2) @@ -176,9 +176,6 @@ importers: packages: - '@adraffy/ens-normalize@1.10.1': - resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} - '@adraffy/ens-normalize@1.11.1': resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} @@ -201,9 +198,8 @@ packages: msgpackr: ^1.11.4 viem: ^2.31.7 - '@avail-project/nexus-core@https://codeload.github.com/availproject/nexus-sdk/tar.gz/adfb0928feef77fb68ed76aaf3d6911e5c17bc78': - resolution: {gitHosted: true, tarball: https://codeload.github.com/availproject/nexus-sdk/tar.gz/adfb0928feef77fb68ed76aaf3d6911e5c17bc78} - version: 1.4.0 + '@avail-project/nexus-core@1.5.0': + resolution: {integrity: sha512-V4jO9NnNxIKErMaTgohf67dZRezY+c2bq09i1bVq0xUPoXNoEDpC8/MNxMM6YMYx9lhPlDlCKefQEU1ztgJ3sA==} engines: {node: '>=18.0.0', npm: '>=9.0.0'} '@babel/code-frame@7.28.6': @@ -311,10 +307,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.26.10': - resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.28.6': resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} @@ -1271,9 +1263,6 @@ packages: resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} engines: {node: ^14.21.3 || >=16} - '@noble/curves@1.2.0': - resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} - '@noble/curves@1.4.2': resolution: {integrity: sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==} @@ -1293,10 +1282,6 @@ packages: resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} engines: {node: ^14.21.3 || >=16} - '@noble/hashes@1.3.2': - resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} - engines: {node: '>= 16'} - '@noble/hashes@1.4.0': resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} @@ -2433,9 +2418,6 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@starkware-industries/starkware-crypto-utils@0.2.1': - resolution: {integrity: sha512-rA5O9b53zaoBOQwQxBd0cbumFbQoBm9NH/vfu+o0Cq3oouEbNPALneLlLjOmFEId2/WOJ5ecC64rFLI/PwuIPQ==} - '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -2542,10 +2524,6 @@ packages: peerDependencies: react: ^18 || ^19 - '@tronweb3/tronwallet-abstract-adapter@1.1.10': - resolution: {integrity: sha512-gZExaEZwPfI9oI7qSi56p/5Zl/DEzo1JlcD3lQzz1cuz0rZmEFpIqDS2mhY4r/IwEPwilIU7lg7T8/RQDVk9gA==} - engines: {node: '>=16', pnpm: '>=7'} - '@ts-morph/common@0.19.0': resolution: {integrity: sha512-Unz/WHmd4pGax91rdIKWi51wnVUW11QttMEPpBiBgIewnc9UQIX7UDLxr5vRlqeByXCwhkF6VabSsI0raWcyAQ==} @@ -2564,9 +2542,6 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - '@types/bn.js@5.2.0': - resolution: {integrity: sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==} - '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -2606,12 +2581,6 @@ packages: '@types/node@20.19.30': resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==} - '@types/node@22.7.5': - resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} - - '@types/pbkdf2@3.1.2': - resolution: {integrity: sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew==} - '@types/react-dom@19.1.2': resolution: {integrity: sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==} peerDependencies: @@ -2620,9 +2589,6 @@ packages: '@types/react@19.1.2': resolution: {integrity: sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==} - '@types/secp256k1@4.0.7': - resolution: {integrity: sha512-Rcvjl6vARGAKRO6jHeKMatGrvOMGrR/AR11N1x2LqintPCyDZ7NBhrh238Z2VZc7aM7KIwnFpFQ7fnfK4H/9Qw==} - '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} @@ -2987,12 +2953,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - aes-js@3.1.2: - resolution: {integrity: sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==} - - aes-js@4.0.0-beta.5: - resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} - agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -3083,9 +3043,6 @@ packages: asn1.js@4.10.1: resolution: {integrity: sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==} - assert@2.1.0: - resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} - ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} @@ -3170,18 +3127,9 @@ packages: big.js@6.2.2: resolution: {integrity: sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==} - bignumber.js@9.1.2: - resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} - - bip39@3.1.0: - resolution: {integrity: sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==} - bl@5.1.0: resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==} - blakejs@1.2.1: - resolution: {integrity: sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==} - bn.js@4.12.2: resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==} @@ -3246,9 +3194,6 @@ packages: bs58@6.0.0: resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} - bs58check@2.1.2: - resolution: {integrity: sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==} - buffer-xor@1.0.3: resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} @@ -3689,9 +3634,6 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - enc-utils@3.0.0: - resolution: {integrity: sha512-e57t/Z2HzWOLwOp7DZcV0VMEY8t7ptWwsxyp6kM2b2zrk6JqIpXxzkruHAMiBsy5wg9jp/183GdiRXCvBtzsYg==} - encode-utf8@1.0.3: resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==} @@ -3962,33 +3904,15 @@ packages: eth-rpc-errors@4.0.3: resolution: {integrity: sha512-Z3ymjopaoft7JDoxZcEb3pwdGh7yiYMhOwm2doUt6ASXlMavpNlK6Cre0+IMl2VSGyEU9rkiperQhp5iRxn5Pg==} - ethereum-cryptography@0.1.3: - resolution: {integrity: sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ==} - ethereum-cryptography@2.2.1: resolution: {integrity: sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==} - ethereumjs-util@7.1.5: - resolution: {integrity: sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg==} - engines: {node: '>=10.0.0'} - - ethereumjs-wallet@1.0.2: - resolution: {integrity: sha512-CCWV4RESJgRdHIvFciVQFnCHfqyhXWchTPlkfp28Qc53ufs+doi5I/cV2+xeK9+qEo25XCWfP9MiL+WEPAZfdA==} - deprecated: 'New package name format for new versions: @ethereumjs/wallet. Please update.' - - ethers@6.13.5: - resolution: {integrity: sha512-+knKNieu5EKRThQJWwqaJ10a6HE9sSehGeqWN65//wE7j47ZpFhKAnHB/JJFibwwg61I/koxaPsXbXpD/skNOQ==} - engines: {node: '>=14.0.0'} - event-iterator@2.0.0: resolution: {integrity: sha512-KGft0ldl31BZVV//jj+IAIGCxkvvUkkON+ScH6zfoX+l+omX6001ggyRSpI0Io2Hlro0ThXotswCtfzS8UkIiQ==} eventemitter2@6.4.9: resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} - eventemitter3@4.0.7: - resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -4582,10 +4506,6 @@ packages: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} - is-nan@1.3.2: - resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} - engines: {node: '>= 0.4'} - is-negative-zero@2.0.3: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} @@ -4652,9 +4572,6 @@ packages: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} - is-typedarray@1.0.0: - resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} - is-unicode-supported@1.3.0: resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} engines: {node: '>=12'} @@ -4722,9 +4639,6 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - js-sha3@0.8.0: - resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -5285,9 +5199,6 @@ packages: node-addon-api@2.0.2: resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==} - node-addon-api@5.1.0: - resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} - node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -5346,10 +5257,6 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} - object-is@1.1.6: - resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} - engines: {node: '>= 0.4'} - object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -5835,9 +5742,6 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} - regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - regex-recursion@5.1.1: resolution: {integrity: sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==} @@ -5932,10 +5836,6 @@ packages: resolution: {integrity: sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==} engines: {node: '>= 0.8'} - rlp@2.2.7: - resolution: {integrity: sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ==} - hasBin: true - router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -5977,13 +5877,6 @@ packages: scroll-into-view-if-needed@3.1.0: resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} - scrypt-js@3.0.1: - resolution: {integrity: sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==} - - secp256k1@4.0.4: - resolution: {integrity: sha512-6JfvwvjUOn8F/jUoBY2Q1v5WY5XS+rj8qSe0v8Y4ezH4InLgTEeOOPQsRll9OV429Pvo6BCHGavIyJfr3TAhsw==} - engines: {node: '>=18.0.0'} - secure-json-parse@4.1.0: resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} @@ -5991,11 +5884,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.1: - resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} - engines: {node: '>=10'} - hasBin: true - semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} @@ -6024,9 +5912,6 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} - setimmediate@1.0.5: - resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} - setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -6344,9 +6229,6 @@ packages: trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} - tronweb@6.1.1: - resolution: {integrity: sha512-9i2N+cTkRY7Y1B/V0+ZVwCYZFhdFDalh8sbI8Tpj5O65hMURvjFnaP1u/dTwVnVw07d9M143/19KarxeAzK6pg==} - trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} @@ -6383,9 +6265,6 @@ packages: tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - tslib@2.7.0: - resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -6425,9 +6304,6 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typedarray-to-buffer@3.1.5: - resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} - typescript-eslint@8.58.1: resolution: {integrity: sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -6456,9 +6332,6 @@ packages: uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -6608,9 +6481,6 @@ packages: resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} engines: {node: '>=6.14.2'} - utf8@3.0.0: - resolution: {integrity: sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==} - util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -6630,10 +6500,6 @@ packages: v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - validator@13.15.23: - resolution: {integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==} - engines: {node: '>= 0.10'} - valtio@1.13.2: resolution: {integrity: sha512-Qik0o+DSy741TmkqmRfjq+0xpZBXi/Y6+fXZLn0xNF1z/waFMbE3rkivv5Zcf9RrMUp6zswf2J7sbh2KBlba5A==} engines: {node: '>=12.20.0'} @@ -6759,18 +6625,6 @@ packages: utf-8-validate: optional: true - ws@8.17.1: - resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -6935,8 +6789,6 @@ packages: snapshots: - '@adraffy/ens-normalize@1.10.1': {} - '@adraffy/ens-normalize@1.11.1': {} '@alloc/quick-lru@5.2.0': {} @@ -6961,7 +6813,7 @@ snapshots: transitivePeerDependencies: - google-protobuf - '@avail-project/nexus-core@https://codeload.github.com/availproject/nexus-sdk/tar.gz/adfb0928feef77fb68ed76aaf3d6911e5c17bc78(@opentelemetry/api@1.9.0)(bufferutil@4.1.0)(google-protobuf@3.21.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@avail-project/nexus-core@1.5.0(@opentelemetry/api@1.9.0)(bufferutil@4.1.0)(google-protobuf@3.21.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@avail-project/ca-common': 2.3.0(@cosmjs/proto-signing@0.34.1)(@cosmjs/stargate@0.34.1(bufferutil@4.1.0)(utf-8-validate@5.0.10))(axios@1.15.0)(decimal.js@10.6.0)(google-protobuf@3.21.4)(long@5.3.2)(msgpackr@1.11.8)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@cosmjs/proto-signing': 0.34.1 @@ -6971,8 +6823,6 @@ snapshots: '@opentelemetry/exporter-logs-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) - '@starkware-industries/starkware-crypto-utils': 0.2.1 - '@tronweb3/tronwallet-abstract-adapter': 1.1.10(bufferutil@4.1.0)(utf-8-validate@5.0.10) axios: 1.15.0 buffer: 6.0.3 decimal.js: 10.6.0 @@ -6981,7 +6831,6 @@ snapshots: long: 5.3.2 msgpackr: 1.11.8 posthog-js: 1.328.0 - tronweb: 6.1.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) tslib: 2.8.1 viem: 2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: @@ -7138,10 +6987,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/runtime@7.26.10': - dependencies: - regenerator-runtime: 0.14.1 - '@babel/runtime@7.28.6': {} '@babel/template@7.28.6': @@ -8155,10 +8000,6 @@ snapshots: '@noble/ciphers@1.3.0': {} - '@noble/curves@1.2.0': - dependencies: - '@noble/hashes': 1.3.2 - '@noble/curves@1.4.2': dependencies: '@noble/hashes': 1.4.0 @@ -8179,8 +8020,6 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 - '@noble/hashes@1.3.2': {} - '@noble/hashes@1.4.0': {} '@noble/hashes@1.7.0': {} @@ -9683,25 +9522,6 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@starkware-industries/starkware-crypto-utils@0.2.1': - dependencies: - assert: 2.1.0 - bip39: 3.1.0 - bn.js: 4.12.2 - brorand: 1.1.0 - buffer: 6.0.3 - crypto-browserify: 3.12.1 - elliptic: 6.6.1 - enc-utils: 3.0.0 - ethereumjs-wallet: 1.0.2 - hash.js: 1.1.7 - hmac-drbg: 1.0.1 - inherits: 2.0.4 - js-sha3: 0.8.0 - minimalistic-assert: 1.0.1 - minimalistic-crypto-utils: 1.0.1 - stream-browserify: 3.0.0 - '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -9786,15 +9606,6 @@ snapshots: '@tanstack/query-core': 5.90.20 react: 19.2.2 - '@tronweb3/tronwallet-abstract-adapter@1.1.10(bufferutil@4.1.0)(utf-8-validate@5.0.10)': - dependencies: - eventemitter3: 4.0.7 - tronweb: 6.1.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - bufferutil - - debug - - utf-8-validate - '@ts-morph/common@0.19.0': dependencies: fast-glob: 3.3.3 @@ -9815,10 +9626,6 @@ snapshots: tslib: 2.8.1 optional: true - '@types/bn.js@5.2.0': - dependencies: - '@types/node': 20.19.30 - '@types/connect@3.4.38': dependencies: '@types/node': 20.19.30 @@ -9857,14 +9664,6 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/node@22.7.5': - dependencies: - undici-types: 6.19.8 - - '@types/pbkdf2@3.1.2': - dependencies: - '@types/node': 20.19.30 - '@types/react-dom@19.1.2(@types/react@19.1.2)': dependencies: '@types/react': 19.1.2 @@ -9873,10 +9672,6 @@ snapshots: dependencies: csstype: 3.2.3 - '@types/secp256k1@4.0.7': - dependencies: - '@types/node': 20.19.30 - '@types/statuses@2.0.6': {} '@types/trusted-types@2.0.7': {} @@ -10697,10 +10492,6 @@ snapshots: acorn@8.15.0: {} - aes-js@3.1.2: {} - - aes-js@4.0.0-beta.5: {} - agent-base@7.1.4: {} agentkeepalive@4.6.0: @@ -10825,14 +10616,6 @@ snapshots: inherits: 2.0.4 minimalistic-assert: 1.0.1 - assert@2.1.0: - dependencies: - call-bind: 1.0.8 - is-nan: 1.3.2 - object-is: 1.1.6 - object.assign: 4.1.7 - util: 0.12.5 - ast-types-flow@0.0.8: {} ast-types@0.16.1: @@ -10906,20 +10689,12 @@ snapshots: big.js@6.2.2: {} - bignumber.js@9.1.2: {} - - bip39@3.1.0: - dependencies: - '@noble/hashes': 1.8.0 - bl@5.1.0: dependencies: buffer: 6.0.3 inherits: 2.0.4 readable-stream: 3.6.2 - blakejs@1.2.1: {} - bn.js@4.12.2: {} bn.js@5.2.2: {} @@ -11023,12 +10798,6 @@ snapshots: dependencies: base-x: 5.0.1 - bs58check@2.1.2: - dependencies: - bs58: 4.0.1 - create-hash: 1.2.0 - safe-buffer: 5.2.1 - buffer-xor@1.0.3: {} buffer@6.0.3: @@ -11454,11 +11223,6 @@ snapshots: emoji-regex@9.2.2: {} - enc-utils@3.0.0: - dependencies: - is-typedarray: 1.0.0 - typedarray-to-buffer: 3.1.5 - encode-utf8@1.0.3: {} encodeurl@2.0.0: {} @@ -11956,24 +11720,6 @@ snapshots: dependencies: fast-safe-stringify: 2.1.1 - ethereum-cryptography@0.1.3: - dependencies: - '@types/pbkdf2': 3.1.2 - '@types/secp256k1': 4.0.7 - blakejs: 1.2.1 - browserify-aes: 1.2.0 - bs58check: 2.1.2 - create-hash: 1.2.0 - create-hmac: 1.1.7 - hash.js: 1.1.7 - keccak: 3.0.4 - pbkdf2: 3.1.5 - randombytes: 2.1.0 - safe-buffer: 5.2.1 - scrypt-js: 3.0.1 - secp256k1: 4.0.4 - setimmediate: 1.0.5 - ethereum-cryptography@2.2.1: dependencies: '@noble/curves': 1.4.2 @@ -11981,44 +11727,10 @@ snapshots: '@scure/bip32': 1.4.0 '@scure/bip39': 1.3.0 - ethereumjs-util@7.1.5: - dependencies: - '@types/bn.js': 5.2.0 - bn.js: 5.2.2 - create-hash: 1.2.0 - ethereum-cryptography: 0.1.3 - rlp: 2.2.7 - - ethereumjs-wallet@1.0.2: - dependencies: - aes-js: 3.1.2 - bs58check: 2.1.2 - ethereum-cryptography: 0.1.3 - ethereumjs-util: 7.1.5 - randombytes: 2.1.0 - scrypt-js: 3.0.1 - utf8: 3.0.0 - uuid: 8.3.2 - - ethers@6.13.5(bufferutil@4.1.0)(utf-8-validate@5.0.10): - dependencies: - '@adraffy/ens-normalize': 1.10.1 - '@noble/curves': 1.2.0 - '@noble/hashes': 1.3.2 - '@types/node': 22.7.5 - aes-js: 4.0.0-beta.5 - tslib: 2.7.0 - ws: 8.17.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - bufferutil - - utf-8-validate - event-iterator@2.0.0: {} eventemitter2@6.4.9: {} - eventemitter3@4.0.7: {} - eventemitter3@5.0.1: {} events@3.3.0: {} @@ -12693,11 +12405,6 @@ snapshots: is-map@2.0.3: {} - is-nan@1.3.2: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - is-negative-zero@2.0.3: {} is-node-process@1.2.0: {} @@ -12751,8 +12458,6 @@ snapshots: dependencies: which-typed-array: 1.1.20 - is-typedarray@1.0.0: {} - is-unicode-supported@1.3.0: {} is-weakmap@2.0.2: {} @@ -12830,8 +12535,6 @@ snapshots: joycon@3.1.1: {} - js-sha3@0.8.0: {} - js-tokens@4.0.0: {} js-yaml@3.14.2: @@ -13622,8 +13325,6 @@ snapshots: node-addon-api@2.0.2: {} - node-addon-api@5.1.0: {} - node-domexception@1.0.0: {} node-fetch-native@1.6.7: {} @@ -13667,11 +13368,6 @@ snapshots: object-inspect@1.13.4: {} - object-is@1.1.6: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - object-keys@1.1.1: {} object.assign@4.1.7: @@ -14286,8 +13982,6 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 - regenerator-runtime@0.14.1: {} - regex-recursion@5.1.1: dependencies: regex: 5.1.1 @@ -14428,10 +14122,6 @@ snapshots: hash-base: 3.1.2 inherits: 2.0.4 - rlp@2.2.7: - dependencies: - bn.js: 5.2.2 - router@2.2.0: dependencies: debug: 4.4.3(supports-color@5.5.0) @@ -14492,20 +14182,10 @@ snapshots: dependencies: compute-scroll-into-view: 3.1.1 - scrypt-js@3.0.1: {} - - secp256k1@4.0.4: - dependencies: - elliptic: 6.6.1 - node-addon-api: 5.1.0 - node-gyp-build: 4.8.4 - secure-json-parse@4.1.0: {} semver@6.3.1: {} - semver@7.7.1: {} - semver@7.7.3: {} send@1.2.1: @@ -14557,8 +14237,6 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 - setimmediate@1.0.5: {} - setprototypeof@1.2.0: {} sha.js@2.4.12: @@ -14966,22 +14644,6 @@ snapshots: trim-lines@3.0.1: {} - tronweb@6.1.1(bufferutil@4.1.0)(utf-8-validate@5.0.10): - dependencies: - '@babel/runtime': 7.26.10 - axios: 1.15.0 - bignumber.js: 9.1.2 - ethereum-cryptography: 2.2.1 - ethers: 6.13.5(bufferutil@4.1.0)(utf-8-validate@5.0.10) - eventemitter3: 5.0.1 - google-protobuf: 3.21.4 - semver: 7.7.1 - validator: 13.15.23 - transitivePeerDependencies: - - bufferutil - - debug - - utf-8-validate - trough@2.2.0: {} ts-api-utils@2.5.0(typescript@5.9.3): @@ -15026,8 +14688,6 @@ snapshots: tslib@1.14.1: {} - tslib@2.7.0: {} - tslib@2.8.1: {} tsx@4.21.0: @@ -15086,10 +14746,6 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typedarray-to-buffer@3.1.5: - dependencies: - is-typedarray: 1.0.0 - typescript-eslint@8.58.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) @@ -15122,8 +14778,6 @@ snapshots: uncrypto@0.1.3: {} - undici-types@6.19.8: {} - undici-types@6.21.0: {} undici-types@7.18.2: {} @@ -15250,8 +14904,6 @@ snapshots: dependencies: node-gyp-build: 4.8.4 - utf8@3.0.0: {} - util-deprecate@1.0.2: {} util@0.12.5: @@ -15268,8 +14920,6 @@ snapshots: v8-compile-cache-lib@3.0.1: {} - validator@13.15.23: {} - valtio@1.13.2(@types/react@19.1.2)(react@19.2.2): dependencies: derive-valtio: 0.1.0(valtio@1.13.2(@types/react@19.1.2)(react@19.2.2)) @@ -15479,11 +15129,6 @@ snapshots: bufferutil: 4.1.0 utf-8-validate: 5.0.10 - ws@8.17.1(bufferutil@4.1.0)(utf-8-validate@5.0.10): - optionalDependencies: - bufferutil: 4.1.0 - utf-8-validate: 5.0.10 - ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10): optionalDependencies: bufferutil: 4.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d6b0d8f..ee2c58d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -22,3 +22,4 @@ overrides: minimumReleaseAgeExclude: - '@avail-project/ca-common@2.3.0' + - '@avail-project/nexus-core@1.5.0' From b8babf9a3dfde3e95456ec4bb50778e5bc8c016f Mon Sep 17 00:00:00 2001 From: shrinathprabhu Date: Thu, 28 May 2026 17:48:05 +0530 Subject: [PATCH 6/6] remove tron data --- public/r/bridge-deposit.json | 2 +- public/r/deposit.json | 2 +- public/r/fast-bridge.json | 2 +- public/r/nexus-one.json | 2 +- public/r/swaps.json | 2 +- public/r/transfer.json | 2 +- public/r/view-history.json | 2 +- registry/nexus-elements/common/hooks/useNexusError.ts | 7 +------ 8 files changed, 8 insertions(+), 13 deletions(-) diff --git a/public/r/bridge-deposit.json b/public/r/bridge-deposit.json index 12be655..6253ad5 100644 --- a/public/r/bridge-deposit.json +++ b/public/r/bridge-deposit.json @@ -136,7 +136,7 @@ }, { "path": "registry/nexus-elements/common/hooks/useNexusError.ts", - "content": "import { ERROR_CODES, NexusError } from \"@avail-project/nexus-core\";\n\nconst DEFAULT_ERROR_MESSAGE = \"Oops! Something went wrong. Please try again.\";\nconst USER_REJECTED_MESSAGE = \"Transaction was rejected in your wallet.\";\nconst EMPTY_ERROR_MESSAGE =\n \"Unable to determine transaction state. Please refresh and try again.\";\n\nconst ERROR_MESSAGE_BY_CODE: Partial> = {\n [ERROR_CODES.INVALID_VALUES_ALLOWANCE_HOOK]:\n \"Invalid allowance selection. Please review allowance values and try again.\",\n [ERROR_CODES.SDK_NOT_INITIALIZED]:\n \"Nexus SDK is not initialized. Reconnect your wallet and try again.\",\n [ERROR_CODES.SDK_INIT_STATE_NOT_EXPECTED]:\n \"Nexus is still initializing. Please wait a few seconds and retry.\",\n [ERROR_CODES.CHAIN_NOT_FOUND]:\n \"Selected chain is not supported for this route.\",\n [ERROR_CODES.CHAIN_DATA_NOT_FOUND]:\n \"Chain metadata is unavailable for this route. Please try another chain.\",\n [ERROR_CODES.ASSET_NOT_FOUND]:\n \"Requested asset was not found in your balances.\",\n [ERROR_CODES.COSMOS_ERROR]:\n \"Cosmos-side operation failed. Please retry in a moment.\",\n [ERROR_CODES.TOKEN_NOT_SUPPORTED]:\n \"Selected token is not supported for this route.\",\n [ERROR_CODES.UNIVERSE_NOT_SUPPORTED]:\n \"Selected chain universe is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_SUPPORTED]:\n \"Selected environment is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_KNOWN]:\n \"Selected environment is not recognized.\",\n [ERROR_CODES.UNKNOWN_SIGNATURE]:\n \"Unsupported signature type for this transaction.\",\n [ERROR_CODES.TRON_DEPOSIT_FAIL]:\n \"TRON deposit transaction failed. Please retry.\",\n [ERROR_CODES.TRON_APPROVAL_FAIL]:\n \"TRON approval transaction failed. Please retry.\",\n [ERROR_CODES.LIQUIDITY_TIMEOUT]:\n \"Timed out waiting for liquidity. Please retry.\",\n [ERROR_CODES.USER_DENIED_INTENT]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_ALLOWANCE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_INTENT_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_SIWE_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.INSUFFICIENT_BALANCE]: \"Insufficient balance to proceed.\",\n [ERROR_CODES.WALLET_NOT_CONNECTED]:\n \"Wallet is not connected. Connect your wallet and try again.\",\n [ERROR_CODES.FETCH_GAS_PRICE_FAILED]:\n \"Unable to estimate gas right now. Please retry.\",\n [ERROR_CODES.SIMULATION_FAILED]:\n \"Simulation failed. Please review your inputs and try again.\",\n [ERROR_CODES.QUOTE_FAILED]:\n \"Unable to fetch a quote right now. Please retry.\",\n [ERROR_CODES.SWAP_FAILED]: \"Swap execution failed. Please retry.\",\n [ERROR_CODES.VAULT_CONTRACT_NOT_FOUND]:\n \"Required vault contract is unavailable on this chain.\",\n [ERROR_CODES.SLIPPAGE_EXCEEDED_ALLOWANCE]:\n \"Slippage exceeded tolerance. Refresh quote and retry.\",\n [ERROR_CODES.RATES_CHANGED_BEYOND_TOLERANCE]:\n \"Rates changed beyond tolerance. Review and retry.\",\n [ERROR_CODES.RFF_FEE_EXPIRED]:\n \"Quote expired. Refresh and try again.\",\n [ERROR_CODES.INVALID_INPUT]:\n \"Some transaction inputs are invalid. Please review and try again.\",\n [ERROR_CODES.INVALID_ADDRESS_LENGTH]:\n \"Address format is invalid for the selected chain.\",\n [ERROR_CODES.NO_BALANCE_FOR_ADDRESS]:\n \"No balance found for this wallet on supported source chains.\",\n [ERROR_CODES.TRANSACTION_TIMEOUT]:\n \"Transaction is taking longer than expected. Check your wallet and explorer.\",\n [ERROR_CODES.TRANSACTION_REVERTED]:\n \"Transaction reverted on-chain. Please verify inputs and retry.\",\n [ERROR_CODES.DESTINATION_REQUEST_HASH_NOT_FOUND]:\n \"Could not finalize destination request. Please retry.\",\n};\n\nfunction isRecord(value: unknown): value is Record {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction getErrorMessage(value: unknown): string | undefined {\n if (!isRecord(value)) return undefined;\n const message = value.message;\n return typeof message === \"string\" ? message : undefined;\n}\n\nfunction getErrorCode(value: unknown): string | number | undefined {\n if (!isRecord(value)) return undefined;\n const code = value.code;\n if (typeof code === \"string\" || typeof code === \"number\") {\n return code;\n }\n return undefined;\n}\n\nfunction looksLikeUserRejection(err: unknown): boolean {\n if (err instanceof NexusError) {\n return (\n err.code === ERROR_CODES.USER_DENIED_ALLOWANCE ||\n err.code === ERROR_CODES.USER_DENIED_INTENT ||\n err.code === ERROR_CODES.USER_DENIED_INTENT_SIGNATURE ||\n err.code === ERROR_CODES.USER_DENIED_SIWE_SIGNATURE\n );\n }\n\n const code = getErrorCode(err);\n if (code === 4001 || code === \"ACTION_REJECTED\") {\n return true;\n }\n\n const message = getErrorMessage(err)?.toLowerCase();\n if (!message) return false;\n return (\n message.includes(\"user denied\") ||\n message.includes(\"user rejected\") ||\n message.includes(\"rejected request\") ||\n message.includes(\"denied transaction signature\")\n );\n}\n\nfunction sanitizeMessage(message?: string): string {\n if (!message) return DEFAULT_ERROR_MESSAGE;\n const cleaned = message\n .replace(/^Internal error:\\s*/i, \"\")\n .replace(/^COSMOS:\\s*/i, \"\")\n .trim();\n return cleaned || DEFAULT_ERROR_MESSAGE;\n}\n\nfunction handler(err: unknown) {\n if (err === null || err === undefined) {\n console.error(\"Unexpected empty error from Nexus SDK:\", err);\n return {\n code: \"unexpected_error\",\n message: EMPTY_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (looksLikeUserRejection(err)) {\n return {\n code: ERROR_CODES.USER_DENIED_INTENT,\n message: USER_REJECTED_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (err instanceof NexusError) {\n const mappedMessage =\n ERROR_MESSAGE_BY_CODE[err.code] ?? sanitizeMessage(err.message);\n return {\n code: err.code,\n message: mappedMessage,\n context: err?.data?.context,\n details: err?.data?.details,\n };\n }\n\n const unknownMessage = sanitizeMessage(getErrorMessage(err));\n console.error(\"Unexpected error:\", err);\n return {\n code: String(getErrorCode(err) ?? \"unexpected_error\"),\n message: unknownMessage || DEFAULT_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n}\n\nexport function useNexusError() {\n return handler;\n}\n", + "content": "import { ERROR_CODES, NexusError } from \"@avail-project/nexus-core\";\n\nconst DEFAULT_ERROR_MESSAGE = \"Oops! Something went wrong. Please try again.\";\nconst USER_REJECTED_MESSAGE = \"Transaction was rejected in your wallet.\";\nconst EMPTY_ERROR_MESSAGE =\n \"Unable to determine transaction state. Please refresh and try again.\";\n\nconst ERROR_MESSAGE_BY_CODE: Partial> = {\n [ERROR_CODES.INVALID_VALUES_ALLOWANCE_HOOK]:\n \"Invalid allowance selection. Please review allowance values and try again.\",\n [ERROR_CODES.SDK_NOT_INITIALIZED]:\n \"Nexus SDK is not initialized. Reconnect your wallet and try again.\",\n [ERROR_CODES.SDK_INIT_STATE_NOT_EXPECTED]:\n \"Nexus is still initializing. Please wait a few seconds and retry.\",\n [ERROR_CODES.CHAIN_NOT_FOUND]:\n \"Selected chain is not supported for this route.\",\n [ERROR_CODES.CHAIN_DATA_NOT_FOUND]:\n \"Chain metadata is unavailable for this route. Please try another chain.\",\n [ERROR_CODES.ASSET_NOT_FOUND]:\n \"Requested asset was not found in your balances.\",\n [ERROR_CODES.COSMOS_ERROR]:\n \"Cosmos-side operation failed. Please retry in a moment.\",\n [ERROR_CODES.TOKEN_NOT_SUPPORTED]:\n \"Selected token is not supported for this route.\",\n [ERROR_CODES.UNIVERSE_NOT_SUPPORTED]:\n \"Selected chain universe is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_SUPPORTED]:\n \"Selected environment is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_KNOWN]:\n \"Selected environment is not recognized.\",\n [ERROR_CODES.UNKNOWN_SIGNATURE]:\n \"Unsupported signature type for this transaction.\",\n [ERROR_CODES.LIQUIDITY_TIMEOUT]:\n \"Timed out waiting for liquidity. Please retry.\",\n [ERROR_CODES.USER_DENIED_INTENT]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_ALLOWANCE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_INTENT_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_SIWE_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.INSUFFICIENT_BALANCE]: \"Insufficient balance to proceed.\",\n [ERROR_CODES.WALLET_NOT_CONNECTED]:\n \"Wallet is not connected. Connect your wallet and try again.\",\n [ERROR_CODES.FETCH_GAS_PRICE_FAILED]:\n \"Unable to estimate gas right now. Please retry.\",\n [ERROR_CODES.SIMULATION_FAILED]:\n \"Simulation failed. Please review your inputs and try again.\",\n [ERROR_CODES.QUOTE_FAILED]:\n \"Unable to fetch a quote right now. Please retry.\",\n [ERROR_CODES.SWAP_FAILED]: \"Swap execution failed. Please retry.\",\n [ERROR_CODES.VAULT_CONTRACT_NOT_FOUND]:\n \"Required vault contract is unavailable on this chain.\",\n [ERROR_CODES.SLIPPAGE_EXCEEDED_ALLOWANCE]:\n \"Slippage exceeded tolerance. Refresh quote and retry.\",\n [ERROR_CODES.RATES_CHANGED_BEYOND_TOLERANCE]:\n \"Rates changed beyond tolerance. Review and retry.\",\n [ERROR_CODES.RFF_FEE_EXPIRED]: \"Quote expired. Refresh and try again.\",\n [ERROR_CODES.INVALID_INPUT]:\n \"Some transaction inputs are invalid. Please review and try again.\",\n [ERROR_CODES.INVALID_ADDRESS_LENGTH]:\n \"Address format is invalid for the selected chain.\",\n [ERROR_CODES.NO_BALANCE_FOR_ADDRESS]:\n \"No balance found for this wallet on supported source chains.\",\n [ERROR_CODES.TRANSACTION_TIMEOUT]:\n \"Transaction is taking longer than expected. Check your wallet and explorer.\",\n [ERROR_CODES.TRANSACTION_REVERTED]:\n \"Transaction reverted on-chain. Please verify inputs and retry.\",\n [ERROR_CODES.DESTINATION_REQUEST_HASH_NOT_FOUND]:\n \"Could not finalize destination request. Please retry.\",\n};\n\nfunction isRecord(value: unknown): value is Record {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction getErrorMessage(value: unknown): string | undefined {\n if (!isRecord(value)) return undefined;\n const message = value.message;\n return typeof message === \"string\" ? message : undefined;\n}\n\nfunction getErrorCode(value: unknown): string | number | undefined {\n if (!isRecord(value)) return undefined;\n const code = value.code;\n if (typeof code === \"string\" || typeof code === \"number\") {\n return code;\n }\n return undefined;\n}\n\nfunction looksLikeUserRejection(err: unknown): boolean {\n if (err instanceof NexusError) {\n return (\n err.code === ERROR_CODES.USER_DENIED_ALLOWANCE ||\n err.code === ERROR_CODES.USER_DENIED_INTENT ||\n err.code === ERROR_CODES.USER_DENIED_INTENT_SIGNATURE ||\n err.code === ERROR_CODES.USER_DENIED_SIWE_SIGNATURE\n );\n }\n\n const code = getErrorCode(err);\n if (code === 4001 || code === \"ACTION_REJECTED\") {\n return true;\n }\n\n const message = getErrorMessage(err)?.toLowerCase();\n if (!message) return false;\n return (\n message.includes(\"user denied\") ||\n message.includes(\"user rejected\") ||\n message.includes(\"rejected request\") ||\n message.includes(\"denied transaction signature\")\n );\n}\n\nfunction sanitizeMessage(message?: string): string {\n if (!message) return DEFAULT_ERROR_MESSAGE;\n const cleaned = message\n .replace(/^Internal error:\\s*/i, \"\")\n .replace(/^COSMOS:\\s*/i, \"\")\n .trim();\n return cleaned || DEFAULT_ERROR_MESSAGE;\n}\n\nfunction handler(err: unknown) {\n if (err === null || err === undefined) {\n console.error(\"Unexpected empty error from Nexus SDK:\", err);\n return {\n code: \"unexpected_error\",\n message: EMPTY_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (looksLikeUserRejection(err)) {\n return {\n code: ERROR_CODES.USER_DENIED_INTENT,\n message: USER_REJECTED_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (err instanceof NexusError) {\n const mappedMessage =\n ERROR_MESSAGE_BY_CODE[err.code] ?? sanitizeMessage(err.message);\n return {\n code: err.code,\n message: mappedMessage,\n context: err?.data?.context,\n details: err?.data?.details,\n };\n }\n\n const unknownMessage = sanitizeMessage(getErrorMessage(err));\n console.error(\"Unexpected error:\", err);\n return {\n code: String(getErrorCode(err) ?? \"unexpected_error\"),\n message: unknownMessage || DEFAULT_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n}\n\nexport function useNexusError() {\n return handler;\n}\n", "type": "registry:component", "target": "components/common/hooks/useNexusError.ts" }, diff --git a/public/r/deposit.json b/public/r/deposit.json index b833cf9..135a0f5 100644 --- a/public/r/deposit.json +++ b/public/r/deposit.json @@ -249,7 +249,7 @@ }, { "path": "registry/nexus-elements/common/hooks/useNexusError.ts", - "content": "import { ERROR_CODES, NexusError } from \"@avail-project/nexus-core\";\n\nconst DEFAULT_ERROR_MESSAGE = \"Oops! Something went wrong. Please try again.\";\nconst USER_REJECTED_MESSAGE = \"Transaction was rejected in your wallet.\";\nconst EMPTY_ERROR_MESSAGE =\n \"Unable to determine transaction state. Please refresh and try again.\";\n\nconst ERROR_MESSAGE_BY_CODE: Partial> = {\n [ERROR_CODES.INVALID_VALUES_ALLOWANCE_HOOK]:\n \"Invalid allowance selection. Please review allowance values and try again.\",\n [ERROR_CODES.SDK_NOT_INITIALIZED]:\n \"Nexus SDK is not initialized. Reconnect your wallet and try again.\",\n [ERROR_CODES.SDK_INIT_STATE_NOT_EXPECTED]:\n \"Nexus is still initializing. Please wait a few seconds and retry.\",\n [ERROR_CODES.CHAIN_NOT_FOUND]:\n \"Selected chain is not supported for this route.\",\n [ERROR_CODES.CHAIN_DATA_NOT_FOUND]:\n \"Chain metadata is unavailable for this route. Please try another chain.\",\n [ERROR_CODES.ASSET_NOT_FOUND]:\n \"Requested asset was not found in your balances.\",\n [ERROR_CODES.COSMOS_ERROR]:\n \"Cosmos-side operation failed. Please retry in a moment.\",\n [ERROR_CODES.TOKEN_NOT_SUPPORTED]:\n \"Selected token is not supported for this route.\",\n [ERROR_CODES.UNIVERSE_NOT_SUPPORTED]:\n \"Selected chain universe is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_SUPPORTED]:\n \"Selected environment is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_KNOWN]:\n \"Selected environment is not recognized.\",\n [ERROR_CODES.UNKNOWN_SIGNATURE]:\n \"Unsupported signature type for this transaction.\",\n [ERROR_CODES.TRON_DEPOSIT_FAIL]:\n \"TRON deposit transaction failed. Please retry.\",\n [ERROR_CODES.TRON_APPROVAL_FAIL]:\n \"TRON approval transaction failed. Please retry.\",\n [ERROR_CODES.LIQUIDITY_TIMEOUT]:\n \"Timed out waiting for liquidity. Please retry.\",\n [ERROR_CODES.USER_DENIED_INTENT]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_ALLOWANCE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_INTENT_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_SIWE_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.INSUFFICIENT_BALANCE]: \"Insufficient balance to proceed.\",\n [ERROR_CODES.WALLET_NOT_CONNECTED]:\n \"Wallet is not connected. Connect your wallet and try again.\",\n [ERROR_CODES.FETCH_GAS_PRICE_FAILED]:\n \"Unable to estimate gas right now. Please retry.\",\n [ERROR_CODES.SIMULATION_FAILED]:\n \"Simulation failed. Please review your inputs and try again.\",\n [ERROR_CODES.QUOTE_FAILED]:\n \"Unable to fetch a quote right now. Please retry.\",\n [ERROR_CODES.SWAP_FAILED]: \"Swap execution failed. Please retry.\",\n [ERROR_CODES.VAULT_CONTRACT_NOT_FOUND]:\n \"Required vault contract is unavailable on this chain.\",\n [ERROR_CODES.SLIPPAGE_EXCEEDED_ALLOWANCE]:\n \"Slippage exceeded tolerance. Refresh quote and retry.\",\n [ERROR_CODES.RATES_CHANGED_BEYOND_TOLERANCE]:\n \"Rates changed beyond tolerance. Review and retry.\",\n [ERROR_CODES.RFF_FEE_EXPIRED]:\n \"Quote expired. Refresh and try again.\",\n [ERROR_CODES.INVALID_INPUT]:\n \"Some transaction inputs are invalid. Please review and try again.\",\n [ERROR_CODES.INVALID_ADDRESS_LENGTH]:\n \"Address format is invalid for the selected chain.\",\n [ERROR_CODES.NO_BALANCE_FOR_ADDRESS]:\n \"No balance found for this wallet on supported source chains.\",\n [ERROR_CODES.TRANSACTION_TIMEOUT]:\n \"Transaction is taking longer than expected. Check your wallet and explorer.\",\n [ERROR_CODES.TRANSACTION_REVERTED]:\n \"Transaction reverted on-chain. Please verify inputs and retry.\",\n [ERROR_CODES.DESTINATION_REQUEST_HASH_NOT_FOUND]:\n \"Could not finalize destination request. Please retry.\",\n};\n\nfunction isRecord(value: unknown): value is Record {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction getErrorMessage(value: unknown): string | undefined {\n if (!isRecord(value)) return undefined;\n const message = value.message;\n return typeof message === \"string\" ? message : undefined;\n}\n\nfunction getErrorCode(value: unknown): string | number | undefined {\n if (!isRecord(value)) return undefined;\n const code = value.code;\n if (typeof code === \"string\" || typeof code === \"number\") {\n return code;\n }\n return undefined;\n}\n\nfunction looksLikeUserRejection(err: unknown): boolean {\n if (err instanceof NexusError) {\n return (\n err.code === ERROR_CODES.USER_DENIED_ALLOWANCE ||\n err.code === ERROR_CODES.USER_DENIED_INTENT ||\n err.code === ERROR_CODES.USER_DENIED_INTENT_SIGNATURE ||\n err.code === ERROR_CODES.USER_DENIED_SIWE_SIGNATURE\n );\n }\n\n const code = getErrorCode(err);\n if (code === 4001 || code === \"ACTION_REJECTED\") {\n return true;\n }\n\n const message = getErrorMessage(err)?.toLowerCase();\n if (!message) return false;\n return (\n message.includes(\"user denied\") ||\n message.includes(\"user rejected\") ||\n message.includes(\"rejected request\") ||\n message.includes(\"denied transaction signature\")\n );\n}\n\nfunction sanitizeMessage(message?: string): string {\n if (!message) return DEFAULT_ERROR_MESSAGE;\n const cleaned = message\n .replace(/^Internal error:\\s*/i, \"\")\n .replace(/^COSMOS:\\s*/i, \"\")\n .trim();\n return cleaned || DEFAULT_ERROR_MESSAGE;\n}\n\nfunction handler(err: unknown) {\n if (err === null || err === undefined) {\n console.error(\"Unexpected empty error from Nexus SDK:\", err);\n return {\n code: \"unexpected_error\",\n message: EMPTY_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (looksLikeUserRejection(err)) {\n return {\n code: ERROR_CODES.USER_DENIED_INTENT,\n message: USER_REJECTED_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (err instanceof NexusError) {\n const mappedMessage =\n ERROR_MESSAGE_BY_CODE[err.code] ?? sanitizeMessage(err.message);\n return {\n code: err.code,\n message: mappedMessage,\n context: err?.data?.context,\n details: err?.data?.details,\n };\n }\n\n const unknownMessage = sanitizeMessage(getErrorMessage(err));\n console.error(\"Unexpected error:\", err);\n return {\n code: String(getErrorCode(err) ?? \"unexpected_error\"),\n message: unknownMessage || DEFAULT_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n}\n\nexport function useNexusError() {\n return handler;\n}\n", + "content": "import { ERROR_CODES, NexusError } from \"@avail-project/nexus-core\";\n\nconst DEFAULT_ERROR_MESSAGE = \"Oops! Something went wrong. Please try again.\";\nconst USER_REJECTED_MESSAGE = \"Transaction was rejected in your wallet.\";\nconst EMPTY_ERROR_MESSAGE =\n \"Unable to determine transaction state. Please refresh and try again.\";\n\nconst ERROR_MESSAGE_BY_CODE: Partial> = {\n [ERROR_CODES.INVALID_VALUES_ALLOWANCE_HOOK]:\n \"Invalid allowance selection. Please review allowance values and try again.\",\n [ERROR_CODES.SDK_NOT_INITIALIZED]:\n \"Nexus SDK is not initialized. Reconnect your wallet and try again.\",\n [ERROR_CODES.SDK_INIT_STATE_NOT_EXPECTED]:\n \"Nexus is still initializing. Please wait a few seconds and retry.\",\n [ERROR_CODES.CHAIN_NOT_FOUND]:\n \"Selected chain is not supported for this route.\",\n [ERROR_CODES.CHAIN_DATA_NOT_FOUND]:\n \"Chain metadata is unavailable for this route. Please try another chain.\",\n [ERROR_CODES.ASSET_NOT_FOUND]:\n \"Requested asset was not found in your balances.\",\n [ERROR_CODES.COSMOS_ERROR]:\n \"Cosmos-side operation failed. Please retry in a moment.\",\n [ERROR_CODES.TOKEN_NOT_SUPPORTED]:\n \"Selected token is not supported for this route.\",\n [ERROR_CODES.UNIVERSE_NOT_SUPPORTED]:\n \"Selected chain universe is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_SUPPORTED]:\n \"Selected environment is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_KNOWN]:\n \"Selected environment is not recognized.\",\n [ERROR_CODES.UNKNOWN_SIGNATURE]:\n \"Unsupported signature type for this transaction.\",\n [ERROR_CODES.LIQUIDITY_TIMEOUT]:\n \"Timed out waiting for liquidity. Please retry.\",\n [ERROR_CODES.USER_DENIED_INTENT]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_ALLOWANCE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_INTENT_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_SIWE_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.INSUFFICIENT_BALANCE]: \"Insufficient balance to proceed.\",\n [ERROR_CODES.WALLET_NOT_CONNECTED]:\n \"Wallet is not connected. Connect your wallet and try again.\",\n [ERROR_CODES.FETCH_GAS_PRICE_FAILED]:\n \"Unable to estimate gas right now. Please retry.\",\n [ERROR_CODES.SIMULATION_FAILED]:\n \"Simulation failed. Please review your inputs and try again.\",\n [ERROR_CODES.QUOTE_FAILED]:\n \"Unable to fetch a quote right now. Please retry.\",\n [ERROR_CODES.SWAP_FAILED]: \"Swap execution failed. Please retry.\",\n [ERROR_CODES.VAULT_CONTRACT_NOT_FOUND]:\n \"Required vault contract is unavailable on this chain.\",\n [ERROR_CODES.SLIPPAGE_EXCEEDED_ALLOWANCE]:\n \"Slippage exceeded tolerance. Refresh quote and retry.\",\n [ERROR_CODES.RATES_CHANGED_BEYOND_TOLERANCE]:\n \"Rates changed beyond tolerance. Review and retry.\",\n [ERROR_CODES.RFF_FEE_EXPIRED]: \"Quote expired. Refresh and try again.\",\n [ERROR_CODES.INVALID_INPUT]:\n \"Some transaction inputs are invalid. Please review and try again.\",\n [ERROR_CODES.INVALID_ADDRESS_LENGTH]:\n \"Address format is invalid for the selected chain.\",\n [ERROR_CODES.NO_BALANCE_FOR_ADDRESS]:\n \"No balance found for this wallet on supported source chains.\",\n [ERROR_CODES.TRANSACTION_TIMEOUT]:\n \"Transaction is taking longer than expected. Check your wallet and explorer.\",\n [ERROR_CODES.TRANSACTION_REVERTED]:\n \"Transaction reverted on-chain. Please verify inputs and retry.\",\n [ERROR_CODES.DESTINATION_REQUEST_HASH_NOT_FOUND]:\n \"Could not finalize destination request. Please retry.\",\n};\n\nfunction isRecord(value: unknown): value is Record {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction getErrorMessage(value: unknown): string | undefined {\n if (!isRecord(value)) return undefined;\n const message = value.message;\n return typeof message === \"string\" ? message : undefined;\n}\n\nfunction getErrorCode(value: unknown): string | number | undefined {\n if (!isRecord(value)) return undefined;\n const code = value.code;\n if (typeof code === \"string\" || typeof code === \"number\") {\n return code;\n }\n return undefined;\n}\n\nfunction looksLikeUserRejection(err: unknown): boolean {\n if (err instanceof NexusError) {\n return (\n err.code === ERROR_CODES.USER_DENIED_ALLOWANCE ||\n err.code === ERROR_CODES.USER_DENIED_INTENT ||\n err.code === ERROR_CODES.USER_DENIED_INTENT_SIGNATURE ||\n err.code === ERROR_CODES.USER_DENIED_SIWE_SIGNATURE\n );\n }\n\n const code = getErrorCode(err);\n if (code === 4001 || code === \"ACTION_REJECTED\") {\n return true;\n }\n\n const message = getErrorMessage(err)?.toLowerCase();\n if (!message) return false;\n return (\n message.includes(\"user denied\") ||\n message.includes(\"user rejected\") ||\n message.includes(\"rejected request\") ||\n message.includes(\"denied transaction signature\")\n );\n}\n\nfunction sanitizeMessage(message?: string): string {\n if (!message) return DEFAULT_ERROR_MESSAGE;\n const cleaned = message\n .replace(/^Internal error:\\s*/i, \"\")\n .replace(/^COSMOS:\\s*/i, \"\")\n .trim();\n return cleaned || DEFAULT_ERROR_MESSAGE;\n}\n\nfunction handler(err: unknown) {\n if (err === null || err === undefined) {\n console.error(\"Unexpected empty error from Nexus SDK:\", err);\n return {\n code: \"unexpected_error\",\n message: EMPTY_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (looksLikeUserRejection(err)) {\n return {\n code: ERROR_CODES.USER_DENIED_INTENT,\n message: USER_REJECTED_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (err instanceof NexusError) {\n const mappedMessage =\n ERROR_MESSAGE_BY_CODE[err.code] ?? sanitizeMessage(err.message);\n return {\n code: err.code,\n message: mappedMessage,\n context: err?.data?.context,\n details: err?.data?.details,\n };\n }\n\n const unknownMessage = sanitizeMessage(getErrorMessage(err));\n console.error(\"Unexpected error:\", err);\n return {\n code: String(getErrorCode(err) ?? \"unexpected_error\"),\n message: unknownMessage || DEFAULT_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n}\n\nexport function useNexusError() {\n return handler;\n}\n", "type": "registry:component", "target": "components/common/hooks/useNexusError.ts" }, diff --git a/public/r/fast-bridge.json b/public/r/fast-bridge.json index 02218ec..778292d 100644 --- a/public/r/fast-bridge.json +++ b/public/r/fast-bridge.json @@ -117,7 +117,7 @@ }, { "path": "registry/nexus-elements/common/hooks/useNexusError.ts", - "content": "import { ERROR_CODES, NexusError } from \"@avail-project/nexus-core\";\n\nconst DEFAULT_ERROR_MESSAGE = \"Oops! Something went wrong. Please try again.\";\nconst USER_REJECTED_MESSAGE = \"Transaction was rejected in your wallet.\";\nconst EMPTY_ERROR_MESSAGE =\n \"Unable to determine transaction state. Please refresh and try again.\";\n\nconst ERROR_MESSAGE_BY_CODE: Partial> = {\n [ERROR_CODES.INVALID_VALUES_ALLOWANCE_HOOK]:\n \"Invalid allowance selection. Please review allowance values and try again.\",\n [ERROR_CODES.SDK_NOT_INITIALIZED]:\n \"Nexus SDK is not initialized. Reconnect your wallet and try again.\",\n [ERROR_CODES.SDK_INIT_STATE_NOT_EXPECTED]:\n \"Nexus is still initializing. Please wait a few seconds and retry.\",\n [ERROR_CODES.CHAIN_NOT_FOUND]:\n \"Selected chain is not supported for this route.\",\n [ERROR_CODES.CHAIN_DATA_NOT_FOUND]:\n \"Chain metadata is unavailable for this route. Please try another chain.\",\n [ERROR_CODES.ASSET_NOT_FOUND]:\n \"Requested asset was not found in your balances.\",\n [ERROR_CODES.COSMOS_ERROR]:\n \"Cosmos-side operation failed. Please retry in a moment.\",\n [ERROR_CODES.TOKEN_NOT_SUPPORTED]:\n \"Selected token is not supported for this route.\",\n [ERROR_CODES.UNIVERSE_NOT_SUPPORTED]:\n \"Selected chain universe is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_SUPPORTED]:\n \"Selected environment is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_KNOWN]:\n \"Selected environment is not recognized.\",\n [ERROR_CODES.UNKNOWN_SIGNATURE]:\n \"Unsupported signature type for this transaction.\",\n [ERROR_CODES.TRON_DEPOSIT_FAIL]:\n \"TRON deposit transaction failed. Please retry.\",\n [ERROR_CODES.TRON_APPROVAL_FAIL]:\n \"TRON approval transaction failed. Please retry.\",\n [ERROR_CODES.LIQUIDITY_TIMEOUT]:\n \"Timed out waiting for liquidity. Please retry.\",\n [ERROR_CODES.USER_DENIED_INTENT]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_ALLOWANCE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_INTENT_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_SIWE_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.INSUFFICIENT_BALANCE]: \"Insufficient balance to proceed.\",\n [ERROR_CODES.WALLET_NOT_CONNECTED]:\n \"Wallet is not connected. Connect your wallet and try again.\",\n [ERROR_CODES.FETCH_GAS_PRICE_FAILED]:\n \"Unable to estimate gas right now. Please retry.\",\n [ERROR_CODES.SIMULATION_FAILED]:\n \"Simulation failed. Please review your inputs and try again.\",\n [ERROR_CODES.QUOTE_FAILED]:\n \"Unable to fetch a quote right now. Please retry.\",\n [ERROR_CODES.SWAP_FAILED]: \"Swap execution failed. Please retry.\",\n [ERROR_CODES.VAULT_CONTRACT_NOT_FOUND]:\n \"Required vault contract is unavailable on this chain.\",\n [ERROR_CODES.SLIPPAGE_EXCEEDED_ALLOWANCE]:\n \"Slippage exceeded tolerance. Refresh quote and retry.\",\n [ERROR_CODES.RATES_CHANGED_BEYOND_TOLERANCE]:\n \"Rates changed beyond tolerance. Review and retry.\",\n [ERROR_CODES.RFF_FEE_EXPIRED]:\n \"Quote expired. Refresh and try again.\",\n [ERROR_CODES.INVALID_INPUT]:\n \"Some transaction inputs are invalid. Please review and try again.\",\n [ERROR_CODES.INVALID_ADDRESS_LENGTH]:\n \"Address format is invalid for the selected chain.\",\n [ERROR_CODES.NO_BALANCE_FOR_ADDRESS]:\n \"No balance found for this wallet on supported source chains.\",\n [ERROR_CODES.TRANSACTION_TIMEOUT]:\n \"Transaction is taking longer than expected. Check your wallet and explorer.\",\n [ERROR_CODES.TRANSACTION_REVERTED]:\n \"Transaction reverted on-chain. Please verify inputs and retry.\",\n [ERROR_CODES.DESTINATION_REQUEST_HASH_NOT_FOUND]:\n \"Could not finalize destination request. Please retry.\",\n};\n\nfunction isRecord(value: unknown): value is Record {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction getErrorMessage(value: unknown): string | undefined {\n if (!isRecord(value)) return undefined;\n const message = value.message;\n return typeof message === \"string\" ? message : undefined;\n}\n\nfunction getErrorCode(value: unknown): string | number | undefined {\n if (!isRecord(value)) return undefined;\n const code = value.code;\n if (typeof code === \"string\" || typeof code === \"number\") {\n return code;\n }\n return undefined;\n}\n\nfunction looksLikeUserRejection(err: unknown): boolean {\n if (err instanceof NexusError) {\n return (\n err.code === ERROR_CODES.USER_DENIED_ALLOWANCE ||\n err.code === ERROR_CODES.USER_DENIED_INTENT ||\n err.code === ERROR_CODES.USER_DENIED_INTENT_SIGNATURE ||\n err.code === ERROR_CODES.USER_DENIED_SIWE_SIGNATURE\n );\n }\n\n const code = getErrorCode(err);\n if (code === 4001 || code === \"ACTION_REJECTED\") {\n return true;\n }\n\n const message = getErrorMessage(err)?.toLowerCase();\n if (!message) return false;\n return (\n message.includes(\"user denied\") ||\n message.includes(\"user rejected\") ||\n message.includes(\"rejected request\") ||\n message.includes(\"denied transaction signature\")\n );\n}\n\nfunction sanitizeMessage(message?: string): string {\n if (!message) return DEFAULT_ERROR_MESSAGE;\n const cleaned = message\n .replace(/^Internal error:\\s*/i, \"\")\n .replace(/^COSMOS:\\s*/i, \"\")\n .trim();\n return cleaned || DEFAULT_ERROR_MESSAGE;\n}\n\nfunction handler(err: unknown) {\n if (err === null || err === undefined) {\n console.error(\"Unexpected empty error from Nexus SDK:\", err);\n return {\n code: \"unexpected_error\",\n message: EMPTY_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (looksLikeUserRejection(err)) {\n return {\n code: ERROR_CODES.USER_DENIED_INTENT,\n message: USER_REJECTED_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (err instanceof NexusError) {\n const mappedMessage =\n ERROR_MESSAGE_BY_CODE[err.code] ?? sanitizeMessage(err.message);\n return {\n code: err.code,\n message: mappedMessage,\n context: err?.data?.context,\n details: err?.data?.details,\n };\n }\n\n const unknownMessage = sanitizeMessage(getErrorMessage(err));\n console.error(\"Unexpected error:\", err);\n return {\n code: String(getErrorCode(err) ?? \"unexpected_error\"),\n message: unknownMessage || DEFAULT_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n}\n\nexport function useNexusError() {\n return handler;\n}\n", + "content": "import { ERROR_CODES, NexusError } from \"@avail-project/nexus-core\";\n\nconst DEFAULT_ERROR_MESSAGE = \"Oops! Something went wrong. Please try again.\";\nconst USER_REJECTED_MESSAGE = \"Transaction was rejected in your wallet.\";\nconst EMPTY_ERROR_MESSAGE =\n \"Unable to determine transaction state. Please refresh and try again.\";\n\nconst ERROR_MESSAGE_BY_CODE: Partial> = {\n [ERROR_CODES.INVALID_VALUES_ALLOWANCE_HOOK]:\n \"Invalid allowance selection. Please review allowance values and try again.\",\n [ERROR_CODES.SDK_NOT_INITIALIZED]:\n \"Nexus SDK is not initialized. Reconnect your wallet and try again.\",\n [ERROR_CODES.SDK_INIT_STATE_NOT_EXPECTED]:\n \"Nexus is still initializing. Please wait a few seconds and retry.\",\n [ERROR_CODES.CHAIN_NOT_FOUND]:\n \"Selected chain is not supported for this route.\",\n [ERROR_CODES.CHAIN_DATA_NOT_FOUND]:\n \"Chain metadata is unavailable for this route. Please try another chain.\",\n [ERROR_CODES.ASSET_NOT_FOUND]:\n \"Requested asset was not found in your balances.\",\n [ERROR_CODES.COSMOS_ERROR]:\n \"Cosmos-side operation failed. Please retry in a moment.\",\n [ERROR_CODES.TOKEN_NOT_SUPPORTED]:\n \"Selected token is not supported for this route.\",\n [ERROR_CODES.UNIVERSE_NOT_SUPPORTED]:\n \"Selected chain universe is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_SUPPORTED]:\n \"Selected environment is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_KNOWN]:\n \"Selected environment is not recognized.\",\n [ERROR_CODES.UNKNOWN_SIGNATURE]:\n \"Unsupported signature type for this transaction.\",\n [ERROR_CODES.LIQUIDITY_TIMEOUT]:\n \"Timed out waiting for liquidity. Please retry.\",\n [ERROR_CODES.USER_DENIED_INTENT]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_ALLOWANCE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_INTENT_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_SIWE_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.INSUFFICIENT_BALANCE]: \"Insufficient balance to proceed.\",\n [ERROR_CODES.WALLET_NOT_CONNECTED]:\n \"Wallet is not connected. Connect your wallet and try again.\",\n [ERROR_CODES.FETCH_GAS_PRICE_FAILED]:\n \"Unable to estimate gas right now. Please retry.\",\n [ERROR_CODES.SIMULATION_FAILED]:\n \"Simulation failed. Please review your inputs and try again.\",\n [ERROR_CODES.QUOTE_FAILED]:\n \"Unable to fetch a quote right now. Please retry.\",\n [ERROR_CODES.SWAP_FAILED]: \"Swap execution failed. Please retry.\",\n [ERROR_CODES.VAULT_CONTRACT_NOT_FOUND]:\n \"Required vault contract is unavailable on this chain.\",\n [ERROR_CODES.SLIPPAGE_EXCEEDED_ALLOWANCE]:\n \"Slippage exceeded tolerance. Refresh quote and retry.\",\n [ERROR_CODES.RATES_CHANGED_BEYOND_TOLERANCE]:\n \"Rates changed beyond tolerance. Review and retry.\",\n [ERROR_CODES.RFF_FEE_EXPIRED]: \"Quote expired. Refresh and try again.\",\n [ERROR_CODES.INVALID_INPUT]:\n \"Some transaction inputs are invalid. Please review and try again.\",\n [ERROR_CODES.INVALID_ADDRESS_LENGTH]:\n \"Address format is invalid for the selected chain.\",\n [ERROR_CODES.NO_BALANCE_FOR_ADDRESS]:\n \"No balance found for this wallet on supported source chains.\",\n [ERROR_CODES.TRANSACTION_TIMEOUT]:\n \"Transaction is taking longer than expected. Check your wallet and explorer.\",\n [ERROR_CODES.TRANSACTION_REVERTED]:\n \"Transaction reverted on-chain. Please verify inputs and retry.\",\n [ERROR_CODES.DESTINATION_REQUEST_HASH_NOT_FOUND]:\n \"Could not finalize destination request. Please retry.\",\n};\n\nfunction isRecord(value: unknown): value is Record {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction getErrorMessage(value: unknown): string | undefined {\n if (!isRecord(value)) return undefined;\n const message = value.message;\n return typeof message === \"string\" ? message : undefined;\n}\n\nfunction getErrorCode(value: unknown): string | number | undefined {\n if (!isRecord(value)) return undefined;\n const code = value.code;\n if (typeof code === \"string\" || typeof code === \"number\") {\n return code;\n }\n return undefined;\n}\n\nfunction looksLikeUserRejection(err: unknown): boolean {\n if (err instanceof NexusError) {\n return (\n err.code === ERROR_CODES.USER_DENIED_ALLOWANCE ||\n err.code === ERROR_CODES.USER_DENIED_INTENT ||\n err.code === ERROR_CODES.USER_DENIED_INTENT_SIGNATURE ||\n err.code === ERROR_CODES.USER_DENIED_SIWE_SIGNATURE\n );\n }\n\n const code = getErrorCode(err);\n if (code === 4001 || code === \"ACTION_REJECTED\") {\n return true;\n }\n\n const message = getErrorMessage(err)?.toLowerCase();\n if (!message) return false;\n return (\n message.includes(\"user denied\") ||\n message.includes(\"user rejected\") ||\n message.includes(\"rejected request\") ||\n message.includes(\"denied transaction signature\")\n );\n}\n\nfunction sanitizeMessage(message?: string): string {\n if (!message) return DEFAULT_ERROR_MESSAGE;\n const cleaned = message\n .replace(/^Internal error:\\s*/i, \"\")\n .replace(/^COSMOS:\\s*/i, \"\")\n .trim();\n return cleaned || DEFAULT_ERROR_MESSAGE;\n}\n\nfunction handler(err: unknown) {\n if (err === null || err === undefined) {\n console.error(\"Unexpected empty error from Nexus SDK:\", err);\n return {\n code: \"unexpected_error\",\n message: EMPTY_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (looksLikeUserRejection(err)) {\n return {\n code: ERROR_CODES.USER_DENIED_INTENT,\n message: USER_REJECTED_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (err instanceof NexusError) {\n const mappedMessage =\n ERROR_MESSAGE_BY_CODE[err.code] ?? sanitizeMessage(err.message);\n return {\n code: err.code,\n message: mappedMessage,\n context: err?.data?.context,\n details: err?.data?.details,\n };\n }\n\n const unknownMessage = sanitizeMessage(getErrorMessage(err));\n console.error(\"Unexpected error:\", err);\n return {\n code: String(getErrorCode(err) ?? \"unexpected_error\"),\n message: unknownMessage || DEFAULT_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n}\n\nexport function useNexusError() {\n return handler;\n}\n", "type": "registry:component", "target": "components/common/hooks/useNexusError.ts" }, diff --git a/public/r/nexus-one.json b/public/r/nexus-one.json index 4914f31..0817f0c 100644 --- a/public/r/nexus-one.json +++ b/public/r/nexus-one.json @@ -151,7 +151,7 @@ }, { "path": "registry/nexus-elements/common/hooks/useNexusError.ts", - "content": "import { ERROR_CODES, NexusError } from \"@avail-project/nexus-core\";\n\nconst DEFAULT_ERROR_MESSAGE = \"Oops! Something went wrong. Please try again.\";\nconst USER_REJECTED_MESSAGE = \"Transaction was rejected in your wallet.\";\nconst EMPTY_ERROR_MESSAGE =\n \"Unable to determine transaction state. Please refresh and try again.\";\n\nconst ERROR_MESSAGE_BY_CODE: Partial> = {\n [ERROR_CODES.INVALID_VALUES_ALLOWANCE_HOOK]:\n \"Invalid allowance selection. Please review allowance values and try again.\",\n [ERROR_CODES.SDK_NOT_INITIALIZED]:\n \"Nexus SDK is not initialized. Reconnect your wallet and try again.\",\n [ERROR_CODES.SDK_INIT_STATE_NOT_EXPECTED]:\n \"Nexus is still initializing. Please wait a few seconds and retry.\",\n [ERROR_CODES.CHAIN_NOT_FOUND]:\n \"Selected chain is not supported for this route.\",\n [ERROR_CODES.CHAIN_DATA_NOT_FOUND]:\n \"Chain metadata is unavailable for this route. Please try another chain.\",\n [ERROR_CODES.ASSET_NOT_FOUND]:\n \"Requested asset was not found in your balances.\",\n [ERROR_CODES.COSMOS_ERROR]:\n \"Cosmos-side operation failed. Please retry in a moment.\",\n [ERROR_CODES.TOKEN_NOT_SUPPORTED]:\n \"Selected token is not supported for this route.\",\n [ERROR_CODES.UNIVERSE_NOT_SUPPORTED]:\n \"Selected chain universe is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_SUPPORTED]:\n \"Selected environment is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_KNOWN]:\n \"Selected environment is not recognized.\",\n [ERROR_CODES.UNKNOWN_SIGNATURE]:\n \"Unsupported signature type for this transaction.\",\n [ERROR_CODES.TRON_DEPOSIT_FAIL]:\n \"TRON deposit transaction failed. Please retry.\",\n [ERROR_CODES.TRON_APPROVAL_FAIL]:\n \"TRON approval transaction failed. Please retry.\",\n [ERROR_CODES.LIQUIDITY_TIMEOUT]:\n \"Timed out waiting for liquidity. Please retry.\",\n [ERROR_CODES.USER_DENIED_INTENT]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_ALLOWANCE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_INTENT_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_SIWE_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.INSUFFICIENT_BALANCE]: \"Insufficient balance to proceed.\",\n [ERROR_CODES.WALLET_NOT_CONNECTED]:\n \"Wallet is not connected. Connect your wallet and try again.\",\n [ERROR_CODES.FETCH_GAS_PRICE_FAILED]:\n \"Unable to estimate gas right now. Please retry.\",\n [ERROR_CODES.SIMULATION_FAILED]:\n \"Simulation failed. Please review your inputs and try again.\",\n [ERROR_CODES.QUOTE_FAILED]:\n \"Unable to fetch a quote right now. Please retry.\",\n [ERROR_CODES.SWAP_FAILED]: \"Swap execution failed. Please retry.\",\n [ERROR_CODES.VAULT_CONTRACT_NOT_FOUND]:\n \"Required vault contract is unavailable on this chain.\",\n [ERROR_CODES.SLIPPAGE_EXCEEDED_ALLOWANCE]:\n \"Slippage exceeded tolerance. Refresh quote and retry.\",\n [ERROR_CODES.RATES_CHANGED_BEYOND_TOLERANCE]:\n \"Rates changed beyond tolerance. Review and retry.\",\n [ERROR_CODES.RFF_FEE_EXPIRED]:\n \"Quote expired. Refresh and try again.\",\n [ERROR_CODES.INVALID_INPUT]:\n \"Some transaction inputs are invalid. Please review and try again.\",\n [ERROR_CODES.INVALID_ADDRESS_LENGTH]:\n \"Address format is invalid for the selected chain.\",\n [ERROR_CODES.NO_BALANCE_FOR_ADDRESS]:\n \"No balance found for this wallet on supported source chains.\",\n [ERROR_CODES.TRANSACTION_TIMEOUT]:\n \"Transaction is taking longer than expected. Check your wallet and explorer.\",\n [ERROR_CODES.TRANSACTION_REVERTED]:\n \"Transaction reverted on-chain. Please verify inputs and retry.\",\n [ERROR_CODES.DESTINATION_REQUEST_HASH_NOT_FOUND]:\n \"Could not finalize destination request. Please retry.\",\n};\n\nfunction isRecord(value: unknown): value is Record {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction getErrorMessage(value: unknown): string | undefined {\n if (!isRecord(value)) return undefined;\n const message = value.message;\n return typeof message === \"string\" ? message : undefined;\n}\n\nfunction getErrorCode(value: unknown): string | number | undefined {\n if (!isRecord(value)) return undefined;\n const code = value.code;\n if (typeof code === \"string\" || typeof code === \"number\") {\n return code;\n }\n return undefined;\n}\n\nfunction looksLikeUserRejection(err: unknown): boolean {\n if (err instanceof NexusError) {\n return (\n err.code === ERROR_CODES.USER_DENIED_ALLOWANCE ||\n err.code === ERROR_CODES.USER_DENIED_INTENT ||\n err.code === ERROR_CODES.USER_DENIED_INTENT_SIGNATURE ||\n err.code === ERROR_CODES.USER_DENIED_SIWE_SIGNATURE\n );\n }\n\n const code = getErrorCode(err);\n if (code === 4001 || code === \"ACTION_REJECTED\") {\n return true;\n }\n\n const message = getErrorMessage(err)?.toLowerCase();\n if (!message) return false;\n return (\n message.includes(\"user denied\") ||\n message.includes(\"user rejected\") ||\n message.includes(\"rejected request\") ||\n message.includes(\"denied transaction signature\")\n );\n}\n\nfunction sanitizeMessage(message?: string): string {\n if (!message) return DEFAULT_ERROR_MESSAGE;\n const cleaned = message\n .replace(/^Internal error:\\s*/i, \"\")\n .replace(/^COSMOS:\\s*/i, \"\")\n .trim();\n return cleaned || DEFAULT_ERROR_MESSAGE;\n}\n\nfunction handler(err: unknown) {\n if (err === null || err === undefined) {\n console.error(\"Unexpected empty error from Nexus SDK:\", err);\n return {\n code: \"unexpected_error\",\n message: EMPTY_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (looksLikeUserRejection(err)) {\n return {\n code: ERROR_CODES.USER_DENIED_INTENT,\n message: USER_REJECTED_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (err instanceof NexusError) {\n const mappedMessage =\n ERROR_MESSAGE_BY_CODE[err.code] ?? sanitizeMessage(err.message);\n return {\n code: err.code,\n message: mappedMessage,\n context: err?.data?.context,\n details: err?.data?.details,\n };\n }\n\n const unknownMessage = sanitizeMessage(getErrorMessage(err));\n console.error(\"Unexpected error:\", err);\n return {\n code: String(getErrorCode(err) ?? \"unexpected_error\"),\n message: unknownMessage || DEFAULT_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n}\n\nexport function useNexusError() {\n return handler;\n}\n", + "content": "import { ERROR_CODES, NexusError } from \"@avail-project/nexus-core\";\n\nconst DEFAULT_ERROR_MESSAGE = \"Oops! Something went wrong. Please try again.\";\nconst USER_REJECTED_MESSAGE = \"Transaction was rejected in your wallet.\";\nconst EMPTY_ERROR_MESSAGE =\n \"Unable to determine transaction state. Please refresh and try again.\";\n\nconst ERROR_MESSAGE_BY_CODE: Partial> = {\n [ERROR_CODES.INVALID_VALUES_ALLOWANCE_HOOK]:\n \"Invalid allowance selection. Please review allowance values and try again.\",\n [ERROR_CODES.SDK_NOT_INITIALIZED]:\n \"Nexus SDK is not initialized. Reconnect your wallet and try again.\",\n [ERROR_CODES.SDK_INIT_STATE_NOT_EXPECTED]:\n \"Nexus is still initializing. Please wait a few seconds and retry.\",\n [ERROR_CODES.CHAIN_NOT_FOUND]:\n \"Selected chain is not supported for this route.\",\n [ERROR_CODES.CHAIN_DATA_NOT_FOUND]:\n \"Chain metadata is unavailable for this route. Please try another chain.\",\n [ERROR_CODES.ASSET_NOT_FOUND]:\n \"Requested asset was not found in your balances.\",\n [ERROR_CODES.COSMOS_ERROR]:\n \"Cosmos-side operation failed. Please retry in a moment.\",\n [ERROR_CODES.TOKEN_NOT_SUPPORTED]:\n \"Selected token is not supported for this route.\",\n [ERROR_CODES.UNIVERSE_NOT_SUPPORTED]:\n \"Selected chain universe is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_SUPPORTED]:\n \"Selected environment is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_KNOWN]:\n \"Selected environment is not recognized.\",\n [ERROR_CODES.UNKNOWN_SIGNATURE]:\n \"Unsupported signature type for this transaction.\",\n [ERROR_CODES.LIQUIDITY_TIMEOUT]:\n \"Timed out waiting for liquidity. Please retry.\",\n [ERROR_CODES.USER_DENIED_INTENT]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_ALLOWANCE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_INTENT_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_SIWE_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.INSUFFICIENT_BALANCE]: \"Insufficient balance to proceed.\",\n [ERROR_CODES.WALLET_NOT_CONNECTED]:\n \"Wallet is not connected. Connect your wallet and try again.\",\n [ERROR_CODES.FETCH_GAS_PRICE_FAILED]:\n \"Unable to estimate gas right now. Please retry.\",\n [ERROR_CODES.SIMULATION_FAILED]:\n \"Simulation failed. Please review your inputs and try again.\",\n [ERROR_CODES.QUOTE_FAILED]:\n \"Unable to fetch a quote right now. Please retry.\",\n [ERROR_CODES.SWAP_FAILED]: \"Swap execution failed. Please retry.\",\n [ERROR_CODES.VAULT_CONTRACT_NOT_FOUND]:\n \"Required vault contract is unavailable on this chain.\",\n [ERROR_CODES.SLIPPAGE_EXCEEDED_ALLOWANCE]:\n \"Slippage exceeded tolerance. Refresh quote and retry.\",\n [ERROR_CODES.RATES_CHANGED_BEYOND_TOLERANCE]:\n \"Rates changed beyond tolerance. Review and retry.\",\n [ERROR_CODES.RFF_FEE_EXPIRED]: \"Quote expired. Refresh and try again.\",\n [ERROR_CODES.INVALID_INPUT]:\n \"Some transaction inputs are invalid. Please review and try again.\",\n [ERROR_CODES.INVALID_ADDRESS_LENGTH]:\n \"Address format is invalid for the selected chain.\",\n [ERROR_CODES.NO_BALANCE_FOR_ADDRESS]:\n \"No balance found for this wallet on supported source chains.\",\n [ERROR_CODES.TRANSACTION_TIMEOUT]:\n \"Transaction is taking longer than expected. Check your wallet and explorer.\",\n [ERROR_CODES.TRANSACTION_REVERTED]:\n \"Transaction reverted on-chain. Please verify inputs and retry.\",\n [ERROR_CODES.DESTINATION_REQUEST_HASH_NOT_FOUND]:\n \"Could not finalize destination request. Please retry.\",\n};\n\nfunction isRecord(value: unknown): value is Record {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction getErrorMessage(value: unknown): string | undefined {\n if (!isRecord(value)) return undefined;\n const message = value.message;\n return typeof message === \"string\" ? message : undefined;\n}\n\nfunction getErrorCode(value: unknown): string | number | undefined {\n if (!isRecord(value)) return undefined;\n const code = value.code;\n if (typeof code === \"string\" || typeof code === \"number\") {\n return code;\n }\n return undefined;\n}\n\nfunction looksLikeUserRejection(err: unknown): boolean {\n if (err instanceof NexusError) {\n return (\n err.code === ERROR_CODES.USER_DENIED_ALLOWANCE ||\n err.code === ERROR_CODES.USER_DENIED_INTENT ||\n err.code === ERROR_CODES.USER_DENIED_INTENT_SIGNATURE ||\n err.code === ERROR_CODES.USER_DENIED_SIWE_SIGNATURE\n );\n }\n\n const code = getErrorCode(err);\n if (code === 4001 || code === \"ACTION_REJECTED\") {\n return true;\n }\n\n const message = getErrorMessage(err)?.toLowerCase();\n if (!message) return false;\n return (\n message.includes(\"user denied\") ||\n message.includes(\"user rejected\") ||\n message.includes(\"rejected request\") ||\n message.includes(\"denied transaction signature\")\n );\n}\n\nfunction sanitizeMessage(message?: string): string {\n if (!message) return DEFAULT_ERROR_MESSAGE;\n const cleaned = message\n .replace(/^Internal error:\\s*/i, \"\")\n .replace(/^COSMOS:\\s*/i, \"\")\n .trim();\n return cleaned || DEFAULT_ERROR_MESSAGE;\n}\n\nfunction handler(err: unknown) {\n if (err === null || err === undefined) {\n console.error(\"Unexpected empty error from Nexus SDK:\", err);\n return {\n code: \"unexpected_error\",\n message: EMPTY_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (looksLikeUserRejection(err)) {\n return {\n code: ERROR_CODES.USER_DENIED_INTENT,\n message: USER_REJECTED_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (err instanceof NexusError) {\n const mappedMessage =\n ERROR_MESSAGE_BY_CODE[err.code] ?? sanitizeMessage(err.message);\n return {\n code: err.code,\n message: mappedMessage,\n context: err?.data?.context,\n details: err?.data?.details,\n };\n }\n\n const unknownMessage = sanitizeMessage(getErrorMessage(err));\n console.error(\"Unexpected error:\", err);\n return {\n code: String(getErrorCode(err) ?? \"unexpected_error\"),\n message: unknownMessage || DEFAULT_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n}\n\nexport function useNexusError() {\n return handler;\n}\n", "type": "registry:component", "target": "components/common/hooks/useNexusError.ts" }, diff --git a/public/r/swaps.json b/public/r/swaps.json index e5fe4f9..7943123 100644 --- a/public/r/swaps.json +++ b/public/r/swaps.json @@ -144,7 +144,7 @@ }, { "path": "registry/nexus-elements/common/hooks/useNexusError.ts", - "content": "import { ERROR_CODES, NexusError } from \"@avail-project/nexus-core\";\n\nconst DEFAULT_ERROR_MESSAGE = \"Oops! Something went wrong. Please try again.\";\nconst USER_REJECTED_MESSAGE = \"Transaction was rejected in your wallet.\";\nconst EMPTY_ERROR_MESSAGE =\n \"Unable to determine transaction state. Please refresh and try again.\";\n\nconst ERROR_MESSAGE_BY_CODE: Partial> = {\n [ERROR_CODES.INVALID_VALUES_ALLOWANCE_HOOK]:\n \"Invalid allowance selection. Please review allowance values and try again.\",\n [ERROR_CODES.SDK_NOT_INITIALIZED]:\n \"Nexus SDK is not initialized. Reconnect your wallet and try again.\",\n [ERROR_CODES.SDK_INIT_STATE_NOT_EXPECTED]:\n \"Nexus is still initializing. Please wait a few seconds and retry.\",\n [ERROR_CODES.CHAIN_NOT_FOUND]:\n \"Selected chain is not supported for this route.\",\n [ERROR_CODES.CHAIN_DATA_NOT_FOUND]:\n \"Chain metadata is unavailable for this route. Please try another chain.\",\n [ERROR_CODES.ASSET_NOT_FOUND]:\n \"Requested asset was not found in your balances.\",\n [ERROR_CODES.COSMOS_ERROR]:\n \"Cosmos-side operation failed. Please retry in a moment.\",\n [ERROR_CODES.TOKEN_NOT_SUPPORTED]:\n \"Selected token is not supported for this route.\",\n [ERROR_CODES.UNIVERSE_NOT_SUPPORTED]:\n \"Selected chain universe is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_SUPPORTED]:\n \"Selected environment is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_KNOWN]:\n \"Selected environment is not recognized.\",\n [ERROR_CODES.UNKNOWN_SIGNATURE]:\n \"Unsupported signature type for this transaction.\",\n [ERROR_CODES.TRON_DEPOSIT_FAIL]:\n \"TRON deposit transaction failed. Please retry.\",\n [ERROR_CODES.TRON_APPROVAL_FAIL]:\n \"TRON approval transaction failed. Please retry.\",\n [ERROR_CODES.LIQUIDITY_TIMEOUT]:\n \"Timed out waiting for liquidity. Please retry.\",\n [ERROR_CODES.USER_DENIED_INTENT]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_ALLOWANCE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_INTENT_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_SIWE_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.INSUFFICIENT_BALANCE]: \"Insufficient balance to proceed.\",\n [ERROR_CODES.WALLET_NOT_CONNECTED]:\n \"Wallet is not connected. Connect your wallet and try again.\",\n [ERROR_CODES.FETCH_GAS_PRICE_FAILED]:\n \"Unable to estimate gas right now. Please retry.\",\n [ERROR_CODES.SIMULATION_FAILED]:\n \"Simulation failed. Please review your inputs and try again.\",\n [ERROR_CODES.QUOTE_FAILED]:\n \"Unable to fetch a quote right now. Please retry.\",\n [ERROR_CODES.SWAP_FAILED]: \"Swap execution failed. Please retry.\",\n [ERROR_CODES.VAULT_CONTRACT_NOT_FOUND]:\n \"Required vault contract is unavailable on this chain.\",\n [ERROR_CODES.SLIPPAGE_EXCEEDED_ALLOWANCE]:\n \"Slippage exceeded tolerance. Refresh quote and retry.\",\n [ERROR_CODES.RATES_CHANGED_BEYOND_TOLERANCE]:\n \"Rates changed beyond tolerance. Review and retry.\",\n [ERROR_CODES.RFF_FEE_EXPIRED]:\n \"Quote expired. Refresh and try again.\",\n [ERROR_CODES.INVALID_INPUT]:\n \"Some transaction inputs are invalid. Please review and try again.\",\n [ERROR_CODES.INVALID_ADDRESS_LENGTH]:\n \"Address format is invalid for the selected chain.\",\n [ERROR_CODES.NO_BALANCE_FOR_ADDRESS]:\n \"No balance found for this wallet on supported source chains.\",\n [ERROR_CODES.TRANSACTION_TIMEOUT]:\n \"Transaction is taking longer than expected. Check your wallet and explorer.\",\n [ERROR_CODES.TRANSACTION_REVERTED]:\n \"Transaction reverted on-chain. Please verify inputs and retry.\",\n [ERROR_CODES.DESTINATION_REQUEST_HASH_NOT_FOUND]:\n \"Could not finalize destination request. Please retry.\",\n};\n\nfunction isRecord(value: unknown): value is Record {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction getErrorMessage(value: unknown): string | undefined {\n if (!isRecord(value)) return undefined;\n const message = value.message;\n return typeof message === \"string\" ? message : undefined;\n}\n\nfunction getErrorCode(value: unknown): string | number | undefined {\n if (!isRecord(value)) return undefined;\n const code = value.code;\n if (typeof code === \"string\" || typeof code === \"number\") {\n return code;\n }\n return undefined;\n}\n\nfunction looksLikeUserRejection(err: unknown): boolean {\n if (err instanceof NexusError) {\n return (\n err.code === ERROR_CODES.USER_DENIED_ALLOWANCE ||\n err.code === ERROR_CODES.USER_DENIED_INTENT ||\n err.code === ERROR_CODES.USER_DENIED_INTENT_SIGNATURE ||\n err.code === ERROR_CODES.USER_DENIED_SIWE_SIGNATURE\n );\n }\n\n const code = getErrorCode(err);\n if (code === 4001 || code === \"ACTION_REJECTED\") {\n return true;\n }\n\n const message = getErrorMessage(err)?.toLowerCase();\n if (!message) return false;\n return (\n message.includes(\"user denied\") ||\n message.includes(\"user rejected\") ||\n message.includes(\"rejected request\") ||\n message.includes(\"denied transaction signature\")\n );\n}\n\nfunction sanitizeMessage(message?: string): string {\n if (!message) return DEFAULT_ERROR_MESSAGE;\n const cleaned = message\n .replace(/^Internal error:\\s*/i, \"\")\n .replace(/^COSMOS:\\s*/i, \"\")\n .trim();\n return cleaned || DEFAULT_ERROR_MESSAGE;\n}\n\nfunction handler(err: unknown) {\n if (err === null || err === undefined) {\n console.error(\"Unexpected empty error from Nexus SDK:\", err);\n return {\n code: \"unexpected_error\",\n message: EMPTY_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (looksLikeUserRejection(err)) {\n return {\n code: ERROR_CODES.USER_DENIED_INTENT,\n message: USER_REJECTED_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (err instanceof NexusError) {\n const mappedMessage =\n ERROR_MESSAGE_BY_CODE[err.code] ?? sanitizeMessage(err.message);\n return {\n code: err.code,\n message: mappedMessage,\n context: err?.data?.context,\n details: err?.data?.details,\n };\n }\n\n const unknownMessage = sanitizeMessage(getErrorMessage(err));\n console.error(\"Unexpected error:\", err);\n return {\n code: String(getErrorCode(err) ?? \"unexpected_error\"),\n message: unknownMessage || DEFAULT_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n}\n\nexport function useNexusError() {\n return handler;\n}\n", + "content": "import { ERROR_CODES, NexusError } from \"@avail-project/nexus-core\";\n\nconst DEFAULT_ERROR_MESSAGE = \"Oops! Something went wrong. Please try again.\";\nconst USER_REJECTED_MESSAGE = \"Transaction was rejected in your wallet.\";\nconst EMPTY_ERROR_MESSAGE =\n \"Unable to determine transaction state. Please refresh and try again.\";\n\nconst ERROR_MESSAGE_BY_CODE: Partial> = {\n [ERROR_CODES.INVALID_VALUES_ALLOWANCE_HOOK]:\n \"Invalid allowance selection. Please review allowance values and try again.\",\n [ERROR_CODES.SDK_NOT_INITIALIZED]:\n \"Nexus SDK is not initialized. Reconnect your wallet and try again.\",\n [ERROR_CODES.SDK_INIT_STATE_NOT_EXPECTED]:\n \"Nexus is still initializing. Please wait a few seconds and retry.\",\n [ERROR_CODES.CHAIN_NOT_FOUND]:\n \"Selected chain is not supported for this route.\",\n [ERROR_CODES.CHAIN_DATA_NOT_FOUND]:\n \"Chain metadata is unavailable for this route. Please try another chain.\",\n [ERROR_CODES.ASSET_NOT_FOUND]:\n \"Requested asset was not found in your balances.\",\n [ERROR_CODES.COSMOS_ERROR]:\n \"Cosmos-side operation failed. Please retry in a moment.\",\n [ERROR_CODES.TOKEN_NOT_SUPPORTED]:\n \"Selected token is not supported for this route.\",\n [ERROR_CODES.UNIVERSE_NOT_SUPPORTED]:\n \"Selected chain universe is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_SUPPORTED]:\n \"Selected environment is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_KNOWN]:\n \"Selected environment is not recognized.\",\n [ERROR_CODES.UNKNOWN_SIGNATURE]:\n \"Unsupported signature type for this transaction.\",\n [ERROR_CODES.LIQUIDITY_TIMEOUT]:\n \"Timed out waiting for liquidity. Please retry.\",\n [ERROR_CODES.USER_DENIED_INTENT]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_ALLOWANCE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_INTENT_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_SIWE_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.INSUFFICIENT_BALANCE]: \"Insufficient balance to proceed.\",\n [ERROR_CODES.WALLET_NOT_CONNECTED]:\n \"Wallet is not connected. Connect your wallet and try again.\",\n [ERROR_CODES.FETCH_GAS_PRICE_FAILED]:\n \"Unable to estimate gas right now. Please retry.\",\n [ERROR_CODES.SIMULATION_FAILED]:\n \"Simulation failed. Please review your inputs and try again.\",\n [ERROR_CODES.QUOTE_FAILED]:\n \"Unable to fetch a quote right now. Please retry.\",\n [ERROR_CODES.SWAP_FAILED]: \"Swap execution failed. Please retry.\",\n [ERROR_CODES.VAULT_CONTRACT_NOT_FOUND]:\n \"Required vault contract is unavailable on this chain.\",\n [ERROR_CODES.SLIPPAGE_EXCEEDED_ALLOWANCE]:\n \"Slippage exceeded tolerance. Refresh quote and retry.\",\n [ERROR_CODES.RATES_CHANGED_BEYOND_TOLERANCE]:\n \"Rates changed beyond tolerance. Review and retry.\",\n [ERROR_CODES.RFF_FEE_EXPIRED]: \"Quote expired. Refresh and try again.\",\n [ERROR_CODES.INVALID_INPUT]:\n \"Some transaction inputs are invalid. Please review and try again.\",\n [ERROR_CODES.INVALID_ADDRESS_LENGTH]:\n \"Address format is invalid for the selected chain.\",\n [ERROR_CODES.NO_BALANCE_FOR_ADDRESS]:\n \"No balance found for this wallet on supported source chains.\",\n [ERROR_CODES.TRANSACTION_TIMEOUT]:\n \"Transaction is taking longer than expected. Check your wallet and explorer.\",\n [ERROR_CODES.TRANSACTION_REVERTED]:\n \"Transaction reverted on-chain. Please verify inputs and retry.\",\n [ERROR_CODES.DESTINATION_REQUEST_HASH_NOT_FOUND]:\n \"Could not finalize destination request. Please retry.\",\n};\n\nfunction isRecord(value: unknown): value is Record {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction getErrorMessage(value: unknown): string | undefined {\n if (!isRecord(value)) return undefined;\n const message = value.message;\n return typeof message === \"string\" ? message : undefined;\n}\n\nfunction getErrorCode(value: unknown): string | number | undefined {\n if (!isRecord(value)) return undefined;\n const code = value.code;\n if (typeof code === \"string\" || typeof code === \"number\") {\n return code;\n }\n return undefined;\n}\n\nfunction looksLikeUserRejection(err: unknown): boolean {\n if (err instanceof NexusError) {\n return (\n err.code === ERROR_CODES.USER_DENIED_ALLOWANCE ||\n err.code === ERROR_CODES.USER_DENIED_INTENT ||\n err.code === ERROR_CODES.USER_DENIED_INTENT_SIGNATURE ||\n err.code === ERROR_CODES.USER_DENIED_SIWE_SIGNATURE\n );\n }\n\n const code = getErrorCode(err);\n if (code === 4001 || code === \"ACTION_REJECTED\") {\n return true;\n }\n\n const message = getErrorMessage(err)?.toLowerCase();\n if (!message) return false;\n return (\n message.includes(\"user denied\") ||\n message.includes(\"user rejected\") ||\n message.includes(\"rejected request\") ||\n message.includes(\"denied transaction signature\")\n );\n}\n\nfunction sanitizeMessage(message?: string): string {\n if (!message) return DEFAULT_ERROR_MESSAGE;\n const cleaned = message\n .replace(/^Internal error:\\s*/i, \"\")\n .replace(/^COSMOS:\\s*/i, \"\")\n .trim();\n return cleaned || DEFAULT_ERROR_MESSAGE;\n}\n\nfunction handler(err: unknown) {\n if (err === null || err === undefined) {\n console.error(\"Unexpected empty error from Nexus SDK:\", err);\n return {\n code: \"unexpected_error\",\n message: EMPTY_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (looksLikeUserRejection(err)) {\n return {\n code: ERROR_CODES.USER_DENIED_INTENT,\n message: USER_REJECTED_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (err instanceof NexusError) {\n const mappedMessage =\n ERROR_MESSAGE_BY_CODE[err.code] ?? sanitizeMessage(err.message);\n return {\n code: err.code,\n message: mappedMessage,\n context: err?.data?.context,\n details: err?.data?.details,\n };\n }\n\n const unknownMessage = sanitizeMessage(getErrorMessage(err));\n console.error(\"Unexpected error:\", err);\n return {\n code: String(getErrorCode(err) ?? \"unexpected_error\"),\n message: unknownMessage || DEFAULT_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n}\n\nexport function useNexusError() {\n return handler;\n}\n", "type": "registry:component", "target": "components/common/hooks/useNexusError.ts" }, diff --git a/public/r/transfer.json b/public/r/transfer.json index 25b9a54..cb3fbbc 100644 --- a/public/r/transfer.json +++ b/public/r/transfer.json @@ -117,7 +117,7 @@ }, { "path": "registry/nexus-elements/common/hooks/useNexusError.ts", - "content": "import { ERROR_CODES, NexusError } from \"@avail-project/nexus-core\";\n\nconst DEFAULT_ERROR_MESSAGE = \"Oops! Something went wrong. Please try again.\";\nconst USER_REJECTED_MESSAGE = \"Transaction was rejected in your wallet.\";\nconst EMPTY_ERROR_MESSAGE =\n \"Unable to determine transaction state. Please refresh and try again.\";\n\nconst ERROR_MESSAGE_BY_CODE: Partial> = {\n [ERROR_CODES.INVALID_VALUES_ALLOWANCE_HOOK]:\n \"Invalid allowance selection. Please review allowance values and try again.\",\n [ERROR_CODES.SDK_NOT_INITIALIZED]:\n \"Nexus SDK is not initialized. Reconnect your wallet and try again.\",\n [ERROR_CODES.SDK_INIT_STATE_NOT_EXPECTED]:\n \"Nexus is still initializing. Please wait a few seconds and retry.\",\n [ERROR_CODES.CHAIN_NOT_FOUND]:\n \"Selected chain is not supported for this route.\",\n [ERROR_CODES.CHAIN_DATA_NOT_FOUND]:\n \"Chain metadata is unavailable for this route. Please try another chain.\",\n [ERROR_CODES.ASSET_NOT_FOUND]:\n \"Requested asset was not found in your balances.\",\n [ERROR_CODES.COSMOS_ERROR]:\n \"Cosmos-side operation failed. Please retry in a moment.\",\n [ERROR_CODES.TOKEN_NOT_SUPPORTED]:\n \"Selected token is not supported for this route.\",\n [ERROR_CODES.UNIVERSE_NOT_SUPPORTED]:\n \"Selected chain universe is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_SUPPORTED]:\n \"Selected environment is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_KNOWN]:\n \"Selected environment is not recognized.\",\n [ERROR_CODES.UNKNOWN_SIGNATURE]:\n \"Unsupported signature type for this transaction.\",\n [ERROR_CODES.TRON_DEPOSIT_FAIL]:\n \"TRON deposit transaction failed. Please retry.\",\n [ERROR_CODES.TRON_APPROVAL_FAIL]:\n \"TRON approval transaction failed. Please retry.\",\n [ERROR_CODES.LIQUIDITY_TIMEOUT]:\n \"Timed out waiting for liquidity. Please retry.\",\n [ERROR_CODES.USER_DENIED_INTENT]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_ALLOWANCE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_INTENT_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_SIWE_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.INSUFFICIENT_BALANCE]: \"Insufficient balance to proceed.\",\n [ERROR_CODES.WALLET_NOT_CONNECTED]:\n \"Wallet is not connected. Connect your wallet and try again.\",\n [ERROR_CODES.FETCH_GAS_PRICE_FAILED]:\n \"Unable to estimate gas right now. Please retry.\",\n [ERROR_CODES.SIMULATION_FAILED]:\n \"Simulation failed. Please review your inputs and try again.\",\n [ERROR_CODES.QUOTE_FAILED]:\n \"Unable to fetch a quote right now. Please retry.\",\n [ERROR_CODES.SWAP_FAILED]: \"Swap execution failed. Please retry.\",\n [ERROR_CODES.VAULT_CONTRACT_NOT_FOUND]:\n \"Required vault contract is unavailable on this chain.\",\n [ERROR_CODES.SLIPPAGE_EXCEEDED_ALLOWANCE]:\n \"Slippage exceeded tolerance. Refresh quote and retry.\",\n [ERROR_CODES.RATES_CHANGED_BEYOND_TOLERANCE]:\n \"Rates changed beyond tolerance. Review and retry.\",\n [ERROR_CODES.RFF_FEE_EXPIRED]:\n \"Quote expired. Refresh and try again.\",\n [ERROR_CODES.INVALID_INPUT]:\n \"Some transaction inputs are invalid. Please review and try again.\",\n [ERROR_CODES.INVALID_ADDRESS_LENGTH]:\n \"Address format is invalid for the selected chain.\",\n [ERROR_CODES.NO_BALANCE_FOR_ADDRESS]:\n \"No balance found for this wallet on supported source chains.\",\n [ERROR_CODES.TRANSACTION_TIMEOUT]:\n \"Transaction is taking longer than expected. Check your wallet and explorer.\",\n [ERROR_CODES.TRANSACTION_REVERTED]:\n \"Transaction reverted on-chain. Please verify inputs and retry.\",\n [ERROR_CODES.DESTINATION_REQUEST_HASH_NOT_FOUND]:\n \"Could not finalize destination request. Please retry.\",\n};\n\nfunction isRecord(value: unknown): value is Record {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction getErrorMessage(value: unknown): string | undefined {\n if (!isRecord(value)) return undefined;\n const message = value.message;\n return typeof message === \"string\" ? message : undefined;\n}\n\nfunction getErrorCode(value: unknown): string | number | undefined {\n if (!isRecord(value)) return undefined;\n const code = value.code;\n if (typeof code === \"string\" || typeof code === \"number\") {\n return code;\n }\n return undefined;\n}\n\nfunction looksLikeUserRejection(err: unknown): boolean {\n if (err instanceof NexusError) {\n return (\n err.code === ERROR_CODES.USER_DENIED_ALLOWANCE ||\n err.code === ERROR_CODES.USER_DENIED_INTENT ||\n err.code === ERROR_CODES.USER_DENIED_INTENT_SIGNATURE ||\n err.code === ERROR_CODES.USER_DENIED_SIWE_SIGNATURE\n );\n }\n\n const code = getErrorCode(err);\n if (code === 4001 || code === \"ACTION_REJECTED\") {\n return true;\n }\n\n const message = getErrorMessage(err)?.toLowerCase();\n if (!message) return false;\n return (\n message.includes(\"user denied\") ||\n message.includes(\"user rejected\") ||\n message.includes(\"rejected request\") ||\n message.includes(\"denied transaction signature\")\n );\n}\n\nfunction sanitizeMessage(message?: string): string {\n if (!message) return DEFAULT_ERROR_MESSAGE;\n const cleaned = message\n .replace(/^Internal error:\\s*/i, \"\")\n .replace(/^COSMOS:\\s*/i, \"\")\n .trim();\n return cleaned || DEFAULT_ERROR_MESSAGE;\n}\n\nfunction handler(err: unknown) {\n if (err === null || err === undefined) {\n console.error(\"Unexpected empty error from Nexus SDK:\", err);\n return {\n code: \"unexpected_error\",\n message: EMPTY_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (looksLikeUserRejection(err)) {\n return {\n code: ERROR_CODES.USER_DENIED_INTENT,\n message: USER_REJECTED_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (err instanceof NexusError) {\n const mappedMessage =\n ERROR_MESSAGE_BY_CODE[err.code] ?? sanitizeMessage(err.message);\n return {\n code: err.code,\n message: mappedMessage,\n context: err?.data?.context,\n details: err?.data?.details,\n };\n }\n\n const unknownMessage = sanitizeMessage(getErrorMessage(err));\n console.error(\"Unexpected error:\", err);\n return {\n code: String(getErrorCode(err) ?? \"unexpected_error\"),\n message: unknownMessage || DEFAULT_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n}\n\nexport function useNexusError() {\n return handler;\n}\n", + "content": "import { ERROR_CODES, NexusError } from \"@avail-project/nexus-core\";\n\nconst DEFAULT_ERROR_MESSAGE = \"Oops! Something went wrong. Please try again.\";\nconst USER_REJECTED_MESSAGE = \"Transaction was rejected in your wallet.\";\nconst EMPTY_ERROR_MESSAGE =\n \"Unable to determine transaction state. Please refresh and try again.\";\n\nconst ERROR_MESSAGE_BY_CODE: Partial> = {\n [ERROR_CODES.INVALID_VALUES_ALLOWANCE_HOOK]:\n \"Invalid allowance selection. Please review allowance values and try again.\",\n [ERROR_CODES.SDK_NOT_INITIALIZED]:\n \"Nexus SDK is not initialized. Reconnect your wallet and try again.\",\n [ERROR_CODES.SDK_INIT_STATE_NOT_EXPECTED]:\n \"Nexus is still initializing. Please wait a few seconds and retry.\",\n [ERROR_CODES.CHAIN_NOT_FOUND]:\n \"Selected chain is not supported for this route.\",\n [ERROR_CODES.CHAIN_DATA_NOT_FOUND]:\n \"Chain metadata is unavailable for this route. Please try another chain.\",\n [ERROR_CODES.ASSET_NOT_FOUND]:\n \"Requested asset was not found in your balances.\",\n [ERROR_CODES.COSMOS_ERROR]:\n \"Cosmos-side operation failed. Please retry in a moment.\",\n [ERROR_CODES.TOKEN_NOT_SUPPORTED]:\n \"Selected token is not supported for this route.\",\n [ERROR_CODES.UNIVERSE_NOT_SUPPORTED]:\n \"Selected chain universe is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_SUPPORTED]:\n \"Selected environment is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_KNOWN]:\n \"Selected environment is not recognized.\",\n [ERROR_CODES.UNKNOWN_SIGNATURE]:\n \"Unsupported signature type for this transaction.\",\n [ERROR_CODES.LIQUIDITY_TIMEOUT]:\n \"Timed out waiting for liquidity. Please retry.\",\n [ERROR_CODES.USER_DENIED_INTENT]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_ALLOWANCE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_INTENT_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_SIWE_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.INSUFFICIENT_BALANCE]: \"Insufficient balance to proceed.\",\n [ERROR_CODES.WALLET_NOT_CONNECTED]:\n \"Wallet is not connected. Connect your wallet and try again.\",\n [ERROR_CODES.FETCH_GAS_PRICE_FAILED]:\n \"Unable to estimate gas right now. Please retry.\",\n [ERROR_CODES.SIMULATION_FAILED]:\n \"Simulation failed. Please review your inputs and try again.\",\n [ERROR_CODES.QUOTE_FAILED]:\n \"Unable to fetch a quote right now. Please retry.\",\n [ERROR_CODES.SWAP_FAILED]: \"Swap execution failed. Please retry.\",\n [ERROR_CODES.VAULT_CONTRACT_NOT_FOUND]:\n \"Required vault contract is unavailable on this chain.\",\n [ERROR_CODES.SLIPPAGE_EXCEEDED_ALLOWANCE]:\n \"Slippage exceeded tolerance. Refresh quote and retry.\",\n [ERROR_CODES.RATES_CHANGED_BEYOND_TOLERANCE]:\n \"Rates changed beyond tolerance. Review and retry.\",\n [ERROR_CODES.RFF_FEE_EXPIRED]: \"Quote expired. Refresh and try again.\",\n [ERROR_CODES.INVALID_INPUT]:\n \"Some transaction inputs are invalid. Please review and try again.\",\n [ERROR_CODES.INVALID_ADDRESS_LENGTH]:\n \"Address format is invalid for the selected chain.\",\n [ERROR_CODES.NO_BALANCE_FOR_ADDRESS]:\n \"No balance found for this wallet on supported source chains.\",\n [ERROR_CODES.TRANSACTION_TIMEOUT]:\n \"Transaction is taking longer than expected. Check your wallet and explorer.\",\n [ERROR_CODES.TRANSACTION_REVERTED]:\n \"Transaction reverted on-chain. Please verify inputs and retry.\",\n [ERROR_CODES.DESTINATION_REQUEST_HASH_NOT_FOUND]:\n \"Could not finalize destination request. Please retry.\",\n};\n\nfunction isRecord(value: unknown): value is Record {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction getErrorMessage(value: unknown): string | undefined {\n if (!isRecord(value)) return undefined;\n const message = value.message;\n return typeof message === \"string\" ? message : undefined;\n}\n\nfunction getErrorCode(value: unknown): string | number | undefined {\n if (!isRecord(value)) return undefined;\n const code = value.code;\n if (typeof code === \"string\" || typeof code === \"number\") {\n return code;\n }\n return undefined;\n}\n\nfunction looksLikeUserRejection(err: unknown): boolean {\n if (err instanceof NexusError) {\n return (\n err.code === ERROR_CODES.USER_DENIED_ALLOWANCE ||\n err.code === ERROR_CODES.USER_DENIED_INTENT ||\n err.code === ERROR_CODES.USER_DENIED_INTENT_SIGNATURE ||\n err.code === ERROR_CODES.USER_DENIED_SIWE_SIGNATURE\n );\n }\n\n const code = getErrorCode(err);\n if (code === 4001 || code === \"ACTION_REJECTED\") {\n return true;\n }\n\n const message = getErrorMessage(err)?.toLowerCase();\n if (!message) return false;\n return (\n message.includes(\"user denied\") ||\n message.includes(\"user rejected\") ||\n message.includes(\"rejected request\") ||\n message.includes(\"denied transaction signature\")\n );\n}\n\nfunction sanitizeMessage(message?: string): string {\n if (!message) return DEFAULT_ERROR_MESSAGE;\n const cleaned = message\n .replace(/^Internal error:\\s*/i, \"\")\n .replace(/^COSMOS:\\s*/i, \"\")\n .trim();\n return cleaned || DEFAULT_ERROR_MESSAGE;\n}\n\nfunction handler(err: unknown) {\n if (err === null || err === undefined) {\n console.error(\"Unexpected empty error from Nexus SDK:\", err);\n return {\n code: \"unexpected_error\",\n message: EMPTY_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (looksLikeUserRejection(err)) {\n return {\n code: ERROR_CODES.USER_DENIED_INTENT,\n message: USER_REJECTED_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (err instanceof NexusError) {\n const mappedMessage =\n ERROR_MESSAGE_BY_CODE[err.code] ?? sanitizeMessage(err.message);\n return {\n code: err.code,\n message: mappedMessage,\n context: err?.data?.context,\n details: err?.data?.details,\n };\n }\n\n const unknownMessage = sanitizeMessage(getErrorMessage(err));\n console.error(\"Unexpected error:\", err);\n return {\n code: String(getErrorCode(err) ?? \"unexpected_error\"),\n message: unknownMessage || DEFAULT_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n}\n\nexport function useNexusError() {\n return handler;\n}\n", "type": "registry:component", "target": "components/common/hooks/useNexusError.ts" }, diff --git a/public/r/view-history.json b/public/r/view-history.json index 48c8b92..b0297d7 100644 --- a/public/r/view-history.json +++ b/public/r/view-history.json @@ -68,7 +68,7 @@ }, { "path": "registry/nexus-elements/common/hooks/useNexusError.ts", - "content": "import { ERROR_CODES, NexusError } from \"@avail-project/nexus-core\";\n\nconst DEFAULT_ERROR_MESSAGE = \"Oops! Something went wrong. Please try again.\";\nconst USER_REJECTED_MESSAGE = \"Transaction was rejected in your wallet.\";\nconst EMPTY_ERROR_MESSAGE =\n \"Unable to determine transaction state. Please refresh and try again.\";\n\nconst ERROR_MESSAGE_BY_CODE: Partial> = {\n [ERROR_CODES.INVALID_VALUES_ALLOWANCE_HOOK]:\n \"Invalid allowance selection. Please review allowance values and try again.\",\n [ERROR_CODES.SDK_NOT_INITIALIZED]:\n \"Nexus SDK is not initialized. Reconnect your wallet and try again.\",\n [ERROR_CODES.SDK_INIT_STATE_NOT_EXPECTED]:\n \"Nexus is still initializing. Please wait a few seconds and retry.\",\n [ERROR_CODES.CHAIN_NOT_FOUND]:\n \"Selected chain is not supported for this route.\",\n [ERROR_CODES.CHAIN_DATA_NOT_FOUND]:\n \"Chain metadata is unavailable for this route. Please try another chain.\",\n [ERROR_CODES.ASSET_NOT_FOUND]:\n \"Requested asset was not found in your balances.\",\n [ERROR_CODES.COSMOS_ERROR]:\n \"Cosmos-side operation failed. Please retry in a moment.\",\n [ERROR_CODES.TOKEN_NOT_SUPPORTED]:\n \"Selected token is not supported for this route.\",\n [ERROR_CODES.UNIVERSE_NOT_SUPPORTED]:\n \"Selected chain universe is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_SUPPORTED]:\n \"Selected environment is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_KNOWN]:\n \"Selected environment is not recognized.\",\n [ERROR_CODES.UNKNOWN_SIGNATURE]:\n \"Unsupported signature type for this transaction.\",\n [ERROR_CODES.TRON_DEPOSIT_FAIL]:\n \"TRON deposit transaction failed. Please retry.\",\n [ERROR_CODES.TRON_APPROVAL_FAIL]:\n \"TRON approval transaction failed. Please retry.\",\n [ERROR_CODES.LIQUIDITY_TIMEOUT]:\n \"Timed out waiting for liquidity. Please retry.\",\n [ERROR_CODES.USER_DENIED_INTENT]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_ALLOWANCE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_INTENT_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_SIWE_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.INSUFFICIENT_BALANCE]: \"Insufficient balance to proceed.\",\n [ERROR_CODES.WALLET_NOT_CONNECTED]:\n \"Wallet is not connected. Connect your wallet and try again.\",\n [ERROR_CODES.FETCH_GAS_PRICE_FAILED]:\n \"Unable to estimate gas right now. Please retry.\",\n [ERROR_CODES.SIMULATION_FAILED]:\n \"Simulation failed. Please review your inputs and try again.\",\n [ERROR_CODES.QUOTE_FAILED]:\n \"Unable to fetch a quote right now. Please retry.\",\n [ERROR_CODES.SWAP_FAILED]: \"Swap execution failed. Please retry.\",\n [ERROR_CODES.VAULT_CONTRACT_NOT_FOUND]:\n \"Required vault contract is unavailable on this chain.\",\n [ERROR_CODES.SLIPPAGE_EXCEEDED_ALLOWANCE]:\n \"Slippage exceeded tolerance. Refresh quote and retry.\",\n [ERROR_CODES.RATES_CHANGED_BEYOND_TOLERANCE]:\n \"Rates changed beyond tolerance. Review and retry.\",\n [ERROR_CODES.RFF_FEE_EXPIRED]:\n \"Quote expired. Refresh and try again.\",\n [ERROR_CODES.INVALID_INPUT]:\n \"Some transaction inputs are invalid. Please review and try again.\",\n [ERROR_CODES.INVALID_ADDRESS_LENGTH]:\n \"Address format is invalid for the selected chain.\",\n [ERROR_CODES.NO_BALANCE_FOR_ADDRESS]:\n \"No balance found for this wallet on supported source chains.\",\n [ERROR_CODES.TRANSACTION_TIMEOUT]:\n \"Transaction is taking longer than expected. Check your wallet and explorer.\",\n [ERROR_CODES.TRANSACTION_REVERTED]:\n \"Transaction reverted on-chain. Please verify inputs and retry.\",\n [ERROR_CODES.DESTINATION_REQUEST_HASH_NOT_FOUND]:\n \"Could not finalize destination request. Please retry.\",\n};\n\nfunction isRecord(value: unknown): value is Record {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction getErrorMessage(value: unknown): string | undefined {\n if (!isRecord(value)) return undefined;\n const message = value.message;\n return typeof message === \"string\" ? message : undefined;\n}\n\nfunction getErrorCode(value: unknown): string | number | undefined {\n if (!isRecord(value)) return undefined;\n const code = value.code;\n if (typeof code === \"string\" || typeof code === \"number\") {\n return code;\n }\n return undefined;\n}\n\nfunction looksLikeUserRejection(err: unknown): boolean {\n if (err instanceof NexusError) {\n return (\n err.code === ERROR_CODES.USER_DENIED_ALLOWANCE ||\n err.code === ERROR_CODES.USER_DENIED_INTENT ||\n err.code === ERROR_CODES.USER_DENIED_INTENT_SIGNATURE ||\n err.code === ERROR_CODES.USER_DENIED_SIWE_SIGNATURE\n );\n }\n\n const code = getErrorCode(err);\n if (code === 4001 || code === \"ACTION_REJECTED\") {\n return true;\n }\n\n const message = getErrorMessage(err)?.toLowerCase();\n if (!message) return false;\n return (\n message.includes(\"user denied\") ||\n message.includes(\"user rejected\") ||\n message.includes(\"rejected request\") ||\n message.includes(\"denied transaction signature\")\n );\n}\n\nfunction sanitizeMessage(message?: string): string {\n if (!message) return DEFAULT_ERROR_MESSAGE;\n const cleaned = message\n .replace(/^Internal error:\\s*/i, \"\")\n .replace(/^COSMOS:\\s*/i, \"\")\n .trim();\n return cleaned || DEFAULT_ERROR_MESSAGE;\n}\n\nfunction handler(err: unknown) {\n if (err === null || err === undefined) {\n console.error(\"Unexpected empty error from Nexus SDK:\", err);\n return {\n code: \"unexpected_error\",\n message: EMPTY_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (looksLikeUserRejection(err)) {\n return {\n code: ERROR_CODES.USER_DENIED_INTENT,\n message: USER_REJECTED_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (err instanceof NexusError) {\n const mappedMessage =\n ERROR_MESSAGE_BY_CODE[err.code] ?? sanitizeMessage(err.message);\n return {\n code: err.code,\n message: mappedMessage,\n context: err?.data?.context,\n details: err?.data?.details,\n };\n }\n\n const unknownMessage = sanitizeMessage(getErrorMessage(err));\n console.error(\"Unexpected error:\", err);\n return {\n code: String(getErrorCode(err) ?? \"unexpected_error\"),\n message: unknownMessage || DEFAULT_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n}\n\nexport function useNexusError() {\n return handler;\n}\n", + "content": "import { ERROR_CODES, NexusError } from \"@avail-project/nexus-core\";\n\nconst DEFAULT_ERROR_MESSAGE = \"Oops! Something went wrong. Please try again.\";\nconst USER_REJECTED_MESSAGE = \"Transaction was rejected in your wallet.\";\nconst EMPTY_ERROR_MESSAGE =\n \"Unable to determine transaction state. Please refresh and try again.\";\n\nconst ERROR_MESSAGE_BY_CODE: Partial> = {\n [ERROR_CODES.INVALID_VALUES_ALLOWANCE_HOOK]:\n \"Invalid allowance selection. Please review allowance values and try again.\",\n [ERROR_CODES.SDK_NOT_INITIALIZED]:\n \"Nexus SDK is not initialized. Reconnect your wallet and try again.\",\n [ERROR_CODES.SDK_INIT_STATE_NOT_EXPECTED]:\n \"Nexus is still initializing. Please wait a few seconds and retry.\",\n [ERROR_CODES.CHAIN_NOT_FOUND]:\n \"Selected chain is not supported for this route.\",\n [ERROR_CODES.CHAIN_DATA_NOT_FOUND]:\n \"Chain metadata is unavailable for this route. Please try another chain.\",\n [ERROR_CODES.ASSET_NOT_FOUND]:\n \"Requested asset was not found in your balances.\",\n [ERROR_CODES.COSMOS_ERROR]:\n \"Cosmos-side operation failed. Please retry in a moment.\",\n [ERROR_CODES.TOKEN_NOT_SUPPORTED]:\n \"Selected token is not supported for this route.\",\n [ERROR_CODES.UNIVERSE_NOT_SUPPORTED]:\n \"Selected chain universe is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_SUPPORTED]:\n \"Selected environment is not supported yet.\",\n [ERROR_CODES.ENVIRONMENT_NOT_KNOWN]:\n \"Selected environment is not recognized.\",\n [ERROR_CODES.UNKNOWN_SIGNATURE]:\n \"Unsupported signature type for this transaction.\",\n [ERROR_CODES.LIQUIDITY_TIMEOUT]:\n \"Timed out waiting for liquidity. Please retry.\",\n [ERROR_CODES.USER_DENIED_INTENT]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_ALLOWANCE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_INTENT_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.USER_DENIED_SIWE_SIGNATURE]: USER_REJECTED_MESSAGE,\n [ERROR_CODES.INSUFFICIENT_BALANCE]: \"Insufficient balance to proceed.\",\n [ERROR_CODES.WALLET_NOT_CONNECTED]:\n \"Wallet is not connected. Connect your wallet and try again.\",\n [ERROR_CODES.FETCH_GAS_PRICE_FAILED]:\n \"Unable to estimate gas right now. Please retry.\",\n [ERROR_CODES.SIMULATION_FAILED]:\n \"Simulation failed. Please review your inputs and try again.\",\n [ERROR_CODES.QUOTE_FAILED]:\n \"Unable to fetch a quote right now. Please retry.\",\n [ERROR_CODES.SWAP_FAILED]: \"Swap execution failed. Please retry.\",\n [ERROR_CODES.VAULT_CONTRACT_NOT_FOUND]:\n \"Required vault contract is unavailable on this chain.\",\n [ERROR_CODES.SLIPPAGE_EXCEEDED_ALLOWANCE]:\n \"Slippage exceeded tolerance. Refresh quote and retry.\",\n [ERROR_CODES.RATES_CHANGED_BEYOND_TOLERANCE]:\n \"Rates changed beyond tolerance. Review and retry.\",\n [ERROR_CODES.RFF_FEE_EXPIRED]: \"Quote expired. Refresh and try again.\",\n [ERROR_CODES.INVALID_INPUT]:\n \"Some transaction inputs are invalid. Please review and try again.\",\n [ERROR_CODES.INVALID_ADDRESS_LENGTH]:\n \"Address format is invalid for the selected chain.\",\n [ERROR_CODES.NO_BALANCE_FOR_ADDRESS]:\n \"No balance found for this wallet on supported source chains.\",\n [ERROR_CODES.TRANSACTION_TIMEOUT]:\n \"Transaction is taking longer than expected. Check your wallet and explorer.\",\n [ERROR_CODES.TRANSACTION_REVERTED]:\n \"Transaction reverted on-chain. Please verify inputs and retry.\",\n [ERROR_CODES.DESTINATION_REQUEST_HASH_NOT_FOUND]:\n \"Could not finalize destination request. Please retry.\",\n};\n\nfunction isRecord(value: unknown): value is Record {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction getErrorMessage(value: unknown): string | undefined {\n if (!isRecord(value)) return undefined;\n const message = value.message;\n return typeof message === \"string\" ? message : undefined;\n}\n\nfunction getErrorCode(value: unknown): string | number | undefined {\n if (!isRecord(value)) return undefined;\n const code = value.code;\n if (typeof code === \"string\" || typeof code === \"number\") {\n return code;\n }\n return undefined;\n}\n\nfunction looksLikeUserRejection(err: unknown): boolean {\n if (err instanceof NexusError) {\n return (\n err.code === ERROR_CODES.USER_DENIED_ALLOWANCE ||\n err.code === ERROR_CODES.USER_DENIED_INTENT ||\n err.code === ERROR_CODES.USER_DENIED_INTENT_SIGNATURE ||\n err.code === ERROR_CODES.USER_DENIED_SIWE_SIGNATURE\n );\n }\n\n const code = getErrorCode(err);\n if (code === 4001 || code === \"ACTION_REJECTED\") {\n return true;\n }\n\n const message = getErrorMessage(err)?.toLowerCase();\n if (!message) return false;\n return (\n message.includes(\"user denied\") ||\n message.includes(\"user rejected\") ||\n message.includes(\"rejected request\") ||\n message.includes(\"denied transaction signature\")\n );\n}\n\nfunction sanitizeMessage(message?: string): string {\n if (!message) return DEFAULT_ERROR_MESSAGE;\n const cleaned = message\n .replace(/^Internal error:\\s*/i, \"\")\n .replace(/^COSMOS:\\s*/i, \"\")\n .trim();\n return cleaned || DEFAULT_ERROR_MESSAGE;\n}\n\nfunction handler(err: unknown) {\n if (err === null || err === undefined) {\n console.error(\"Unexpected empty error from Nexus SDK:\", err);\n return {\n code: \"unexpected_error\",\n message: EMPTY_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (looksLikeUserRejection(err)) {\n return {\n code: ERROR_CODES.USER_DENIED_INTENT,\n message: USER_REJECTED_MESSAGE,\n context: undefined,\n details: undefined,\n };\n }\n\n if (err instanceof NexusError) {\n const mappedMessage =\n ERROR_MESSAGE_BY_CODE[err.code] ?? sanitizeMessage(err.message);\n return {\n code: err.code,\n message: mappedMessage,\n context: err?.data?.context,\n details: err?.data?.details,\n };\n }\n\n const unknownMessage = sanitizeMessage(getErrorMessage(err));\n console.error(\"Unexpected error:\", err);\n return {\n code: String(getErrorCode(err) ?? \"unexpected_error\"),\n message: unknownMessage || DEFAULT_ERROR_MESSAGE,\n context: undefined,\n details: undefined,\n };\n}\n\nexport function useNexusError() {\n return handler;\n}\n", "type": "registry:component", "target": "components/common/hooks/useNexusError.ts" }, diff --git a/registry/nexus-elements/common/hooks/useNexusError.ts b/registry/nexus-elements/common/hooks/useNexusError.ts index f28c3d9..7bf4de4 100644 --- a/registry/nexus-elements/common/hooks/useNexusError.ts +++ b/registry/nexus-elements/common/hooks/useNexusError.ts @@ -30,10 +30,6 @@ const ERROR_MESSAGE_BY_CODE: Partial> = { "Selected environment is not recognized.", [ERROR_CODES.UNKNOWN_SIGNATURE]: "Unsupported signature type for this transaction.", - [ERROR_CODES.TRON_DEPOSIT_FAIL]: - "TRON deposit transaction failed. Please retry.", - [ERROR_CODES.TRON_APPROVAL_FAIL]: - "TRON approval transaction failed. Please retry.", [ERROR_CODES.LIQUIDITY_TIMEOUT]: "Timed out waiting for liquidity. Please retry.", [ERROR_CODES.USER_DENIED_INTENT]: USER_REJECTED_MESSAGE, @@ -56,8 +52,7 @@ const ERROR_MESSAGE_BY_CODE: Partial> = { "Slippage exceeded tolerance. Refresh quote and retry.", [ERROR_CODES.RATES_CHANGED_BEYOND_TOLERANCE]: "Rates changed beyond tolerance. Review and retry.", - [ERROR_CODES.RFF_FEE_EXPIRED]: - "Quote expired. Refresh and try again.", + [ERROR_CODES.RFF_FEE_EXPIRED]: "Quote expired. Refresh and try again.", [ERROR_CODES.INVALID_INPUT]: "Some transaction inputs are invalid. Please review and try again.", [ERROR_CODES.INVALID_ADDRESS_LENGTH]: