diff --git a/components/helpers/preview-panel.tsx b/components/helpers/preview-panel.tsx index 20bd88c9..5e7e4aa3 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/nexus-one-swap-showcase.tsx b/components/showcase/nexus-one-swap-showcase.tsx index 9cce62aa..a0d17b32 100644 --- a/components/showcase/nexus-one-swap-showcase.tsx +++ b/components/showcase/nexus-one-swap-showcase.tsx @@ -25,7 +25,6 @@ const NexusOneSwapShowcase = () => { config={{ mode: "swap", prefill: { - amount: "1", source: { token: USDC_ARBITRUM, chain: 42161, diff --git a/components/showcase/showcase-wrapper.tsx b/components/showcase/showcase-wrapper.tsx index 7e2b0b5f..4a7ba428 100644 --- a/components/showcase/showcase-wrapper.tsx +++ b/components/showcase/showcase-wrapper.tsx @@ -125,7 +125,12 @@ const ShowcaseWrapper = ({ )}
) : ( - {children} + + {children} + )} ); diff --git a/package.json b/package.json index 9147da18..d3ae3327 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": "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 d7c4414f..da307104 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: 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/e1cca4979977bd00e930be9d9ff7c75f17d3a9fd': - resolution: {gitHosted: true, integrity: sha512-42RzziIHsC9KV/goOO4F6X2XXvWsKCwLUUMxDwAyh9lkO6QVE7DQzR3nElhQF+94utHbZCBXyGjRODXOHtnTHA==, tarball: https://codeload.github.com/availproject/nexus-sdk/tar.gz/e1cca4979977bd00e930be9d9ff7c75f17d3a9fd} - 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'} @@ -1404,7 +1389,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==} @@ -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/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@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': {} @@ -9662,7 +9501,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 @@ -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: @@ -13788,10 +13484,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: @@ -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 d6b0d8f7..ee2c58d7 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' diff --git a/public/r/bridge-deposit.json b/public/r/bridge-deposit.json index 12be655c..6253ad50 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 b833cf9a..135a0f55 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 02218ec3..778292d9 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 1d265eb0..0817f0cd 100644 --- a/public/r/nexus-one.json +++ b/public/r/nexus-one.json @@ -85,19 +85,19 @@ }, { "path": "registry/nexus-elements/nexus-one/components/swap-idle-form.tsx", - "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 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 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" }, { "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 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" }, @@ -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/nexus-provider.json b/public/r/nexus-provider.json index abb472b3..5f98f6a9 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/public/r/swaps.json b/public/r/swaps.json index e5fe4f9d..79431232 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 25b9a54f..cb3fbbce 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 48c8b92d..b0297d74 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 f28c3d98..7bf4de47 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]: 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 10d9bb9d..abf2e3e5 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/components/swap-intent-preview.tsx b/registry/nexus-elements/nexus-one/components/swap-intent-preview.tsx index aa166a18..515147af 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 01ad70e9..020dfe4d 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({ 0 ? usdValue.toFixed(2) : ""} swapType={swapType} + allowOverBalanceAmounts={needsWalletConnection} onOpenSourcePicker={(index) => { setEditingAssetIndex(index ?? null); openDrawerStep("choose-swap-asset"); @@ -5443,7 +6158,13 @@ export function NexusOne({ }} >