diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..fae8e3d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true +} diff --git a/components/layout/top-bar.tsx b/components/layout/top-bar.tsx index d9cd196..cef9ebd 100644 --- a/components/layout/top-bar.tsx +++ b/components/layout/top-bar.tsx @@ -153,11 +153,13 @@ export default function Topbar() { alt="Nexus Elements" width={100} height={100} + loading="eager" className="w-[100px] h-[100px] dark:hidden block" /> Nexus Elements { return { to: contractAddress, data: encoded, - gasPriceSelector: "medium", + gasPrice: "medium" as any, // v2: GasPriceSelector (was gasPriceSelector string in v1) tokenApproval: { - token: tokenAddress, + toTokenSymbol: "USDC", // v2: was token address amount, spender: contractAddress, }, - }; + } as any; }; return ( diff --git a/components/showcase/showcase-wrapper.tsx b/components/showcase/showcase-wrapper.tsx index 9a71fa8..5c7aa71 100644 --- a/components/showcase/showcase-wrapper.tsx +++ b/components/showcase/showcase-wrapper.tsx @@ -55,7 +55,7 @@ const ShowcaseWrapper = ({ useEffect(() => { // Read from localStorage on client side only const storedNetwork = getItem(NETWORK_KEY); - setCurrentNetwork(storedNetwork ?? "mainnet"); + setCurrentNetwork(storedNetwork ?? "testnet"); }, []); const resolvedToggle = diff --git a/package.json b/package.json index 9c03b57..a5e65d8 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@avail-project/nexus-core": "1.2.0", + "@avail-project/nexus-sdk-v2": "github:availproject/nexus-sdk-v2#f4e99ab1d7e96c607d98fef3107f3945cdfbd92a", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-collapsible": "^1.1.12", @@ -30,6 +31,7 @@ "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", + "@radix-ui/react-visually-hidden": "^1.2.4", "@tanstack/react-query": "^5.90.21", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -38,11 +40,11 @@ "fumadocs-core": "^16.0.10", "fumadocs-mdx": "^13.0.7", "lucide-react": "^0.487.0", - "next": "15.5.9", + "next": "16.2.2", "next-themes": "^0.4.6", "pino-pretty": "^13.1.2", - "react": "19.2.2", - "react-dom": "19.2.2", + "react": "19.2.4", + "react-dom": "19.2.4", "rehype-pretty-code": "^0.14.1", "shadcn": "2.9.3-canary.0", "shiki": "^1.22.0", @@ -57,12 +59,12 @@ "@eslint/eslintrc": "^3.3.1", "@tailwindcss/postcss": "^4.1.11", "@types/node": "^20.19.9", - "@types/react": "19.1.2", - "@types/react-dom": "19.1.2", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", "buffer": "^6.0.3", "crypto-browserify": "^3.12.1", "eslint": "^9.32.0", - "eslint-config-next": "15.3.1", + "eslint-config-next": "16.2.1", "stream-browserify": "^3.0.0", "tailwindcss": "^4.1.11", "ts-node": "^10.9.2", @@ -71,8 +73,8 @@ }, "pnpm": { "overrides": { - "@types/react": "19.1.2", - "@types/react-dom": "19.1.2" + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3" } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3eda2c6..9a6946f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,8 +5,8 @@ settings: excludeLinksFromLockfile: false overrides: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 importers: @@ -15,54 +15,60 @@ importers: '@avail-project/nexus-core': specifier: 1.2.0 version: 1.2.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) + '@avail-project/nexus-sdk-v2': + specifier: github:availproject/nexus-sdk-v2#f4e99ab1d7e96c607d98fef3107f3945cdfbd92a + version: git+https://git@github.com:availproject/nexus-sdk-v2.git#f4e99ab1d7e96c607d98fef3107f3945cdfbd92a(@opentelemetry/api@1.9.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@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) + version: 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-checkbox': specifier: ^1.3.3 - version: 1.3.3(@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) + version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-collapsible': specifier: ^1.1.12 - version: 1.1.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) + version: 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-dialog': specifier: ^1.1.15 - version: 1.1.15(@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) + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-dropdown-menu': specifier: ^2.1.16 - version: 2.1.16(@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) + version: 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-label': specifier: ^2.1.8 - version: 2.1.8(@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) + version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-popover': specifier: ^1.1.15 - version: 1.1.15(@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) + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-select': specifier: ^2.2.6 - version: 2.2.6(@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) + version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-separator': specifier: ^1.1.8 - version: 1.1.8(@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) + version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-slot': specifier: ^1.2.4 - version: 1.2.4(@types/react@19.1.2)(react@19.2.2) + version: 1.2.4(@types/react@19.2.14)(react@19.2.4) '@radix-ui/react-switch': specifier: ^1.2.6 - version: 1.2.6(@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) + version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-tabs': specifier: ^1.1.13 - version: 1.1.13(@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) + version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-toggle': specifier: ^1.1.10 - version: 1.1.10(@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) + version: 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-toggle-group': specifier: ^1.1.11 - version: 1.1.11(@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) + version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-tooltip': specifier: ^1.2.8 - version: 1.2.8(@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) + version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-visually-hidden': + specifier: ^1.2.4 + version: 1.2.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-query': specifier: ^5.90.21 - version: 5.90.21(react@19.2.2) + version: 5.90.21(react@19.2.4) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -71,34 +77,34 @@ importers: version: 2.1.1 connectkit: specifier: ^1.9.1 - version: 1.9.1(@babel/core@7.28.6)(@tanstack/react-query@5.90.21(react@19.2.2))(react-dom@19.2.2(react@19.2.2))(react-is@16.13.1)(react@19.2.2)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.2))(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) + version: 1.9.1(@babel/core@7.28.6)(@tanstack/react-query@5.90.21(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-is@16.13.1)(react@19.2.4)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.4))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) front-matter: specifier: ^4.0.2 version: 4.0.2 fumadocs-core: specifier: ^16.0.10 - version: 16.4.7(@types/react@19.1.2)(lucide-react@0.487.0(react@19.2.2))(next@15.5.9(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.2(react@19.2.2))(react@19.2.2))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod@3.25.76) + version: 16.4.7(@types/react@19.2.14)(lucide-react@0.487.0(react@19.2.4))(next@16.2.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76) fumadocs-mdx: specifier: ^13.0.7 - version: 13.0.8(fumadocs-core@16.4.7(@types/react@19.1.2)(lucide-react@0.487.0(react@19.2.2))(next@15.5.9(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.2(react@19.2.2))(react@19.2.2))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod@3.25.76))(next@15.5.9(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.2(react@19.2.2))(react@19.2.2))(react@19.2.2) + version: 13.0.8(fumadocs-core@16.4.7(@types/react@19.2.14)(lucide-react@0.487.0(react@19.2.4))(next@16.2.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76))(next@16.2.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) lucide-react: specifier: ^0.487.0 - version: 0.487.0(react@19.2.2) + version: 0.487.0(react@19.2.4) next: - specifier: 15.5.9 - version: 15.5.9(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.2(react@19.2.2))(react@19.2.2) + specifier: 16.2.2 + version: 16.2.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-themes: specifier: ^0.4.6 - version: 0.4.6(react-dom@19.2.2(react@19.2.2))(react@19.2.2) + version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) pino-pretty: specifier: ^13.1.2 version: 13.1.3 react: - specifier: 19.2.2 - version: 19.2.2 + specifier: 19.2.4 + version: 19.2.4 react-dom: - specifier: 19.2.2 - version: 19.2.2(react@19.2.2) + specifier: 19.2.4 + version: 19.2.4(react@19.2.4) rehype-pretty-code: specifier: ^0.14.1 version: 0.14.1(shiki@1.29.2) @@ -110,7 +116,7 @@ importers: version: 1.29.2 sonner: specifier: ^2.0.7 - version: 2.0.7(react-dom@19.2.2(react@19.2.2))(react@19.2.2) + version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tailwind-merge: specifier: ^3.3.1 version: 3.4.0 @@ -122,7 +128,7 @@ importers: version: 2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: specifier: ^2.17.4 - version: 2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.2))(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + version: 2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.4))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) zod: specifier: ^3.25.76 version: 3.25.76 @@ -137,11 +143,11 @@ importers: specifier: ^20.19.9 version: 20.19.30 '@types/react': - specifier: 19.1.2 - version: 19.1.2 + specifier: 19.2.14 + version: 19.2.14 '@types/react-dom': - specifier: 19.1.2 - version: 19.1.2(@types/react@19.1.2) + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.14) buffer: specifier: ^6.0.3 version: 6.0.3 @@ -152,8 +158,8 @@ importers: specifier: ^9.32.0 version: 9.39.2(jiti@2.6.1) eslint-config-next: - specifier: 15.3.1 - version: 15.3.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + specifier: 16.2.1 + version: 16.2.1(@typescript-eslint/parser@8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) stream-browserify: specifier: ^3.0.0 version: 3.0.0 @@ -201,6 +207,17 @@ packages: resolution: {integrity: sha512-rVVQx1vISzGFMOxrKTXEgcQqzYzG5rox7fXvf/1ULMuAsQSqu3O2nBvJiYbTnmYIYFwZoBFz0mZskuf7Fm20ow==} engines: {node: '>=18.0.0', npm: '>=9.0.0'} + '@avail-project/nexus-sdk-v2@git+https://git@github.com:availproject/nexus-sdk-v2.git#f4e99ab1d7e96c607d98fef3107f3945cdfbd92a': + resolution: {commit: f4e99ab1d7e96c607d98fef3107f3945cdfbd92a, repo: git@github.com:availproject/nexus-sdk-v2.git, type: git} + version: 0.0.1 + engines: {node: '>=18.0.0', npm: '>=9.0.0'} + + '@avail-project/nexus-types@git+https://git@github.com:availproject/nexus-v2.git#2e7bcad2e9d51fb207aac612af6744d6a11d80f8': + resolution: {commit: 2e7bcad2e9d51fb207aac612af6744d6a11d80f8, repo: git@github.com:availproject/nexus-v2.git, type: git} + version: 1.0.0 + peerDependencies: + zod: '>=4.0.0' + '@babel/code-frame@7.28.6': resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} engines: {node: '>=6.9.0'} @@ -1180,56 +1197,56 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@next/env@15.5.9': - resolution: {integrity: sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==} + '@next/env@16.2.2': + resolution: {integrity: sha512-LqSGz5+xGk9EL/iBDr2yo/CgNQV6cFsNhRR2xhSXYh7B/hb4nePCxlmDvGEKG30NMHDFf0raqSyOZiQrO7BkHQ==} - '@next/eslint-plugin-next@15.3.1': - resolution: {integrity: sha512-oEs4dsfM6iyER3jTzMm4kDSbrQJq8wZw5fmT6fg2V3SMo+kgG+cShzLfEV20senZzv8VF+puNLheiGPlBGsv2A==} + '@next/eslint-plugin-next@16.2.1': + resolution: {integrity: sha512-r0epZGo24eT4g08jJlg2OEryBphXqO8aL18oajoTKLzHJ6jVr6P6FI58DLMug04MwD3j8Fj0YK0slyzneKVyzA==} - '@next/swc-darwin-arm64@15.5.7': - resolution: {integrity: sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==} + '@next/swc-darwin-arm64@16.2.2': + resolution: {integrity: sha512-B92G3ulrwmkDSEJEp9+XzGLex5wC1knrmCSIylyVeiAtCIfvEJYiN3v5kXPlYt5R4RFlsfO/v++aKV63Acrugg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.5.7': - resolution: {integrity: sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==} + '@next/swc-darwin-x64@16.2.2': + resolution: {integrity: sha512-7ZwSgNKJNQiwW0CKhNm9B1WS2L1Olc4B2XY0hPYCAL3epFnugMhuw5TMWzMilQ3QCZcCHoYm9NGWTHbr5REFxw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.5.7': - resolution: {integrity: sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==} + '@next/swc-linux-arm64-gnu@16.2.2': + resolution: {integrity: sha512-c3m8kBHMziMgo2fICOP/cd/5YlrxDU5YYjAJeQLyFsCqVF8xjOTH/QYG4a2u48CvvZZSj1eHQfBCbyh7kBr30Q==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.5.7': - resolution: {integrity: sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==} + '@next/swc-linux-arm64-musl@16.2.2': + resolution: {integrity: sha512-VKLuscm0P/mIfzt+SDdn2+8TNNJ7f0qfEkA+az7OqQbjzKdBxAHs0UvuiVoCtbwX+dqMEL9U54b5wQ/aN3dHeg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.5.7': - resolution: {integrity: sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==} + '@next/swc-linux-x64-gnu@16.2.2': + resolution: {integrity: sha512-kU3OPHJq6sBUjOk7wc5zJ7/lipn8yGldMoAv4z67j6ov6Xo/JvzA7L7LCsyzzsXmgLEhk3Qkpwqaq/1+XpNR3g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.5.7': - resolution: {integrity: sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==} + '@next/swc-linux-x64-musl@16.2.2': + resolution: {integrity: sha512-CKXRILyErMtUftp+coGcZ38ZwE/Aqq45VMCcRLr2I4OXKrgxIBDXHnBgeX/UMil0S09i2JXaDL3Q+TN8D/cKmg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.5.7': - resolution: {integrity: sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==} + '@next/swc-win32-arm64-msvc@16.2.2': + resolution: {integrity: sha512-sS/jSk5VUoShUqINJFvNjVT7JfR5ORYj/+/ZpOYbbIohv/lQfduWnGAycq2wlknbOql2xOR0DoV0s6Xfcy49+g==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.5.7': - resolution: {integrity: sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==} + '@next/swc-win32-x64-msvc@16.2.2': + resolution: {integrity: sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1422,8 +1439,8 @@ packages: '@radix-ui/react-accordion@1.2.12': resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1435,8 +1452,8 @@ packages: '@radix-ui/react-arrow@1.1.7': resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1448,8 +1465,8 @@ packages: '@radix-ui/react-checkbox@1.3.3': resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1461,8 +1478,8 @@ packages: '@radix-ui/react-collapsible@1.1.12': resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1474,8 +1491,8 @@ packages: '@radix-ui/react-collection@1.1.7': resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1487,7 +1504,7 @@ packages: '@radix-ui/react-compose-refs@1.1.2': resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1496,7 +1513,7 @@ packages: '@radix-ui/react-context@1.1.2': resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1505,8 +1522,8 @@ packages: '@radix-ui/react-dialog@1.1.15': resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1518,7 +1535,7 @@ packages: '@radix-ui/react-direction@1.1.1': resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1527,8 +1544,8 @@ packages: '@radix-ui/react-dismissable-layer@1.1.11': resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1540,8 +1557,8 @@ packages: '@radix-ui/react-dropdown-menu@2.1.16': resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1553,7 +1570,7 @@ packages: '@radix-ui/react-focus-guards@1.1.3': resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1562,8 +1579,8 @@ packages: '@radix-ui/react-focus-scope@1.1.7': resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1575,7 +1592,7 @@ packages: '@radix-ui/react-id@1.1.1': resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1584,8 +1601,8 @@ packages: '@radix-ui/react-label@2.1.8': resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1597,8 +1614,8 @@ packages: '@radix-ui/react-menu@2.1.16': resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1610,8 +1627,8 @@ packages: '@radix-ui/react-popover@1.1.15': resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1623,8 +1640,8 @@ packages: '@radix-ui/react-popper@1.2.8': resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1636,8 +1653,8 @@ packages: '@radix-ui/react-portal@1.1.9': resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1649,8 +1666,8 @@ packages: '@radix-ui/react-presence@1.1.5': resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1662,8 +1679,8 @@ packages: '@radix-ui/react-primitive@2.1.3': resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1675,8 +1692,8 @@ packages: '@radix-ui/react-primitive@2.1.4': resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1688,8 +1705,8 @@ packages: '@radix-ui/react-roving-focus@1.1.11': resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1701,8 +1718,8 @@ packages: '@radix-ui/react-select@2.2.6': resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1714,8 +1731,8 @@ packages: '@radix-ui/react-separator@1.1.8': resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1727,7 +1744,7 @@ packages: '@radix-ui/react-slot@1.2.3': resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1736,7 +1753,7 @@ packages: '@radix-ui/react-slot@1.2.4': resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1745,8 +1762,8 @@ packages: '@radix-ui/react-switch@1.2.6': resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1758,8 +1775,8 @@ packages: '@radix-ui/react-tabs@1.1.13': resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1771,8 +1788,8 @@ packages: '@radix-ui/react-toggle-group@1.1.11': resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1784,8 +1801,8 @@ packages: '@radix-ui/react-toggle@1.1.10': resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1797,8 +1814,8 @@ packages: '@radix-ui/react-tooltip@1.2.8': resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1810,7 +1827,7 @@ packages: '@radix-ui/react-use-callback-ref@1.1.1': resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1819,7 +1836,7 @@ packages: '@radix-ui/react-use-controllable-state@1.2.2': resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1828,7 +1845,7 @@ packages: '@radix-ui/react-use-effect-event@0.0.2': resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1837,7 +1854,7 @@ packages: '@radix-ui/react-use-escape-keydown@1.1.1': resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1846,7 +1863,7 @@ packages: '@radix-ui/react-use-layout-effect@1.1.1': resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1855,7 +1872,7 @@ packages: '@radix-ui/react-use-previous@1.1.1': resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1864,7 +1881,7 @@ packages: '@radix-ui/react-use-rect@1.1.1': resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1873,7 +1890,7 @@ packages: '@radix-ui/react-use-size@1.1.1': resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1882,8 +1899,21 @@ packages: '@radix-ui/react-visually-hidden@1.2.3': resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} peerDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-visually-hidden@1.2.4': + resolution: {integrity: sha512-kaeiyGCe844dkb9AVF+rb4yTyb1LiLN/e3es3nLiRyN4dC8AduBYPMnnNlDjX2VDOcvDEiPnRNMJeWCfsX0txg==} + peerDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1927,9 +1957,6 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@rushstack/eslint-patch@1.15.0': - resolution: {integrity: sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==} - '@safe-global/safe-apps-provider@0.18.6': resolution: {integrity: sha512-4LhMmjPWlIO8TTDC2AwLk44XKXaK6hfBTWyljDm0HQ6TWlOEijVWNrt2s3OCVMSxlXAcEzYfqyu1daHZooTC2Q==} @@ -2581,13 +2608,13 @@ packages: '@types/pbkdf2@3.1.2': resolution: {integrity: sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew==} - '@types/react-dom@19.1.2': - resolution: {integrity: sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 - '@types/react@19.1.2': - resolution: {integrity: sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==} + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} '@types/secp256k1@4.0.7': resolution: {integrity: sha512-Rcvjl6vARGAKRO6jHeKMatGrvOMGrR/AR11N1x2LqintPCyDZ7NBhrh238Z2VZc7aM7KIwnFpFQ7fnfK4H/9Qw==} @@ -2613,63 +2640,63 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.53.0': - resolution: {integrity: sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==} + '@typescript-eslint/eslint-plugin@8.57.2': + resolution: {integrity: sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.53.0 - eslint: ^8.57.0 || ^9.0.0 + '@typescript-eslint/parser': ^8.57.2 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.53.0': - resolution: {integrity: sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==} + '@typescript-eslint/parser@8.57.2': + resolution: {integrity: sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.53.0': - resolution: {integrity: sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==} + '@typescript-eslint/project-service@8.57.2': + resolution: {integrity: sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.53.0': - resolution: {integrity: sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==} + '@typescript-eslint/scope-manager@8.57.2': + resolution: {integrity: sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.53.0': - resolution: {integrity: sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==} + '@typescript-eslint/tsconfig-utils@8.57.2': + resolution: {integrity: sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.53.0': - resolution: {integrity: sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw==} + '@typescript-eslint/type-utils@8.57.2': + resolution: {integrity: sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.53.0': - resolution: {integrity: sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==} + '@typescript-eslint/types@8.57.2': + resolution: {integrity: sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.53.0': - resolution: {integrity: sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==} + '@typescript-eslint/typescript-estree@8.57.2': + resolution: {integrity: sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.53.0': - resolution: {integrity: sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==} + '@typescript-eslint/utils@8.57.2': + resolution: {integrity: sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.53.0': - resolution: {integrity: sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==} + '@typescript-eslint/visitor-keys@8.57.2': + resolution: {integrity: sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -3105,6 +3132,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + base-x@3.0.11: resolution: {integrity: sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==} @@ -3114,6 +3145,11 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.12: + resolution: {integrity: sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==} + engines: {node: '>=6.0.0'} + hasBin: true + baseline-browser-mapping@2.9.15: resolution: {integrity: sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==} hasBin: true @@ -3158,6 +3194,10 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -3708,6 +3748,9 @@ packages: es-toolkit@1.44.0: resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==} + es-toolkit@1.45.1: + resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} + es6-promise@4.2.8: resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} @@ -3745,10 +3788,10 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - eslint-config-next@15.3.1: - resolution: {integrity: sha512-GnmyVd9TE/Ihe3RrvcafFhXErErtr2jS0JDeCSp3vWvy86AXwHsRBt0E3MqP/m8ACS1ivcsi5uaqjbhsG18qKw==} + eslint-config-next@16.2.1: + resolution: {integrity: sha512-qhabwjQZ1Mk53XzXvmogf8KQ0tG0CQXF0CZ56+2/lVhmObgmaqj7x5A1DSrWdZd3kwI7GTPGUjFne+krRxYmFg==} peerDependencies: - eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 + eslint: '>=9.0.0' typescript: '>=3.3.1' peerDependenciesMeta: typescript: @@ -3807,9 +3850,9 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - eslint-plugin-react-hooks@5.2.0: - resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} - engines: {node: '>=10'} + eslint-plugin-react-hooks@7.0.1: + resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} + engines: {node: '>=18'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 @@ -3831,6 +3874,10 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint@9.39.2: resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4135,7 +4182,7 @@ packages: '@orama/core': 1.x.x '@oramacloud/client': 2.x.x '@tanstack/react-router': 1.x.x - '@types/react': 19.1.2 + '@types/react': 19.2.14 algoliasearch: 5.x.x lucide-react: '*' next: 16.x.x @@ -4255,6 +4302,10 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + globals@16.4.0: + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -4351,6 +4402,12 @@ packages: help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hey-listen@1.0.8: resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==} @@ -5110,6 +5167,10 @@ packages: minimalistic-crypto-utils@1.0.1: resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -5117,10 +5178,6 @@ packages: resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==} engines: {node: '>=10'} - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -5193,9 +5250,9 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@15.5.9: - resolution: {integrity: sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==} - engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + next@16.2.2: + resolution: {integrity: sha512-i6AJdyVa4oQjyvX/6GeER8dpY/xlIV+4NMv/svykcLtURJSy/WzDnnUk/TM4d0uewFHK7xSQz4TbIwPgjky+3A==} + engines: {node: '>=20.9.0'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -5665,10 +5722,10 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} - react-dom@19.2.2: - resolution: {integrity: sha512-fhyD2BLrew6qYf4NNtHff1rLXvzR25rq49p+FeqByOazc6TcSi2n8EYulo5C1PbH+1uBW++5S1SG7FcUU6mlDg==} + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: - react: ^19.2.2 + react: ^19.2.4 react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -5677,7 +5734,7 @@ packages: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: '@types/react': @@ -5687,7 +5744,7 @@ packages: resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} engines: {node: '>=10'} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -5697,7 +5754,7 @@ packages: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -5718,8 +5775,8 @@ packages: react-dom: optional: true - react@19.2.2: - resolution: {integrity: sha512-BdOGOY8OKRBcgoDkwqA8Q5XvOIhoNx/Sh6BnGJlet2Abt0X5BK0BDrqGyQgLhAVjD2nAg5f6o01u/OPUhG022Q==} + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} readable-stream@2.3.8: @@ -6359,6 +6416,13 @@ packages: typedarray-to-buffer@3.1.5: resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + typescript-eslint@8.57.2: + resolution: {integrity: sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -6502,7 +6566,7 @@ packages: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -6512,7 +6576,7 @@ packages: resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} engines: {node: '>=10'} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -6560,7 +6624,7 @@ packages: resolution: {integrity: sha512-Qik0o+DSy741TmkqmRfjq+0xpZBXi/Y6+fXZLn0xNF1z/waFMbE3rkivv5Zcf9RrMUp6zswf2J7sbh2KBlba5A==} engines: {node: '>=12.20.0'} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 react: '>=16.8' peerDependenciesMeta: '@types/react': @@ -6783,6 +6847,12 @@ packages: peerDependencies: zod: ^3.25 || ^4 + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} @@ -6792,11 +6862,14 @@ packages: zod@4.3.5: resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zustand@5.0.0: resolution: {integrity: sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==} engines: {node: '>=12.20.0'} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 immer: '>=9.0.6' react: '>=18.0.0' use-sync-external-store: '>=1.2.0' @@ -6814,7 +6887,7 @@ packages: resolution: {integrity: sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==} engines: {node: '>=12.20.0'} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 immer: '>=9.0.6' react: '>=18.0.0' use-sync-external-store: '>=1.2.0' @@ -6832,7 +6905,7 @@ packages: resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==} engines: {node: '>=12.20.0'} peerDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 immer: '>=9.0.6' react: '>=18.0.0' use-sync-external-store: '>=1.2.0' @@ -6910,6 +6983,34 @@ snapshots: - utf-8-validate - zod + '@avail-project/nexus-sdk-v2@git+https://git@github.com:availproject/nexus-sdk-v2.git#f4e99ab1d7e96c607d98fef3107f3945cdfbd92a(@opentelemetry/api@1.9.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@avail-project/nexus-types': git+https://git@github.com:availproject/nexus-v2.git#2e7bcad2e9d51fb207aac612af6744d6a11d80f8(zod@4.3.6) + '@opentelemetry/api-logs': 0.208.0 + '@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 + axios: 1.13.2 + buffer: 6.0.3 + decimal.js: 10.6.0 + es-toolkit: 1.45.1 + it-ws: 6.1.5(bufferutil@4.1.0)(utf-8-validate@5.0.10) + long: 5.3.2 + posthog-js: 1.328.0 + viem: 2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + zod: 4.3.6 + transitivePeerDependencies: + - '@opentelemetry/api' + - bufferutil + - debug + - typescript + - utf-8-validate + + '@avail-project/nexus-types@git+https://git@github.com:availproject/nexus-v2.git#2e7bcad2e9d51fb207aac612af6744d6a11d80f8(zod@4.3.6)': + dependencies: + zod: 4.3.6 + '@babel/code-frame@7.28.6': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -7083,7 +7184,7 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@base-org/account@2.4.0(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.2))(utf-8-validate@5.0.10)(zod@3.25.76)': + '@base-org/account@2.4.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@coinbase/cdp-sdk': 1.43.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@noble/hashes': 1.4.0 @@ -7093,7 +7194,7 @@ snapshots: ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) preact: 10.24.2 viem: 2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - zustand: 5.0.3(@types/react@19.1.2)(react@19.2.2)(use-sync-external-store@1.4.0(react@19.2.2)) + zustand: 5.0.3(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)) transitivePeerDependencies: - '@types/react' - bufferutil @@ -7145,7 +7246,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@coinbase/wallet-sdk@4.3.6(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.2))(utf-8-validate@5.0.10)(zod@3.25.76)': + '@coinbase/wallet-sdk@4.3.6(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/hashes': 1.4.0 clsx: 1.2.1 @@ -7154,7 +7255,7 @@ snapshots: ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) preact: 10.24.2 viem: 2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - zustand: 5.0.3(@types/react@19.1.2)(react@19.2.2)(use-sync-external-store@1.4.0(react@19.2.2)) + zustand: 5.0.3(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)) transitivePeerDependencies: - '@types/react' - bufferutil @@ -7527,11 +7628,11 @@ snapshots: '@floating-ui/core': 1.7.5 '@floating-ui/utils': 0.2.11 - '@floating-ui/react-dom@2.1.8(react-dom@19.2.2(react@19.2.2))(react@19.2.2)': + '@floating-ui/react-dom@2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@floating-ui/dom': 1.7.6 - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) '@floating-ui/utils@0.2.11': {} @@ -8037,34 +8138,34 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@next/env@15.5.9': {} + '@next/env@16.2.2': {} - '@next/eslint-plugin-next@15.3.1': + '@next/eslint-plugin-next@16.2.1': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.5.7': + '@next/swc-darwin-arm64@16.2.2': optional: true - '@next/swc-darwin-x64@15.5.7': + '@next/swc-darwin-x64@16.2.2': optional: true - '@next/swc-linux-arm64-gnu@15.5.7': + '@next/swc-linux-arm64-gnu@16.2.2': optional: true - '@next/swc-linux-arm64-musl@15.5.7': + '@next/swc-linux-arm64-musl@16.2.2': optional: true - '@next/swc-linux-x64-gnu@15.5.7': + '@next/swc-linux-x64-gnu@16.2.2': optional: true - '@next/swc-linux-x64-musl@15.5.7': + '@next/swc-linux-x64-musl@16.2.2': optional: true - '@next/swc-win32-arm64-msvc@15.5.7': + '@next/swc-win32-arm64-msvc@16.2.2': optional: true - '@next/swc-win32-x64-msvc@15.5.7': + '@next/swc-win32-x64-msvc@16.2.2': optional: true '@noble/ciphers@1.2.1': {} @@ -8230,490 +8331,499 @@ snapshots: '@radix-ui/primitive@1.1.3': {} - '@radix-ui/react-accordion@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)': + '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collapsible': 1.1.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) - '@radix-ui/react-collection': 1.1.7(@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) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-primitive': 2.1.3(@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) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-arrow@1.1.7(@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)': + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@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) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-checkbox@1.3.3(@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)': + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-presence': 1.1.5(@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) - '@radix-ui/react-primitive': 2.1.3(@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) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-collapsible@1.1.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)': + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-presence': 1.1.5(@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) - '@radix-ui/react-primitive': 2.1.3(@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) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-collection@1.1.7(@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)': + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-primitive': 2.1.3(@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) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.2)(react@19.2.2)': + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)': dependencies: - react: 19.2.2 + react: 19.2.4 optionalDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 - '@radix-ui/react-context@1.1.2(@types/react@19.1.2)(react@19.2.2)': + '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)': dependencies: - react: 19.2.2 + react: 19.2.4 optionalDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 - '@radix-ui/react-dialog@1.1.15(@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)': + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-dismissable-layer': 1.1.11(@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) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-focus-scope': 1.1.7(@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) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-portal': 1.1.9(@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) - '@radix-ui/react-presence': 1.1.5(@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) - '@radix-ui/react-primitive': 2.1.3(@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) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.2.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) aria-hidden: 1.2.6 - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) - react-remove-scroll: 2.7.2(@types/react@19.1.2)(react@19.2.2) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-direction@1.1.1(@types/react@19.1.2)(react@19.2.2)': + '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: - react: 19.2.2 + react: 19.2.4 optionalDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 - '@radix-ui/react-dismissable-layer@1.1.11(@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)': + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-primitive': 2.1.3(@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) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-dropdown-menu@2.1.16(@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)': + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-menu': 2.1.16(@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) - '@radix-ui/react-primitive': 2.1.3(@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) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-focus-guards@1.1.3(@types/react@19.1.2)(react@19.2.2)': + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.4)': dependencies: - react: 19.2.2 + react: 19.2.4 optionalDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 - '@radix-ui/react-focus-scope@1.1.7(@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)': + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-primitive': 2.1.3(@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) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-id@1.1.1(@types/react@19.1.2)(react@19.2.2)': + '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 optionalDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 - '@radix-ui/react-label@2.1.8(@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)': + '@radix-ui/react-label@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-primitive': 2.1.4(@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) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-menu@2.1.16(@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)': + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@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) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-dismissable-layer': 1.1.11(@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) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-focus-scope': 1.1.7(@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) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-popper': 1.2.8(@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) - '@radix-ui/react-portal': 1.1.9(@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) - '@radix-ui/react-presence': 1.1.5(@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) - '@radix-ui/react-primitive': 2.1.3(@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) - '@radix-ui/react-roving-focus': 1.1.11(@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) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.2.2) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) aria-hidden: 1.2.6 - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) - react-remove-scroll: 2.7.2(@types/react@19.1.2)(react@19.2.2) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-popover@1.1.15(@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)': + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-dismissable-layer': 1.1.11(@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) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-focus-scope': 1.1.7(@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) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-popper': 1.2.8(@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) - '@radix-ui/react-portal': 1.1.9(@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) - '@radix-ui/react-presence': 1.1.5(@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) - '@radix-ui/react-primitive': 2.1.3(@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) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.2.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) aria-hidden: 1.2.6 - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) - react-remove-scroll: 2.7.2(@types/react@19.1.2)(react@19.2.2) - optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) - - '@radix-ui/react-popper@1.2.8(@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)': - dependencies: - '@floating-ui/react-dom': 2.1.8(react-dom@19.2.2(react@19.2.2))(react@19.2.2) - '@radix-ui/react-arrow': 1.1.7(@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) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-primitive': 2.1.3(@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) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-use-rect': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.2)(react@19.2.2) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) '@radix-ui/rect': 1.1.1 - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-portal@1.1.9(@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)': + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@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) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-presence@1.1.5(@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)': + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-primitive@2.1.3(@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)': + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-primitive@2.1.4(@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)': + '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-slot': 1.2.4(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-roving-focus@1.1.11(@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)': + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@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) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-primitive': 2.1.3(@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) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) - optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) - - '@radix-ui/react-select@2.2.6(@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)': + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@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) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-dismissable-layer': 1.1.11(@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) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-focus-scope': 1.1.7(@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) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-popper': 1.2.8(@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) - '@radix-ui/react-portal': 1.1.9(@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) - '@radix-ui/react-primitive': 2.1.3(@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) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-visually-hidden': 1.2.3(@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) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) aria-hidden: 1.2.6 - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) - react-remove-scroll: 2.7.2(@types/react@19.1.2)(react@19.2.2) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-separator@1.1.8(@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)': + '@radix-ui/react-separator@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-primitive': 2.1.4(@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) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-slot@1.2.3(@types/react@19.1.2)(react@19.2.2)': + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 optionalDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 - '@radix-ui/react-slot@1.2.4(@types/react@19.1.2)(react@19.2.2)': + '@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 optionalDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 - '@radix-ui/react-switch@1.2.6(@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)': + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-primitive': 2.1.3(@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) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-tabs@1.1.13(@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)': + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-presence': 1.1.5(@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) - '@radix-ui/react-primitive': 2.1.3(@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) - '@radix-ui/react-roving-focus': 1.1.11(@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) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-toggle-group@1.1.11(@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)': + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-primitive': 2.1.3(@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) - '@radix-ui/react-roving-focus': 1.1.11(@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) - '@radix-ui/react-toggle': 1.1.10(@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) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-toggle@1.1.10(@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)': + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-primitive': 2.1.3(@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) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-tooltip@1.2.8(@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)': + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-dismissable-layer': 1.1.11(@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) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-popper': 1.2.8(@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) - '@radix-ui/react-portal': 1.1.9(@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) - '@radix-ui/react-presence': 1.1.5(@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) - '@radix-ui/react-primitive': 2.1.3(@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) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-visually-hidden': 1.2.3(@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) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.2)(react@19.2.2)': + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: - react: 19.2.2 + react: 19.2.4 optionalDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.2)(react@19.2.2)': + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.2)(react@19.2.2) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 optionalDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 - '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.2)(react@19.2.2)': + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 optionalDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.1.2)(react@19.2.2)': + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 optionalDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.2)(react@19.2.2)': + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: - react: 19.2.2 + react: 19.2.4 optionalDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 - '@radix-ui/react-use-previous@1.1.1(@types/react@19.1.2)(react@19.2.2)': + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: - react: 19.2.2 + react: 19.2.4 optionalDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 - '@radix-ui/react-use-rect@1.1.1(@types/react@19.1.2)(react@19.2.2)': + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: '@radix-ui/rect': 1.1.1 - react: 19.2.2 + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 optionalDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 - '@radix-ui/react-use-size@1.1.1(@types/react@19.1.2)(react@19.2.2)': + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.2.2) - react: 19.2.2 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-visually-hidden@1.2.3(@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)': + '@radix-ui/react-visually-hidden@1.2.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@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) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) '@radix-ui/rect@1.1.1': {} @@ -8739,12 +8849,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-controllers@1.7.8(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-controllers@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/universal-provider': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - valtio: 1.13.2(@types/react@19.1.2)(react@19.2.2) + valtio: 1.13.2(@types/react@19.2.14)(react@19.2.4) viem: 2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' @@ -8774,14 +8884,14 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.7.8(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-pay@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-ui': 1.7.8(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-utils': 1.7.8(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.2)(react@19.2.2))(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76) lit: 3.3.0 - valtio: 1.13.2(@types/react@19.1.2)(react@19.2.2) + valtio: 1.13.2(@types/react@19.2.14)(react@19.2.4) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -8814,12 +8924,12 @@ snapshots: dependencies: buffer: 6.0.3 - '@reown/appkit-scaffold-ui@1.7.8(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.2)(react@19.2.2))(zod@3.25.76)': + '@reown/appkit-scaffold-ui@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-ui': 1.7.8(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-utils': 1.7.8(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.2)(react@19.2.2))(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 transitivePeerDependencies: @@ -8851,10 +8961,10 @@ snapshots: - valtio - zod - '@reown/appkit-ui@1.7.8(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-ui@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 qrcode: 1.5.3 @@ -8886,15 +8996,15 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.7.8(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.2)(react@19.2.2))(zod@3.25.76)': + '@reown/appkit-utils@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.7.8 '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/logger': 2.1.2 '@walletconnect/universal-provider': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - valtio: 1.13.2(@types/react@19.1.2)(react@19.2.2) + valtio: 1.13.2(@types/react@19.2.14)(react@19.2.4) viem: 2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' @@ -8935,20 +9045,20 @@ snapshots: - typescript - utf-8-validate - '@reown/appkit@1.7.8(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-pay': 1.7.8(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-pay': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.7.8 - '@reown/appkit-scaffold-ui': 1.7.8(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.2)(react@19.2.2))(zod@3.25.76) - '@reown/appkit-ui': 1.7.8(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-utils': 1.7.8(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.2)(react@19.2.2))(zod@3.25.76) + '@reown/appkit-scaffold-ui': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/types': 2.21.0 '@walletconnect/universal-provider': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) bs58: 6.0.0 - valtio: 1.13.2(@types/react@19.1.2)(react@19.2.2) + valtio: 1.13.2(@types/react@19.2.14)(react@19.2.4) viem: 2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' @@ -8980,8 +9090,6 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@rushstack/eslint-patch@1.15.0': {} - '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) @@ -9699,10 +9807,10 @@ snapshots: '@tanstack/query-core@5.90.20': {} - '@tanstack/react-query@5.90.21(react@19.2.2)': + '@tanstack/react-query@5.90.21(react@19.2.4)': dependencies: '@tanstack/query-core': 5.90.20 - react: 19.2.2 + react: 19.2.4 '@tronweb3/tronwallet-abstract-adapter@1.1.10(bufferutil@4.1.0)(utf-8-validate@5.0.10)': dependencies: @@ -9783,11 +9891,11 @@ snapshots: dependencies: '@types/node': 20.19.30 - '@types/react-dom@19.1.2(@types/react@19.1.2)': + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 - '@types/react@19.1.2': + '@types/react@19.2.14': dependencies: csstype: 3.2.3 @@ -9813,14 +9921,14 @@ snapshots: dependencies: '@types/node': 20.19.30 - '@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.53.0 - '@typescript-eslint/type-utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.53.0 + '@typescript-eslint/parser': 8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/type-utils': 8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.2 eslint: 9.39.2(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 @@ -9829,41 +9937,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.53.0 - '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.53.0 + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.2 debug: 4.4.3(supports-color@5.5.0) eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.53.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.57.2(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.9.3) - '@typescript-eslint/types': 8.53.0 + '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3) + '@typescript-eslint/types': 8.57.2 debug: 4.4.3(supports-color@5.5.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.53.0': + '@typescript-eslint/scope-manager@8.57.2': dependencies: - '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/visitor-keys': 8.53.0 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/visitor-keys': 8.57.2 - '@typescript-eslint/tsconfig-utils@8.53.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.57.2(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3(supports-color@5.5.0) eslint: 9.39.2(jiti@2.6.1) ts-api-utils: 2.4.0(typescript@5.9.3) @@ -9871,16 +9979,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.53.0': {} + '@typescript-eslint/types@8.57.2': {} - '@typescript-eslint/typescript-estree@8.53.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.53.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.9.3) - '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/visitor-keys': 8.53.0 + '@typescript-eslint/project-service': 8.57.2(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3) + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/visitor-keys': 8.57.2 debug: 4.4.3(supports-color@5.5.0) - minimatch: 9.0.5 + minimatch: 10.2.4 semver: 7.7.3 tinyglobby: 0.2.15 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -9888,21 +9996,21 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.53.0 - '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.53.0': + '@typescript-eslint/visitor-keys@8.57.2': dependencies: - '@typescript-eslint/types': 8.53.0 - eslint-visitor-keys: 4.2.1 + '@typescript-eslint/types': 8.57.2 + eslint-visitor-keys: 5.0.1 '@ungap/structured-clone@1.3.0': {} @@ -9965,18 +10073,18 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@wagmi/connectors@6.2.0(@tanstack/react-query@5.90.21(react@19.2.2))(@types/react@19.1.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.1.2)(react@19.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.2))(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.2))(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.2))(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76)': + '@wagmi/connectors@6.2.0(@tanstack/react-query@5.90.21(react@19.2.4))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.4))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76)': dependencies: - '@base-org/account': 2.4.0(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.2))(utf-8-validate@5.0.10)(zod@3.25.76) - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.2))(utf-8-validate@5.0.10)(zod@3.25.76) + '@base-org/account': 2.4.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(zod@3.25.76) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(zod@3.25.76) '@gemini-wallet/core': 0.3.2(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@metamask/sdk': 0.33.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.1.2)(react@19.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.2))(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.35(@tanstack/react-query@5.90.21(react@19.2.2))(@types/react@19.1.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.1.2)(react@19.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.2))(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.2))(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.2))(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) + porto: 0.2.35(@tanstack/react-query@5.90.21(react@19.2.4))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.4))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) viem: 2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: typescript: 5.9.3 @@ -10018,12 +10126,12 @@ snapshots: - wagmi - zod - '@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.1.2)(react@19.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.2))(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': + '@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.9.3) viem: 2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - zustand: 5.0.0(@types/react@19.1.2)(react@19.2.2)(use-sync-external-store@1.4.0(react@19.2.2)) + zustand: 5.0.0(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)) optionalDependencies: '@tanstack/query-core': 5.90.20 typescript: 5.9.3 @@ -10125,9 +10233,9 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/ethereum-provider@2.21.1(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/ethereum-provider@2.21.1(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit': 1.7.8(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/jsonrpc-http-connection': 1.0.8 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 @@ -10590,6 +10698,11 @@ snapshots: typescript: 5.9.3 zod: 3.25.76 + abitype@1.1.0(typescript@5.9.3)(zod@4.3.6): + optionalDependencies: + typescript: 5.9.3 + zod: 4.3.6 + abitype@1.2.3(typescript@5.9.3)(zod@3.25.76): optionalDependencies: typescript: 5.9.3 @@ -10798,14 +10911,14 @@ snapshots: axobject-query@4.1.0: {} - babel-plugin-styled-components@2.1.4(@babel/core@7.28.6)(styled-components@5.3.11(@babel/core@7.28.6)(react-dom@19.2.2(react@19.2.2))(react-is@16.13.1)(react@19.2.2))(supports-color@5.5.0): + babel-plugin-styled-components@2.1.4(@babel/core@7.28.6)(styled-components@5.3.11(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react-is@16.13.1)(react@19.2.4))(supports-color@5.5.0): dependencies: '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-module-imports': 7.28.6(supports-color@5.5.0) '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.28.6) lodash: 4.17.21 picomatch: 2.3.1 - styled-components: 5.3.11(@babel/core@7.28.6)(react-dom@19.2.2(react@19.2.2))(react-is@16.13.1)(react@19.2.2) + styled-components: 5.3.11(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react-is@16.13.1)(react@19.2.4) transitivePeerDependencies: - '@babel/core' - supports-color @@ -10814,6 +10927,8 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + base-x@3.0.11: dependencies: safe-buffer: 5.2.1 @@ -10822,6 +10937,8 @@ snapshots: base64-js@1.5.1: {} + baseline-browser-mapping@2.10.12: {} + baseline-browser-mapping@2.9.15: {} bech32@1.1.4: {} @@ -10877,6 +10994,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -11078,22 +11199,22 @@ snapshots: concat-map@0.0.1: {} - connectkit@1.9.1(@babel/core@7.28.6)(@tanstack/react-query@5.90.21(react@19.2.2))(react-dom@19.2.2(react@19.2.2))(react-is@16.13.1)(react@19.2.2)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.2))(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)): + connectkit@1.9.1(@babel/core@7.28.6)(@tanstack/react-query@5.90.21(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-is@16.13.1)(react@19.2.4)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.4))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)): dependencies: - '@tanstack/react-query': 5.90.21(react@19.2.2) + '@tanstack/react-query': 5.90.21(react@19.2.4) buffer: 6.0.3 detect-browser: 5.3.0 - family: 0.1.6(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.2))(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) - framer-motion: 6.5.1(react-dom@19.2.2(react@19.2.2))(react@19.2.2) + family: 0.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.4))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) + framer-motion: 6.5.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) qrcode: 1.5.4 - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) - react-transition-state: 1.1.5(react-dom@19.2.2(react@19.2.2))(react@19.2.2) - react-use-measure: 2.1.7(react-dom@19.2.2(react@19.2.2))(react@19.2.2) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-transition-state: 1.1.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-use-measure: 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) resize-observer-polyfill: 1.5.1 - styled-components: 5.3.11(@babel/core@7.28.6)(react-dom@19.2.2(react@19.2.2))(react-is@16.13.1)(react@19.2.2) + styled-components: 5.3.11(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react-is@16.13.1)(react@19.2.4) viem: 2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - wagmi: 2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.2))(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + wagmi: 2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.4))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) transitivePeerDependencies: - '@babel/core' - react-is @@ -11291,9 +11412,9 @@ snapshots: dequal@2.0.3: {} - derive-valtio@0.1.0(valtio@1.13.2(@types/react@19.1.2)(react@19.2.2)): + derive-valtio@0.1.0(valtio@1.13.2(@types/react@19.2.14)(react@19.2.4)): dependencies: - valtio: 1.13.2(@types/react@19.1.2)(react@19.2.2) + valtio: 1.13.2(@types/react@19.2.14)(react@19.2.4) des.js@1.1.0: dependencies: @@ -11515,6 +11636,8 @@ snapshots: es-toolkit@1.44.0: {} + es-toolkit@1.45.1: {} + es6-promise@4.2.8: {} es6-promisify@5.0.0: @@ -11601,22 +11724,22 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-next@15.3.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + eslint-config-next@16.2.1(@typescript-eslint/parser@8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@next/eslint-plugin-next': 15.3.1 - '@rushstack/eslint-patch': 1.15.0 - '@typescript-eslint/eslint-plugin': 8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@next/eslint-plugin-next': 16.2.1 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1)) + globals: 16.4.0 + typescript-eslint: 8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: + - '@typescript-eslint/parser' - eslint-import-resolver-webpack - eslint-plugin-import-x - supports-color @@ -11640,22 +11763,22 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -11666,7 +11789,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -11678,7 +11801,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -11703,9 +11826,16 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@5.2.0(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@2.6.1)): dependencies: + '@babel/core': 7.28.6 + '@babel/parser': 7.28.6 eslint: 9.39.2(jiti@2.6.1) + hermes-parser: 0.25.1 + zod: 3.25.76 + zod-validation-error: 4.0.2(zod@3.25.76) + transitivePeerDependencies: + - supports-color eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)): dependencies: @@ -11738,6 +11868,8 @@ snapshots: eslint-visitor-keys@4.2.1: {} + eslint-visitor-keys@5.0.1: {} + eslint@9.39.2(jiti@2.6.1): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) @@ -12001,12 +12133,12 @@ snapshots: eyes@0.1.8: {} - family@0.1.6(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.2))(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)): + family@0.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.4))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)): optionalDependencies: - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) viem: 2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - wagmi: 2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.2))(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + wagmi: 2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.4))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) fast-copy@4.0.2: {} @@ -12113,14 +12245,14 @@ snapshots: forwarded@0.2.0: {} - framer-motion@6.5.1(react-dom@19.2.2(react@19.2.2))(react@19.2.2): + framer-motion@6.5.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@motionone/dom': 10.12.0 framesync: 6.0.1 hey-listen: 1.0.8 popmotion: 11.0.3 - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) style-value-types: 5.0.0 tslib: 2.8.1 optionalDependencies: @@ -12145,7 +12277,7 @@ snapshots: fsevents@2.3.3: optional: true - fumadocs-core@16.4.7(@types/react@19.1.2)(lucide-react@0.487.0(react@19.2.2))(next@15.5.9(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.2(react@19.2.2))(react@19.2.2))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod@3.25.76): + fumadocs-core@16.4.7(@types/react@19.2.14)(lucide-react@0.487.0(react@19.2.4))(next@16.2.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76): dependencies: '@formatjs/intl-localematcher': 0.7.5 '@orama/orama': 3.1.18 @@ -12167,23 +12299,23 @@ snapshots: tinyglobby: 0.2.15 unist-util-visit: 5.0.0 optionalDependencies: - '@types/react': 19.1.2 - lucide-react: 0.487.0(react@19.2.2) - next: 15.5.9(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.2(react@19.2.2))(react@19.2.2) - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + '@types/react': 19.2.14 + lucide-react: 0.487.0(react@19.2.4) + next: 16.2.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) zod: 3.25.76 transitivePeerDependencies: - supports-color - fumadocs-mdx@13.0.8(fumadocs-core@16.4.7(@types/react@19.1.2)(lucide-react@0.487.0(react@19.2.2))(next@15.5.9(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.2(react@19.2.2))(react@19.2.2))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod@3.25.76))(next@15.5.9(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.2(react@19.2.2))(react@19.2.2))(react@19.2.2): + fumadocs-mdx@13.0.8(fumadocs-core@16.4.7(@types/react@19.2.14)(lucide-react@0.487.0(react@19.2.4))(next@16.2.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76))(next@16.2.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 chokidar: 4.0.3 esbuild: 0.25.12 estree-util-value-to-estree: 3.5.0 - fumadocs-core: 16.4.7(@types/react@19.1.2)(lucide-react@0.487.0(react@19.2.2))(next@15.5.9(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.2(react@19.2.2))(react@19.2.2))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod@3.25.76) + fumadocs-core: 16.4.7(@types/react@19.2.14)(lucide-react@0.487.0(react@19.2.4))(next@16.2.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76) js-yaml: 4.1.1 lru-cache: 11.2.4 mdast-util-to-markdown: 2.1.2 @@ -12197,8 +12329,8 @@ snapshots: unist-util-visit: 5.0.0 zod: 4.3.5 optionalDependencies: - next: 15.5.9(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.2(react@19.2.2))(react@19.2.2) - react: 19.2.2 + next: 16.2.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 transitivePeerDependencies: - supports-color @@ -12267,6 +12399,8 @@ snapshots: globals@14.0.0: {} + globals@16.4.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -12432,6 +12566,12 @@ snapshots: help-me@5.0.0: {} + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + hey-listen@1.0.8: {} hmac-drbg@1.0.1: @@ -12917,9 +13057,9 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-react@0.487.0(react@19.2.2): + lucide-react@0.487.0(react@19.2.4): dependencies: - react: 19.2.2 + react: 19.2.4 magic-string@0.30.21: dependencies: @@ -13412,6 +13552,10 @@ snapshots: minimalistic-crypto-utils@1.0.1: {} + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.5 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -13420,10 +13564,6 @@ snapshots: dependencies: brace-expansion: 2.0.2 - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.2 - minimist@1.2.8: {} mipd@0.0.7(typescript@5.9.3): @@ -13491,29 +13631,30 @@ snapshots: negotiator@1.0.0: {} - next-themes@0.4.6(react-dom@19.2.2(react@19.2.2))(react@19.2.2): + next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - next@15.5.9(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.2(react@19.2.2))(react@19.2.2): + next@16.2.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@next/env': 15.5.9 + '@next/env': 16.2.2 '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.10.12 caniuse-lite: 1.0.30001765 postcss: 8.4.31 - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) - styled-jsx: 5.1.6(@babel/core@7.28.6)(react@19.2.2) - optionalDependencies: - '@next/swc-darwin-arm64': 15.5.7 - '@next/swc-darwin-x64': 15.5.7 - '@next/swc-linux-arm64-gnu': 15.5.7 - '@next/swc-linux-arm64-musl': 15.5.7 - '@next/swc-linux-x64-gnu': 15.5.7 - '@next/swc-linux-x64-musl': 15.5.7 - '@next/swc-win32-arm64-msvc': 15.5.7 - '@next/swc-win32-x64-msvc': 15.5.7 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + styled-jsx: 5.1.6(@babel/core@7.28.6)(react@19.2.4) + optionalDependencies: + '@next/swc-darwin-arm64': 16.2.2 + '@next/swc-darwin-x64': 16.2.2 + '@next/swc-linux-arm64-gnu': 16.2.2 + '@next/swc-linux-arm64-musl': 16.2.2 + '@next/swc-linux-x64-gnu': 16.2.2 + '@next/swc-linux-x64-musl': 16.2.2 + '@next/swc-win32-arm64-msvc': 16.2.2 + '@next/swc-win32-x64-msvc': 16.2.2 '@opentelemetry/api': 1.9.0 sharp: 0.34.5 transitivePeerDependencies: @@ -13758,6 +13899,21 @@ snapshots: transitivePeerDependencies: - zod + ox@0.9.6(typescript@5.9.3)(zod@4.3.6): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.3)(zod@4.3.6) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - zod + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -13902,21 +14058,21 @@ snapshots: style-value-types: 5.0.0 tslib: 2.8.1 - porto@0.2.35(@tanstack/react-query@5.90.21(react@19.2.2))(@types/react@19.1.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.1.2)(react@19.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.2))(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.2))(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.2))(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)): + porto@0.2.35(@tanstack/react-query@5.90.21(react@19.2.4))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.4))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)): dependencies: - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.1.2)(react@19.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.2))(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) hono: 4.11.4 idb-keyval: 6.2.2 mipd: 0.0.7(typescript@5.9.3) ox: 0.9.17(typescript@5.9.3)(zod@4.3.5) viem: 2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zod: 4.3.5 - zustand: 5.0.10(@types/react@19.1.2)(react@19.2.2)(use-sync-external-store@1.4.0(react@19.2.2)) + zustand: 5.0.10(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)) optionalDependencies: - '@tanstack/react-query': 5.90.21(react@19.2.2) - react: 19.2.2 + '@tanstack/react-query': 5.90.21(react@19.2.4) + react: 19.2.4 typescript: 5.9.3 - wagmi: 2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.2))(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + wagmi: 2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.4))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) transitivePeerDependencies: - '@types/react' - immer @@ -14067,52 +14223,52 @@ snapshots: iconv-lite: 0.7.2 unpipe: 1.0.0 - react-dom@19.2.2(react@19.2.2): + react-dom@19.2.4(react@19.2.4): dependencies: - react: 19.2.2 + react: 19.2.4 scheduler: 0.27.0 react-is@16.13.1: {} - react-remove-scroll-bar@2.3.8(@types/react@19.1.2)(react@19.2.2): + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4): dependencies: - react: 19.2.2 - react-style-singleton: 2.2.3(@types/react@19.1.2)(react@19.2.2) + react: 19.2.4 + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) tslib: 2.8.1 optionalDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 - react-remove-scroll@2.7.2(@types/react@19.1.2)(react@19.2.2): + react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.4): dependencies: - react: 19.2.2 - react-remove-scroll-bar: 2.3.8(@types/react@19.1.2)(react@19.2.2) - react-style-singleton: 2.2.3(@types/react@19.1.2)(react@19.2.2) + react: 19.2.4 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.4) + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.1.2)(react@19.2.2) - use-sidecar: 1.1.3(@types/react@19.1.2)(react@19.2.2) + use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.4) + use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 - react-style-singleton@2.2.3(@types/react@19.1.2)(react@19.2.2): + react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.4): dependencies: get-nonce: 1.0.1 - react: 19.2.2 + react: 19.2.4 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 - react-transition-state@1.1.5(react-dom@19.2.2(react@19.2.2))(react@19.2.2): + react-transition-state@1.1.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - react-use-measure@2.1.7(react-dom@19.2.2(react@19.2.2))(react@19.2.2): + react-use-measure@2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - react: 19.2.2 + react: 19.2.4 optionalDependencies: - react-dom: 19.2.2(react@19.2.2) + react-dom: 19.2.4(react@19.2.4) - react@19.2.2: {} + react@19.2.4: {} readable-stream@2.3.8: dependencies: @@ -14623,10 +14779,10 @@ snapshots: dependencies: atomic-sleep: 1.0.0 - sonner@2.0.7(react-dom@19.2.2(react@19.2.2))(react@19.2.2): + sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) source-map-js@1.2.1: {} @@ -14776,28 +14932,28 @@ snapshots: hey-listen: 1.0.8 tslib: 2.8.1 - styled-components@5.3.11(@babel/core@7.28.6)(react-dom@19.2.2(react@19.2.2))(react-is@16.13.1)(react@19.2.2): + styled-components@5.3.11(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react-is@16.13.1)(react@19.2.4): dependencies: '@babel/helper-module-imports': 7.28.6(supports-color@5.5.0) '@babel/traverse': 7.28.6(supports-color@5.5.0) '@emotion/is-prop-valid': 1.4.0 '@emotion/stylis': 0.8.5 '@emotion/unitless': 0.7.5 - babel-plugin-styled-components: 2.1.4(@babel/core@7.28.6)(styled-components@5.3.11(@babel/core@7.28.6)(react-dom@19.2.2(react@19.2.2))(react-is@16.13.1)(react@19.2.2))(supports-color@5.5.0) + babel-plugin-styled-components: 2.1.4(@babel/core@7.28.6)(styled-components@5.3.11(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react-is@16.13.1)(react@19.2.4))(supports-color@5.5.0) css-to-react-native: 3.2.0 hoist-non-react-statics: 3.3.2 - react: 19.2.2 - react-dom: 19.2.2(react@19.2.2) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) react-is: 16.13.1 shallowequal: 1.1.0 supports-color: 5.5.0 transitivePeerDependencies: - '@babel/core' - styled-jsx@5.1.6(@babel/core@7.28.6)(react@19.2.2): + styled-jsx@5.1.6(@babel/core@7.28.6)(react@19.2.4): dependencies: client-only: 0.0.1 - react: 19.2.2 + react: 19.2.4 optionalDependencies: '@babel/core': 7.28.6 @@ -14990,6 +15146,17 @@ snapshots: dependencies: is-typedarray: 1.0.0 + typescript-eslint@8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + typescript@5.9.3: {} ufo@1.6.3: {} @@ -15112,28 +15279,28 @@ snapshots: dependencies: punycode: 2.3.1 - use-callback-ref@1.3.3(@types/react@19.1.2)(react@19.2.2): + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4): dependencies: - react: 19.2.2 + react: 19.2.4 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 - use-sidecar@1.1.3(@types/react@19.1.2)(react@19.2.2): + use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.4): dependencies: detect-node-es: 1.1.0 - react: 19.2.2 + react: 19.2.4 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.1.2 + '@types/react': 19.2.14 - use-sync-external-store@1.2.0(react@19.2.2): + use-sync-external-store@1.2.0(react@19.2.4): dependencies: - react: 19.2.2 + react: 19.2.4 - use-sync-external-store@1.4.0(react@19.2.2): + use-sync-external-store@1.4.0(react@19.2.4): dependencies: - react: 19.2.2 + react: 19.2.4 utf-8-validate@5.0.10: dependencies: @@ -15159,14 +15326,14 @@ snapshots: validator@13.15.23: {} - valtio@1.13.2(@types/react@19.1.2)(react@19.2.2): + valtio@1.13.2(@types/react@19.2.14)(react@19.2.4): dependencies: - derive-valtio: 0.1.0(valtio@1.13.2(@types/react@19.1.2)(react@19.2.2)) + derive-valtio: 0.1.0(valtio@1.13.2(@types/react@19.2.14)(react@19.2.4)) proxy-compare: 2.6.0 - use-sync-external-store: 1.2.0(react@19.2.2) + use-sync-external-store: 1.2.0(react@19.2.4) optionalDependencies: - '@types/react': 19.1.2 - react: 19.2.2 + '@types/react': 19.2.14 + react: 19.2.4 vary@1.1.2: {} @@ -15236,13 +15403,30 @@ snapshots: - utf-8-validate - zod - wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.2))(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76): + viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6): dependencies: - '@tanstack/react-query': 5.90.21(react@19.2.2) - '@wagmi/connectors': 6.2.0(@tanstack/react-query@5.90.21(react@19.2.2))(@types/react@19.1.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.1.2)(react@19.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.2))(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.2))(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.2))(@types/react@19.1.2)(bufferutil@4.1.0)(react@19.2.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.1.2)(react@19.2.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.2))(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - react: 19.2.2 - use-sync-external-store: 1.4.0(react@19.2.2) + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.3)(zod@4.3.6) + isows: 1.0.7(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + ox: 0.9.6(typescript@5.9.3)(zod@4.3.6) + ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.4))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76): + dependencies: + '@tanstack/react-query': 5.90.21(react@19.2.4) + '@wagmi/connectors': 6.2.0(@tanstack/react-query@5.90.21(react@19.2.4))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.4))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + react: 19.2.4 + use-sync-external-store: 1.4.0(react@19.2.4) viem: 2.38.3(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: typescript: 5.9.3 @@ -15444,28 +15628,34 @@ snapshots: dependencies: zod: 3.25.76 + zod-validation-error@4.0.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod@3.22.4: {} zod@3.25.76: {} zod@4.3.5: {} - zustand@5.0.0(@types/react@19.1.2)(react@19.2.2)(use-sync-external-store@1.4.0(react@19.2.2)): + zod@4.3.6: {} + + zustand@5.0.0(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)): optionalDependencies: - '@types/react': 19.1.2 - react: 19.2.2 - use-sync-external-store: 1.4.0(react@19.2.2) + '@types/react': 19.2.14 + react: 19.2.4 + use-sync-external-store: 1.4.0(react@19.2.4) - zustand@5.0.10(@types/react@19.1.2)(react@19.2.2)(use-sync-external-store@1.4.0(react@19.2.2)): + zustand@5.0.10(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)): optionalDependencies: - '@types/react': 19.1.2 - react: 19.2.2 - use-sync-external-store: 1.4.0(react@19.2.2) + '@types/react': 19.2.14 + react: 19.2.4 + use-sync-external-store: 1.4.0(react@19.2.4) - zustand@5.0.3(@types/react@19.1.2)(react@19.2.2)(use-sync-external-store@1.4.0(react@19.2.2)): + zustand@5.0.3(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)): optionalDependencies: - '@types/react': 19.1.2 - react: 19.2.2 - use-sync-external-store: 1.4.0(react@19.2.2) + '@types/react': 19.2.14 + react: 19.2.4 + use-sync-external-store: 1.4.0(react@19.2.4) zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0ca65b9..2c66ce2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,5 @@ onlyBuiltDependencies: - "@avail-project/ca-common" - "@avail-project/nexus-core" + - "@avail-project/nexus-sdk-v2" + - "@avail-project/nexus-types" diff --git a/providers/Web3Provider.tsx b/providers/Web3Provider.tsx index c8e5402..bb7c726 100644 --- a/providers/Web3Provider.tsx +++ b/providers/Web3Provider.tsx @@ -21,7 +21,7 @@ import { import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; import { Chain, defineChain } from "viem"; import NexusProvider from "@/registry/nexus-elements/nexus/NexusProvider"; -import { type NexusNetwork } from "@avail-project/nexus-core"; +import { type NexusNetwork } from "@avail-project/nexus-sdk-v2"; import { Suspense, useMemo, useState, useEffect } from "react"; import { Skeleton } from "@/registry/nexus-elements/ui/skeleton"; import { getItem, setItem } from "@/lib/local-storage"; @@ -130,7 +130,7 @@ const wagmiConfig = createConfig(defaultConfig); export const NETWORK_KEY = "nexus-elements-network-key"; function NexusContainer({ children }: Readonly<{ children: React.ReactNode }>) { - const [network, setNetwork] = useState("mainnet"); + const [network, setNetwork] = useState("testnet"); const [isInitialized, setIsInitialized] = useState(false); useEffect(() => { @@ -144,8 +144,8 @@ function NexusContainer({ children }: Readonly<{ children: React.ReactNode }>) { setNetwork(storedNetwork); } else { // Set default to mainnet if not found or invalid - setNetwork("mainnet"); - setItem(NETWORK_KEY, "mainnet"); + setNetwork("testnet"); + setItem(NETWORK_KEY, "testnet"); } setIsInitialized(true); @@ -153,7 +153,7 @@ function NexusContainer({ children }: Readonly<{ children: React.ReactNode }>) { const nexusConfig = useMemo( () => ({ network: network, debug: true }), - [network] + [network], ); // Don't render until we've initialized from localStorage diff --git a/middleware.ts b/proxy.ts similarity index 98% rename from middleware.ts rename to proxy.ts index 557c59b..5f953b3 100644 --- a/middleware.ts +++ b/proxy.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; -export function middleware(request: NextRequest) { +export function proxy(request: NextRequest) { const response = NextResponse.next(); // const csp = [ diff --git a/public/r/bridge-deposit.json b/public/r/bridge-deposit.json index 4c816c4..ee00a01 100644 --- a/public/r/bridge-deposit.json +++ b/public/r/bridge-deposit.json @@ -5,7 +5,7 @@ "title": "Bridge Deposit", "description": "A component built with Nexus for bridge deposit functionality", "dependencies": [ - "@avail-project/nexus-core@1.2.0", + "@avail-project/nexus-sdk-v2", "lucide-react", "viem" ], @@ -28,19 +28,19 @@ "files": [ { "path": "registry/nexus-elements/bridge-deposit/components/allowance-modal.tsx", - "content": "\"use client\";\nimport React, {\n type FC,\n memo,\n type RefObject,\n useEffect,\n useMemo,\n useState,\n} from \"react\";\nimport { Button } from \"../../ui/button\";\nimport { Input } from \"../../ui/input\";\nimport { Label } from \"../../ui/label\";\nimport {\n type AllowanceHookSource,\n CHAIN_METADATA,\n formatTokenBalance,\n type OnAllowanceHookData,\n parseUnits,\n} from \"@avail-project/nexus-core\";\nimport { useNexusError } from \"../../common\";\nimport { Loader2 } from \"lucide-react\";\n\ninterface AllowanceModalProps {\n allowance: RefObject;\n callback?: () => void;\n onCloseCallback?: () => void;\n onError?: (message: string) => void;\n}\n\ntype AllowanceChoice = \"min\" | \"max\" | \"custom\";\n\ninterface AllowanceOptionProps {\n index: number;\n name: string;\n choice: AllowanceChoice;\n selectedChoice?: AllowanceChoice;\n onSelect: (index: number, choice: AllowanceChoice) => void;\n title: string;\n description?: string;\n children?: React.ReactNode;\n allowanceValue?: string;\n}\n\nconst ALLOWANCE_CHOICES: Array<{\n choice: AllowanceChoice;\n title: string;\n description: string;\n}> = [\n {\n choice: \"min\",\n title: \"Minimum\",\n description: \"Grant the lowest allowance required for this action.\",\n },\n {\n choice: \"max\",\n title: \"Maximum\",\n description: \"Approve once and skip future approvals for this token.\",\n },\n {\n choice: \"custom\",\n title: \"Custom amount\",\n description: \"Specify an allowance that fits your threshold.\",\n },\n];\n\nconst AllowanceOption: FC = ({\n index,\n name,\n choice,\n selectedChoice,\n onSelect,\n title,\n description,\n children,\n allowanceValue,\n}) => {\n const isActive = selectedChoice === choice;\n\n return (\n \n );\n};\n\nconst AllowanceModal: FC = ({\n allowance,\n callback,\n onCloseCallback,\n onError,\n}) => {\n const handleNexusError = useNexusError();\n const [selectedOption, setSelectedOption] = useState([]);\n const [customValues, setCustomValues] = useState([]);\n const [loading, setLoading] = useState(false);\n\n const { sources, allow, deny } = allowance.current ?? {\n sources: [],\n allow: () => {},\n deny: () => {},\n };\n const defaultChoices = useMemo(\n () => Array.from({ length: sources.length }, () => \"min\"),\n [sources.length],\n );\n\n const isCustomValueValid = (\n value: string,\n minimumRaw: bigint,\n decimals: number,\n ): boolean => {\n if (!value || value.trim() === \"\") return false;\n try {\n const parsedValue = parseUnits(value, decimals);\n if (parsedValue === undefined) return false;\n return parsedValue >= minimumRaw;\n } catch {\n return false;\n }\n };\n\n const hasValidationErrors = useMemo(() => {\n return sources.some((source, index) => {\n if (selectedOption[index] !== \"custom\") return false;\n const value = customValues[index];\n if (!value || value.trim() === \"\") return false;\n return !isCustomValueValid(\n value,\n source.allowance.minimumRaw,\n source.token.decimals,\n );\n });\n }, [sources, selectedOption, customValues]);\n\n const onClose = () => {\n deny();\n allowance.current = null;\n onCloseCallback?.();\n };\n\n const onApprove = () => {\n const processed = sources.map((_, i) => {\n const opt = selectedOption[i];\n if (opt === \"min\" || opt === \"max\") return opt;\n const rawValue = customValues[i]?.trim();\n if (!rawValue) return \"min\";\n const parsed = Number(rawValue);\n if (!Number.isFinite(parsed) || parsed < 0) return \"min\";\n return rawValue;\n });\n setLoading(true);\n try {\n allow(processed);\n callback?.();\n } catch (error) {\n const { message } = handleNexusError(error);\n console.error(\"AllowanceModal onApprove error\", error);\n onError?.(message);\n onCloseCallback?.();\n } finally {\n allowance.current = null;\n setLoading(false);\n }\n };\n\n const handleChoiceChange = (index: number, value: AllowanceChoice) => {\n setSelectedOption((prev) => {\n const next = [...(prev.length ? prev : defaultChoices)];\n next[index] = value;\n return next;\n });\n };\n\n const formatAmount = (value: string | bigint, source: AllowanceHookSource) =>\n formatTokenBalance(value, {\n symbol: source.token.symbol,\n decimals: source.token.decimals,\n }) ?? \"—\";\n\n useEffect(() => {\n setSelectedOption(defaultChoices);\n }, [defaultChoices]);\n\n useEffect(() => {\n setCustomValues(Array.from({ length: sources.length }, () => \"\"));\n }, [sources.length]);\n\n return (\n <>\n
\n

\n Set Token Allowances\n

\n

\n Review every required token and choose the minimum, an unlimited max,\n or define a custom amount before approving.\n

\n
\n\n
\n {sources?.map((source: AllowanceHookSource, index: number) => (\n \n
\n
\n
\n \n
\n
\n

\n {source.token.symbol}\n

\n

\n {source.chain.name}\n

\n
\n
\n\n
\n

\n Current allowance\n

\n

\n {formatAmount(source.allowance.currentRaw, source)}\n

\n
\n
\n\n
\n {ALLOWANCE_CHOICES.map((choice) => {\n if (choice.choice === \"custom\") {\n const customValue = customValues[index] ?? \"\";\n const isCustomSelected = selectedOption[index] === \"custom\";\n const showError =\n isCustomSelected &&\n customValue.trim() !== \"\" &&\n !isCustomValueValid(\n customValue,\n source.allowance.minimumRaw,\n source.token.decimals,\n );\n return (\n \n
\n {\n const next = [...customValues];\n next[index] = e.target.value;\n setCustomValues(next);\n }}\n maxLength={source.token.decimals}\n className={`h-9 w-40 rounded-lg border bg-background/80 text-sm disabled:opacity-60 ${\n showError ? \"border-destructive\" : \"\"\n }`}\n disabled={!isCustomSelected}\n />\n {showError && (\n

\n Min: {source.allowance.minimum}\n

\n )}\n
\n \n );\n }\n return (\n \n );\n })}\n
\n
\n ))}\n \n\n
\n \n \n {loading ? (\n \n ) : (\n \"Approve Selected\"\n )}\n \n
\n \n );\n};\n\nAllowanceModal.displayName = \"AllowanceModal\";\n\nexport default memo(AllowanceModal);\n", + "content": "\"use client\";\nimport React, {\n type FC,\n memo,\n type RefObject,\n useEffect,\n useMemo,\n useState,\n} from \"react\";\nimport { Button } from \"../../ui/button\";\nimport { Input } from \"../../ui/input\";\nimport { Label } from \"../../ui/label\";\nimport {\n type AllowanceHookSource,\n type OnAllowanceHookData,\n} from \"@avail-project/nexus-sdk-v2\";\nimport { formatTokenBalance } from \"@avail-project/nexus-sdk-v2/utils\";\nimport { parseUnits } from \"viem\";\nimport { useNexusError } from \"../../common\";\nimport { Loader2 } from \"lucide-react\";\n\ninterface AllowanceModalProps {\n allowance: RefObject;\n callback?: () => void;\n onCloseCallback?: () => void;\n onError?: (message: string) => void;\n}\n\ntype AllowanceChoice = \"min\" | \"max\" | \"custom\";\n\ninterface AllowanceOptionProps {\n index: number;\n name: string;\n choice: AllowanceChoice;\n selectedChoice?: AllowanceChoice;\n onSelect: (index: number, choice: AllowanceChoice) => void;\n title: string;\n description?: string;\n children?: React.ReactNode;\n allowanceValue?: string;\n}\n\nconst ALLOWANCE_CHOICES: Array<{\n choice: AllowanceChoice;\n title: string;\n description: string;\n}> = [\n {\n choice: \"min\",\n title: \"Minimum\",\n description: \"Grant the lowest allowance required for this action.\",\n },\n {\n choice: \"max\",\n title: \"Maximum\",\n description: \"Approve once and skip future approvals for this token.\",\n },\n {\n choice: \"custom\",\n title: \"Custom amount\",\n description: \"Specify an allowance that fits your threshold.\",\n },\n];\n\nconst AllowanceOption: FC = ({\n index,\n name,\n choice,\n selectedChoice,\n onSelect,\n title,\n description,\n children,\n allowanceValue,\n}) => {\n const isActive = selectedChoice === choice;\n\n return (\n \n );\n};\n\nconst AllowanceModal: FC = ({\n allowance,\n callback,\n onCloseCallback,\n onError,\n}) => {\n const handleNexusError = useNexusError();\n const [selectedOption, setSelectedOption] = useState([]);\n const [customValues, setCustomValues] = useState([]);\n const [loading, setLoading] = useState(false);\n\n const { sources, allow, deny } = allowance.current ?? {\n sources: [],\n allow: () => {},\n deny: () => {},\n };\n const defaultChoices = useMemo(\n () => Array.from({ length: sources.length }, () => \"min\"),\n [sources.length],\n );\n\n const isCustomValueValid = (\n value: string,\n minimumRaw: bigint,\n decimals: number,\n ): boolean => {\n if (!value || value.trim() === \"\") return false;\n try {\n const parsedValue = parseUnits(value, decimals);\n if (parsedValue === undefined) return false;\n return parsedValue >= minimumRaw;\n } catch {\n return false;\n }\n };\n\n const hasValidationErrors = useMemo(() => {\n return sources.some((source, index) => {\n if (selectedOption[index] !== \"custom\") return false;\n const value = customValues[index];\n if (!value || value.trim() === \"\") return false;\n return !isCustomValueValid(\n value,\n source.allowance.minimumRaw,\n source.token.decimals,\n );\n });\n }, [sources, selectedOption, customValues]);\n\n const onClose = () => {\n deny();\n allowance.current = null;\n onCloseCallback?.();\n };\n\n const onApprove = () => {\n const processed = sources.map((_, i) => {\n const opt = selectedOption[i];\n if (opt === \"min\" || opt === \"max\") return opt;\n const rawValue = customValues[i]?.trim();\n if (!rawValue) return \"min\";\n const parsed = Number(rawValue);\n if (!Number.isFinite(parsed) || parsed < 0) return \"min\";\n return rawValue;\n });\n setLoading(true);\n try {\n allow(processed);\n callback?.();\n } catch (error) {\n const { message } = handleNexusError(error);\n console.error(\"AllowanceModal onApprove error\", error);\n onError?.(message);\n onCloseCallback?.();\n } finally {\n allowance.current = null;\n setLoading(false);\n }\n };\n\n const handleChoiceChange = (index: number, value: AllowanceChoice) => {\n setSelectedOption((prev) => {\n const next = [...(prev.length ? prev : defaultChoices)];\n next[index] = value;\n return next;\n });\n };\n\n const formatAmount = (value: string | bigint, source: AllowanceHookSource) =>\n formatTokenBalance(value, {\n symbol: source.token.symbol,\n decimals: source.token.decimals,\n }) ?? \"—\";\n\n useEffect(() => {\n setSelectedOption(defaultChoices);\n }, [defaultChoices]);\n\n useEffect(() => {\n setCustomValues(Array.from({ length: sources.length }, () => \"\"));\n }, [sources.length]);\n\n return (\n <>\n
\n

\n Set Token Allowances\n

\n

\n Review every required token and choose the minimum, an unlimited max,\n or define a custom amount before approving.\n

\n
\n\n
\n {sources?.map((source: AllowanceHookSource, index: number) => (\n \n
\n
\n
\n \n
\n
\n

\n {source.token.symbol}\n

\n

\n {source.chain.name}\n

\n
\n
\n\n
\n

\n Current allowance\n

\n

\n {formatAmount(source.allowance.currentRaw, source)}\n

\n
\n
\n\n
\n {ALLOWANCE_CHOICES.map((choice) => {\n if (choice.choice === \"custom\") {\n const customValue = customValues[index] ?? \"\";\n const isCustomSelected = selectedOption[index] === \"custom\";\n const showError =\n isCustomSelected &&\n customValue.trim() !== \"\" &&\n !isCustomValueValid(\n customValue,\n source.allowance.minimumRaw,\n source.token.decimals,\n );\n return (\n \n
\n {\n const next = [...customValues];\n next[index] = e.target.value;\n setCustomValues(next);\n }}\n maxLength={source.token.decimals}\n className={`h-9 w-40 rounded-lg border bg-background/80 text-sm disabled:opacity-60 ${\n showError ? \"border-destructive\" : \"\"\n }`}\n disabled={!isCustomSelected}\n />\n {showError && (\n

\n Min: {source.allowance.minimum}\n

\n )}\n
\n \n );\n }\n return (\n \n );\n })}\n
\n
\n ))}\n \n\n
\n \n \n {loading ? (\n \n ) : (\n \"Approve Selected\"\n )}\n \n
\n \n );\n};\n\nAllowanceModal.displayName = \"AllowanceModal\";\n\nexport default memo(AllowanceModal);\n", "type": "registry:component", "target": "components/bridge-deposit/components/allowance-modal.tsx" }, { "path": "registry/nexus-elements/bridge-deposit/components/amount-input.tsx", - "content": "\"use client\";\n\nimport {\n formatTokenBalance,\n type SUPPORTED_CHAINS_IDS,\n type UserAsset,\n} from \"@avail-project/nexus-core\";\nimport { Button } from \"../../ui/button\";\nimport { Input } from \"../../ui/input\";\nimport { Fragment } from \"react\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"../../ui/accordion\";\nimport { computeAmountFromFraction, SHORT_CHAIN_NAME } from \"../../common\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { LoaderCircle } from \"lucide-react\";\n\nconst RANGE_OPTIONS = [\n {\n label: \"25%\",\n value: 0.25,\n },\n {\n label: \"50%\",\n value: 0.5,\n },\n {\n label: \"75%\",\n value: 0.75,\n },\n {\n label: \"MAX\",\n value: 1,\n },\n];\n\nconst SAFETY_MARGIN = 0.05;\n\ninterface AmountInputProps\n extends Omit<\n React.InputHTMLAttributes,\n \"onChange\" | \"value\"\n > {\n value?: string;\n onChange?: (value: string) => void;\n bridgableBalance?: UserAsset;\n destinationChain: SUPPORTED_CHAINS_IDS;\n}\n\nconst AmountInput = ({\n value,\n onChange,\n bridgableBalance,\n disabled,\n destinationChain,\n ...props\n}: AmountInputProps) => {\n const { nexusSDK, loading } = useNexus();\n\n const hasSelectedSources =\n bridgableBalance && bridgableBalance.breakdown.length > 0;\n const hasBalance =\n hasSelectedSources && Number.parseFloat(bridgableBalance.balance) > 0;\n\n return (\n
\n \n \n
\n onChange?.(e.target.value)}\n className=\"p-0 text-2xl! placeholder:text-2xl w-full border-none focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none bg-transparent!\"\n disabled={disabled || loading || !hasSelectedSources}\n />\n {bridgableBalance && hasSelectedSources && (\n

\n {formatTokenBalance(bridgableBalance?.balance, {\n symbol: bridgableBalance?.symbol,\n decimals: bridgableBalance?.decimals,\n })}\n

\n )}\n {bridgableBalance && !hasSelectedSources && (\n

\n No sources selected\n

\n )}\n {loading && !bridgableBalance && (\n \n )}\n
\n
\n
\n {RANGE_OPTIONS.map((option) => (\n {\n if (!bridgableBalance?.balance) return;\n\n const amount = computeAmountFromFraction(\n bridgableBalance.balance,\n option.value,\n bridgableBalance?.breakdown.find(\n (chain) => chain?.chain?.id === destinationChain,\n )?.decimals ?? bridgableBalance?.decimals,\n SAFETY_MARGIN,\n );\n onChange?.(amount);\n }}\n >\n {option.label}\n \n ))}\n
\n {hasSelectedSources && (\n \n

View Assets

\n \n )}\n
\n\n \n
\n {bridgableBalance?.breakdown.map((chain) => {\n if (Number.parseFloat(chain.balance) === 0) return null;\n return (\n \n
\n
\n
\n \n
\n \n {SHORT_CHAIN_NAME[chain.chain.id]}\n \n
\n
\n

\n {formatTokenBalance(chain.balance, {\n symbol: chain.symbol,\n decimals: chain.decimals,\n })}\n

\n

\n ${chain.balanceInFiat.toFixed(2)}\n

\n
\n
\n
\n );\n })}\n
\n
\n
\n
\n
\n );\n};\n\nexport default AmountInput;\n", + "content": "\"use client\";\n\nimport { type TokenBalance } from \"@avail-project/nexus-sdk-v2\";\nimport { formatTokenBalance } from \"@avail-project/nexus-sdk-v2/utils\";\nimport { Button } from \"../../ui/button\";\nimport { Input } from \"../../ui/input\";\nimport { Fragment } from \"react\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"../../ui/accordion\";\nimport { computeAmountFromFraction, SHORT_CHAIN_NAME } from \"../../common\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { LoaderCircle } from \"lucide-react\";\n\nconst RANGE_OPTIONS = [\n {\n label: \"25%\",\n value: 0.25,\n },\n {\n label: \"50%\",\n value: 0.5,\n },\n {\n label: \"75%\",\n value: 0.75,\n },\n {\n label: \"MAX\",\n value: 1,\n },\n];\n\nconst SAFETY_MARGIN = 0.05;\n\ninterface AmountInputProps extends Omit<\n React.InputHTMLAttributes,\n \"onChange\" | \"value\"\n> {\n value?: string;\n onChange?: (value: string) => void;\n bridgableBalance?: TokenBalance;\n // v2: chainBalances are chain-level entries; destinationChain used for finding chain decimals\n destinationChain: number;\n}\n\nconst AmountInput = ({\n value,\n onChange,\n bridgableBalance,\n disabled,\n destinationChain,\n ...props\n}: AmountInputProps) => {\n const { nexusSDK, loading } = useNexus();\n\n const hasSelectedSources =\n // v2: chainBalances replaces breakdown\n bridgableBalance && (bridgableBalance.chainBalances?.length ?? 0) > 0;\n const hasBalance =\n hasSelectedSources && Number.parseFloat(bridgableBalance!.balance) > 0;\n\n return (\n
\n \n \n
\n onChange?.(e.target.value)}\n className=\"p-0 text-2xl! placeholder:text-2xl w-full border-none focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none bg-transparent!\"\n disabled={disabled || loading || !hasSelectedSources}\n />\n {bridgableBalance && hasSelectedSources && (\n

\n {formatTokenBalance(bridgableBalance?.balance, {\n symbol: bridgableBalance?.symbol,\n decimals: bridgableBalance?.decimals,\n })}\n

\n )}\n {bridgableBalance && !hasSelectedSources && (\n

\n No sources selected\n

\n )}\n {loading && !bridgableBalance && (\n \n )}\n
\n
\n
\n {RANGE_OPTIONS.map((option) => (\n {\n if (!bridgableBalance?.balance) return;\n\n const amount = computeAmountFromFraction(\n bridgableBalance.balance,\n option.value,\n // v2: find chain decimals from chainBalances\n bridgableBalance?.chainBalances?.find(\n (chain) => chain?.chain?.id === destinationChain,\n )?.decimals ?? bridgableBalance?.decimals,\n SAFETY_MARGIN,\n );\n onChange?.(amount);\n }}\n >\n {option.label}\n \n ))}\n
\n {hasSelectedSources && (\n \n

View Assets

\n \n )}\n
\n\n \n
\n {bridgableBalance?.chainBalances?.map((chain: any) => {\n if (Number.parseFloat(chain.balance) === 0) return null;\n return (\n \n
\n
\n
\n \n
\n \n {SHORT_CHAIN_NAME[chain.chain.id]}\n \n
\n
\n

\n {formatTokenBalance(chain.balance, {\n symbol: chain.symbol,\n decimals: chain.decimals,\n })}\n

\n

\n {/* v2: value is a string USD amount */}\n ${Number.parseFloat(chain.value ?? \"0\").toFixed(2)}\n

\n
\n
\n
\n );\n })}\n
\n
\n
\n
\n
\n );\n};\n\nexport default AmountInput;\n", "type": "registry:component", "target": "components/bridge-deposit/components/amount-input.tsx" }, { "path": "registry/nexus-elements/bridge-deposit/components/container.tsx", - "content": "\"use client\";\n\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"../../ui/tabs\";\nimport SimpleDeposit from \"./simple-deposit\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { type BaseDepositProps } from \"../deposit\";\nimport { truncateAddress } from \"@avail-project/nexus-core\";\n\ninterface ContainerProps extends BaseDepositProps {\n fiatSubheading?: string;\n destinationLabel?: string;\n}\n\nconst Container = ({\n address,\n fiatSubheading = \"Cards, Apple Pay\",\n token,\n chain,\n chainOptions,\n destinationLabel,\n depositExecute,\n}: ContainerProps) => {\n const { nexusSDK } = useNexus();\n return (\n \n \n \n
\n

Wallet

\n

{truncateAddress(address, 4, 4)}

\n
\n
\n \n
\n

Transfer QR

\n

{chainOptions?.length ?? \"0\"} chains

\n
\n
\n \n
\n

Fiat

\n

{fiatSubheading}

\n
\n
\n
\n \n \n \n \n

Coming soon

\n
\n \n

Coming soon

\n
\n
\n );\n};\n\nexport default Container;\n", + "content": "\"use client\";\n\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"../../ui/tabs\";\nimport SimpleDeposit from \"./simple-deposit\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { type BaseDepositProps } from \"../deposit\";\nimport { truncateAddress } from \"@avail-project/nexus-sdk-v2/utils\";\n\ninterface ContainerProps extends BaseDepositProps {\n fiatSubheading?: string;\n destinationLabel?: string;\n}\n\nconst Container = ({\n address,\n fiatSubheading = \"Cards, Apple Pay\",\n token,\n chain,\n chainOptions,\n destinationLabel,\n depositExecute,\n}: ContainerProps) => {\n const { nexusSDK } = useNexus();\n return (\n \n \n \n
\n

Wallet

\n

{truncateAddress(address, 4, 4)}

\n
\n
\n \n
\n

Transfer QR

\n

{chainOptions?.length ?? \"0\"} chains

\n
\n
\n \n
\n

Fiat

\n

{fiatSubheading}

\n
\n
\n
\n \n \n \n \n

Coming soon

\n
\n \n

Coming soon

\n
\n
\n );\n};\n\nexport default Container;\n", "type": "registry:component", "target": "components/bridge-deposit/components/container.tsx" }, @@ -64,19 +64,19 @@ }, { "path": "registry/nexus-elements/bridge-deposit/components/simple-deposit.tsx", - "content": "\"use client\";\n\nimport { useCallback } from \"react\";\nimport { type BaseDepositProps } from \"../deposit\";\nimport AmountInput from \"./amount-input\";\nimport SourceSelect from \"./source-select\";\nimport { Button } from \"../../ui/button\";\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n} from \"../../ui/dialog\";\nimport AllowanceModal from \"./allowance-modal\";\nimport DepositTransactionStatusStep from \"./transaction-status-step\";\nimport DepositFeeBreakdown from \"./fee-breakdown\";\nimport SourceBreakdown from \"./source-breakdown\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport useDeposit from \"../hooks/useDeposit\";\nimport { LoaderPinwheel, X } from \"lucide-react\";\nimport { Skeleton } from \"../../ui/skeleton\";\nimport { type SUPPORTED_TOKENS } from \"@avail-project/nexus-core\";\n\ninterface SimpleDepositProps extends BaseDepositProps {\n destinationLabel?: string;\n}\n\nconst SimpleDeposit = ({\n address,\n token,\n chain,\n chainOptions,\n destinationLabel = \"on Hyperliquid Perps\",\n depositExecute,\n}: SimpleDepositProps) => {\n const {\n nexusSDK,\n intent,\n bridgableBalance,\n fetchBridgableBalance,\n allowance,\n } = useNexus();\n\n const {\n inputs,\n setInputs,\n status,\n explorerUrls,\n loading,\n isProcessing,\n isSuccess,\n isError,\n simulating,\n refreshing,\n txError,\n setTxError,\n timer,\n filteredBridgableBalance,\n unfilteredBridgableBalance,\n simulation,\n startTransaction,\n cancelSimulation,\n steps,\n feeBreakdown,\n reset,\n } = useDeposit({\n token: token ?? \"USDC\",\n chain,\n nexusSDK,\n intent,\n bridgableBalance,\n allowance,\n chainOptions,\n address,\n executeBuilder: depositExecute,\n fetchBridgableBalance,\n });\n\n const renderDepositButtonContent = useCallback(() => {\n if (refreshing)\n return (\n
\n \n

Refreshing Quote

\n
\n );\n if (simulating)\n return (\n
\n \n

Preparing Quote

\n
\n );\n return \"Deposit\";\n }, [refreshing, simulating]);\n\n if (isProcessing || isSuccess || isError) {\n return (\n <>\n {allowance.current && (\n {}}>\n \n \n Set Token Allowances\n \n \n \n \n )}\n \n \n );\n }\n\n // Default: show form view (idle/previewing states)\n return (\n
\n {/* Sources */}\n \n setInputs({ ...inputs, selectedSources: selected })\n }\n disabled={Boolean(simulation && !simulation?.bridgeSimulation)}\n />\n {\n setInputs({ ...inputs, amount: value });\n if (!value) {\n cancelSimulation();\n setTxError(null);\n }\n }}\n destinationChain={inputs?.chain}\n bridgableBalance={filteredBridgableBalance}\n disabled={loading || simulating}\n maxLength={filteredBridgableBalance?.decimals}\n />\n {/* Shimmer while simulating */}\n {simulating && !simulation && (\n <>\n \n
\n

You Receive

\n
\n \n \n
\n
\n \n \n )}\n {simulation && inputs?.amount && (\n <>\n \n\n
\n

You Receive

\n
\n {refreshing ? (\n \n ) : (\n

\n {inputs?.amount} {filteredBridgableBalance?.symbol}\n

\n )}\n

\n {destinationLabel}\n

\n
\n
\n\n \n {!simulation.bridgeSimulation && (\n
\n Bridge skipped, executing directly on destination chain\n
\n )}\n \n )}\n {simulation ? (\n
\n {\n reset();\n setTxError(null);\n }}\n className=\"w-1/2\"\n >\n Cancel\n \n \n {renderDepositButtonContent()}\n \n
\n ) : (\n \n )}\n\n {txError && (\n
\n {txError}\n {\n reset();\n setTxError(null);\n }}\n className=\"text-destructive-foreground/80 hover:text-destructive-foreground focus:outline-none\"\n aria-label=\"Dismiss error\"\n >\n \n \n
\n )}\n
\n );\n};\n\nexport default SimpleDeposit;\n", + "content": "\"use client\";\n\nimport { useCallback } from \"react\";\nimport { type BaseDepositProps } from \"../deposit\";\nimport AmountInput from \"./amount-input\";\nimport SourceSelect from \"./source-select\";\nimport { Button } from \"../../ui/button\";\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n} from \"../../ui/dialog\";\nimport AllowanceModal from \"./allowance-modal\";\nimport DepositTransactionStatusStep from \"./transaction-status-step\";\nimport DepositFeeBreakdown from \"./fee-breakdown\";\nimport SourceBreakdown from \"./source-breakdown\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport useDeposit from \"../hooks/useDeposit\";\nimport { LoaderPinwheel, X } from \"lucide-react\";\nimport { Skeleton } from \"../../ui/skeleton\";\n// v2: SUPPORTED_TOKENS removed — token is plain string\n\ninterface SimpleDepositProps extends BaseDepositProps {\n destinationLabel?: string;\n}\n\nconst SimpleDeposit = ({\n address,\n token,\n chain,\n chainOptions,\n destinationLabel = \"on Hyperliquid Perps\",\n depositExecute,\n}: SimpleDepositProps) => {\n const {\n nexusSDK,\n intent,\n bridgableBalance,\n fetchBridgableBalance,\n allowance,\n } = useNexus();\n\n const {\n inputs,\n setInputs,\n status,\n explorerUrls,\n loading,\n isProcessing,\n isSuccess,\n isError,\n simulating,\n refreshing,\n txError,\n setTxError,\n timer,\n filteredBridgableBalance,\n unfilteredBridgableBalance,\n simulation,\n startTransaction,\n cancelSimulation,\n steps,\n feeBreakdown,\n reset,\n } = useDeposit({\n token: token ?? \"USDC\",\n chain,\n nexusSDK: nexusSDK as any, // v2: NexusClient — cast to bypass v1 NexusSDK boundary\n intent: intent as any, // v2: OnIntentHookData ref\n bridgableBalance: bridgableBalance as any, // v2: UserAsset[]\n allowance,\n chainOptions,\n address,\n executeBuilder: depositExecute,\n fetchBridgableBalance,\n });\n\n const renderDepositButtonContent = useCallback(() => {\n if (refreshing)\n return (\n
\n \n

Refreshing Quote

\n
\n );\n if (simulating)\n return (\n
\n \n

Preparing Quote

\n
\n );\n return \"Deposit\";\n }, [refreshing, simulating]);\n\n if (isProcessing || isSuccess || isError) {\n return (\n <>\n {allowance.current && (\n {}}>\n \n \n Set Token Allowances\n \n \n \n \n )}\n \n \n );\n }\n\n // Default: show form view (idle/previewing states)\n return (\n
\n {/* Sources */}\n \n setInputs({ ...inputs, selectedSources: selected })\n }\n disabled={Boolean(simulation && !simulation?.bridgeSimulation)}\n />\n {\n setInputs({ ...inputs, amount: value });\n if (!value) {\n cancelSimulation();\n setTxError(null);\n }\n }}\n destinationChain={inputs?.chain}\n bridgableBalance={filteredBridgableBalance}\n disabled={loading || simulating}\n maxLength={filteredBridgableBalance?.decimals}\n />\n {/* Shimmer while simulating */}\n {simulating && !simulation && (\n <>\n \n
\n

You Receive

\n
\n \n \n
\n
\n \n \n )}\n {simulation && inputs?.amount && (\n <>\n \n\n
\n

You Receive

\n
\n {refreshing ? (\n \n ) : (\n

\n {inputs?.amount} {filteredBridgableBalance?.symbol}\n

\n )}\n

\n {destinationLabel}\n

\n
\n
\n\n \n {!simulation.bridgeSimulation && (\n
\n Bridge skipped, executing directly on destination chain\n
\n )}\n \n )}\n {simulation ? (\n
\n {\n reset();\n setTxError(null);\n }}\n className=\"w-1/2\"\n >\n Cancel\n \n \n {renderDepositButtonContent()}\n \n
\n ) : (\n \n )}\n\n {txError && (\n
\n {txError}\n {\n reset();\n setTxError(null);\n }}\n className=\"text-destructive-foreground/80 hover:text-destructive-foreground focus:outline-none\"\n aria-label=\"Dismiss error\"\n >\n \n \n
\n )}\n
\n );\n};\n\nexport default SimpleDeposit;\n", "type": "registry:component", "target": "components/bridge-deposit/components/simple-deposit.tsx" }, { "path": "registry/nexus-elements/bridge-deposit/components/source-breakdown.tsx", - "content": "import {\n CHAIN_METADATA,\n type SUPPORTED_CHAINS_IDS,\n type UserAsset,\n type ReadableIntent,\n type SUPPORTED_TOKENS,\n formatTokenBalance,\n} from \"@avail-project/nexus-core\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"../../ui/accordion\";\nimport { Skeleton } from \"../../ui/skeleton\";\nimport { useMemo } from \"react\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\n\ninterface SourceBreakdownProps {\n intent?: ReadableIntent;\n tokenSymbol: SUPPORTED_TOKENS;\n isLoading?: boolean;\n chain: SUPPORTED_CHAINS_IDS;\n bridgableBalance?: UserAsset;\n requiredAmount?: string;\n}\n\ntype ReadableIntentSource = {\n amount: string;\n chainID: number;\n chainLogo: string | undefined;\n chainName: string;\n contractAddress: `0x${string}`;\n};\n\nconst SourceBreakdown = ({\n intent,\n tokenSymbol,\n isLoading = false,\n chain,\n bridgableBalance,\n requiredAmount,\n}: SourceBreakdownProps) => {\n const { nexusSDK } = useNexus();\n const fundsOnDestination = useMemo(() => {\n return Number.parseFloat(\n bridgableBalance?.breakdown?.find((b) => b.chain?.id === chain)\n ?.balance ?? \"0\",\n );\n }, [bridgableBalance, chain]);\n\n const amountSpend = useMemo(() => {\n const amountToFormat = intent\n ? Number.parseFloat(requiredAmount ?? \"0\") +\n Number.parseFloat(intent?.fees?.total ?? \"0\")\n : (requiredAmount ?? \"0\");\n return formatTokenBalance(amountToFormat, {\n symbol: tokenSymbol,\n decimals: intent?.token?.decimals,\n });\n }, [requiredAmount, intent, tokenSymbol]);\n\n const displaySources = useMemo(() => {\n if (!intent)\n return [\n {\n chainID: chain,\n chainLogo: CHAIN_METADATA[chain]?.logo,\n chainName: CHAIN_METADATA[chain]?.name ?? \"Destination\",\n amount: requiredAmount ?? \"0\",\n contractAddress: \"\",\n },\n ];\n const baseSources: ReadableIntentSource[] = intent?.sources ?? [];\n const requiredAmountNumber = Number(requiredAmount ?? \"0\");\n const destUsed = Math.max(\n Math.min(requiredAmountNumber, fundsOnDestination),\n 0,\n );\n if (destUsed <= 0) {\n return baseSources;\n }\n const allSources = intent?.allSources ?? [];\n const destDetails = allSources?.find?.(\n (s: ReadableIntentSource) => s?.chainID === chain,\n );\n const hasDest = baseSources?.some?.(\n (s: ReadableIntentSource) => s?.chainID === chain,\n );\n const destSource = {\n chainID: chain,\n chainLogo: destDetails?.chainLogo,\n chainName: destDetails?.chainName ?? \"Destination\",\n amount: destUsed.toString(),\n contractAddress: destDetails?.contractAddress ?? \"\",\n };\n if (hasDest) {\n return baseSources.map((s: ReadableIntentSource) =>\n s?.chainID === chain ? { ...s, amount: destSource.amount } : s,\n );\n }\n return [...baseSources, destSource];\n }, [intent, requiredAmount, fundsOnDestination, chain]);\n\n return (\n \n \n
\n {isLoading ? (\n <>\n
\n

You Spend

\n \n
\n
\n \n
\n \n
\n
\n \n ) : (\n <>\n
\n

You Spend

\n

\n {displaySources?.length < 2\n ? \"1 Asset on 1 Chain\"\n : `1 Asset on ${displaySources?.length} Chains`}\n

\n
\n\n
\n

{amountSpend}

\n \n

View Sources

\n \n
\n \n )}\n
\n {!isLoading && displaySources?.length > 0 && (\n \n
\n {displaySources?.map((source) => (\n \n
\n \n

\n {source.chainName}\n

\n
\n\n

\n {source.amount} {tokenSymbol}\n

\n
\n ))}\n \n
\n )}\n
\n
\n );\n};\n\nexport default SourceBreakdown;\n", + "content": "import {\n type BridgeIntent,\n type TokenBalance,\n} from \"@avail-project/nexus-sdk-v2\";\nimport { formatTokenBalance } from \"@avail-project/nexus-sdk-v2/utils\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"../../ui/accordion\";\nimport { Skeleton } from \"../../ui/skeleton\";\nimport { useMemo } from \"react\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\n\ninterface SourceBreakdownProps {\n intent?: BridgeIntent;\n tokenSymbol?: string;\n isLoading?: boolean;\n chain: number;\n bridgableBalance?: TokenBalance;\n requiredAmount?: string;\n}\n\ntype ReadableIntentSource = {\n amount: string;\n chainID: number;\n chainLogo: string | undefined;\n chainName: string;\n contractAddress: `0x${string}`;\n};\n\nconst SourceBreakdown = ({\n intent,\n tokenSymbol,\n isLoading = false,\n chain,\n bridgableBalance,\n requiredAmount,\n}: SourceBreakdownProps) => {\n const { nexusSDK } = useNexus();\n const fundsOnDestination = useMemo(() => {\n return Number.parseFloat(\n // v2: chainBalances replaces breakdown\n bridgableBalance?.chainBalances?.find((b: any) => b.chain?.id === chain)\n ?.balance ?? \"0\",\n );\n }, [bridgableBalance, chain]);\n\n const amountSpend = useMemo(() => {\n // v2: BridgeIntent.selectedSources[0].token.decimals for decimals\n const decimals = intent?.selectedSources?.[0]?.token?.decimals;\n const amountToFormat = intent\n ? Number.parseFloat(requiredAmount ?? \"0\") +\n Number.parseFloat(intent?.fees?.total ?? \"0\")\n : (requiredAmount ?? \"0\");\n return formatTokenBalance(amountToFormat, {\n symbol: tokenSymbol,\n decimals,\n });\n }, [requiredAmount, intent, tokenSymbol]);\n\n const displaySources = useMemo(() => {\n if (!intent)\n return [\n {\n chainID: chain,\n chainLogo: undefined,\n chainName: \"Destination\",\n amount: requiredAmount ?? \"0\",\n contractAddress: \"\",\n },\n ];\n // v2: BridgeIntent.selectedSources has {chain:{id,name,logo}, token:{contractAddress,...}} shape\n const rawSources = intent?.selectedSources ?? [];\n const baseSources: ReadableIntentSource[] = rawSources.map((s: BridgeIntent[\"selectedSources\"][0]) => ({\n chainID: s.chain.id,\n chainLogo: s.chain.logo,\n chainName: s.chain.name,\n amount: s.amount,\n contractAddress: (s.token?.contractAddress ?? \"\") as `0x${string}`,\n }));\n const requiredAmountNumber = Number(requiredAmount ?? \"0\");\n const destUsed = Math.max(\n Math.min(requiredAmountNumber, fundsOnDestination),\n 0,\n );\n if (destUsed <= 0) {\n return baseSources;\n }\n // v2: BridgeIntent.availableSources replaces allSources\n const allSources = intent?.availableSources ?? [];\n const destDetails = allSources?.find?.((s) => s?.chain?.id === chain);\n const hasDest = baseSources?.some?.(\n (s: ReadableIntentSource) => s?.chainID === chain,\n );\n const destSource = {\n chainID: chain,\n chainLogo: destDetails?.chain?.logo,\n chainName: destDetails?.chain?.name ?? \"Destination\",\n amount: destUsed.toString(),\n contractAddress: destDetails?.token?.contractAddress ?? \"\",\n };\n if (hasDest) {\n return baseSources.map((s: ReadableIntentSource) =>\n s?.chainID === chain ? { ...s, amount: destSource.amount } : s,\n );\n }\n return [...baseSources, destSource];\n }, [intent, requiredAmount, fundsOnDestination, chain]);\n\n return (\n \n \n
\n {isLoading ? (\n <>\n
\n

You Spend

\n \n
\n
\n \n
\n \n
\n
\n \n ) : (\n <>\n
\n

You Spend

\n

\n {displaySources?.length < 2\n ? \"1 Asset on 1 Chain\"\n : `1 Asset on ${displaySources?.length} Chains`}\n

\n
\n\n
\n

{amountSpend}

\n \n

View Sources

\n \n
\n \n )}\n
\n {!isLoading && displaySources?.length > 0 && (\n \n
\n {displaySources?.map((source) => (\n \n
\n \n

\n {source.chainName}\n

\n
\n\n

\n {source.amount} {tokenSymbol}\n

\n
\n ))}\n \n
\n )}\n
\n
\n );\n};\n\nexport default SourceBreakdown;\n", "type": "registry:component", "target": "components/bridge-deposit/components/source-breakdown.tsx" }, { "path": "registry/nexus-elements/bridge-deposit/components/source-select.tsx", - "content": "import { ChevronDown } from \"lucide-react\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../../ui/popover\";\nimport { Label } from \"../../ui/label\";\nimport { Checkbox } from \"../../ui/checkbox\";\nimport {\n type SUPPORTED_TOKENS,\n type UserAsset,\n} from \"@avail-project/nexus-core\";\n\ninterface SourceSelectProps {\n token?: SUPPORTED_TOKENS;\n balanceBreakdown?: UserAsset;\n selected?: number[];\n onChange?: (selected: number[]) => void;\n disabled?: boolean;\n}\n\nconst SourceSelect = ({\n token,\n balanceBreakdown,\n selected = [],\n onChange,\n disabled = false,\n}: SourceSelectProps) => {\n const isSelected = (id: number) => selected?.includes(id);\n const toggle = (id: number) => {\n if (!onChange) return;\n if (disabled) return;\n if (isSelected(id)) onChange(selected.filter((s) => s !== id));\n else onChange([...selected, id]);\n };\n\n const allSelected =\n Boolean(balanceBreakdown?.breakdown.length) &&\n balanceBreakdown?.breakdown.every((chain) =>\n selected.includes(chain.chain.id)\n );\n\n const toggleAll = () => {\n if (!onChange || disabled || !balanceBreakdown?.breakdown.length) return;\n if (allSelected) {\n onChange([]);\n } else {\n onChange(balanceBreakdown.breakdown.map((chain) => chain.chain.id));\n }\n };\n\n return (\n \n \n Customise source chains\n \n \n \n {balanceBreakdown && balanceBreakdown?.breakdown.length > 0 ? (\n <>\n
\n \n \n Select All\n \n
\n
\n {balanceBreakdown?.breakdown.map((chain) => (\n
\n toggle(chain.chain.id)}\n value={chain.chain.id}\n disabled={disabled}\n className={`${\n disabled ? \"cursor-not-allowed\" : \"cursor-pointer\"\n }`}\n />\n
\n
\n \n \n {chain.chain.name}\n \n
\n

\n {chain.balance} {chain.symbol}\n

\n
\n
\n ))}\n
\n \n ) : (\n

\n No chains with balance\n

\n )}\n
\n
\n );\n};\nexport default SourceSelect;\n", + "content": "import { ChevronDown } from \"lucide-react\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../../ui/popover\";\nimport { Label } from \"../../ui/label\";\nimport { Checkbox } from \"../../ui/checkbox\";\nimport { type TokenBalance } from \"@avail-project/nexus-sdk-v2\";\n\ninterface SourceSelectProps {\n token?: string;\n balanceBreakdown?: TokenBalance;\n selected?: number[];\n onChange?: (selected: number[]) => void;\n disabled?: boolean;\n}\n\nconst SourceSelect = ({\n token,\n balanceBreakdown,\n selected = [],\n onChange,\n disabled = false,\n}: SourceSelectProps) => {\n const isSelected = (id: number) => selected?.includes(id);\n const toggle = (id: number) => {\n if (!onChange) return;\n if (disabled) return;\n if (isSelected(id)) onChange(selected.filter((s) => s !== id));\n else onChange([...selected, id]);\n };\n\n const allSelected =\n // v2: chainBalances replaces breakdown\n Boolean(balanceBreakdown?.chainBalances?.length) &&\n balanceBreakdown?.chainBalances?.every((chain) =>\n selected.includes(chain.chain.id)\n );\n\n const toggleAll = () => {\n if (!onChange || disabled || !balanceBreakdown?.chainBalances?.length) return;\n if (allSelected) {\n onChange([]);\n } else {\n onChange(balanceBreakdown.chainBalances.map((chain) => chain.chain.id));\n }\n };\n\n return (\n \n \n Customise source chains\n \n \n \n {balanceBreakdown && (balanceBreakdown?.chainBalances?.length ?? 0) > 0 ? (\n <>\n
\n \n \n Select All\n \n
\n
\n {balanceBreakdown?.chainBalances?.map((chain) => (\n
\n toggle(chain.chain.id)}\n value={chain.chain.id}\n disabled={disabled}\n className={`${\n disabled ? \"cursor-not-allowed\" : \"cursor-pointer\"\n }`}\n />\n
\n
\n \n \n {chain.chain.name}\n \n
\n

\n {chain.balance} {chain.symbol}\n

\n
\n
\n ))}\n
\n \n ) : (\n

\n No chains with balance\n

\n )}\n
\n
\n );\n};\nexport default SourceSelect;\n", "type": "registry:component", "target": "components/bridge-deposit/components/source-select.tsx" }, @@ -88,19 +88,19 @@ }, { "path": "registry/nexus-elements/bridge-deposit/components/transaction-status-step.tsx", - "content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport { StepIndicator } from \"./step-indicator\";\nimport { InfoRow, InfoCard } from \"./info\";\nimport { Button } from \"../../ui/button\";\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger,\n} from \"../../ui/collapsible\";\nimport { ChevronRight, ExternalLink, Loader2 } from \"lucide-react\";\nimport { type DepositStatus } from \"../hooks/useDeposit\";\nimport { type BridgeStepType } from \"@avail-project/nexus-core\";\n\ninterface DepositTransactionStatusProps {\n status: DepositStatus;\n timer: number;\n steps: Array<{ id: number; completed: boolean; step: BridgeStepType }>;\n tokenSymbol: string;\n amount: string;\n destinationLabel: string;\n explorerUrls: {\n intentUrl: string | null;\n executeUrl: string | null;\n };\n feeBreakdown?: {\n totalGasFee: string | number;\n gasFormatted?: string;\n bridgeFormatted?: string;\n };\n onClose: () => void;\n}\n\nconst DepositTransactionStatusStep = ({\n status,\n timer,\n steps,\n tokenSymbol,\n amount,\n destinationLabel,\n explorerUrls,\n feeBreakdown,\n onClose,\n}: DepositTransactionStatusProps) => {\n const [open, setOpen] = useState(false);\n\n const isSuccess = status === \"success\";\n const isError = status === \"error\";\n const isExecuting = status === \"executing\";\n const totalSteps = Array.isArray(steps) ? steps.length : 0;\n const completedSteps = Array.isArray(steps)\n ? steps.reduce((acc, s) => acc + (s?.completed ? 1 : 0), 0)\n : 0;\n const stepPercent = totalSteps > 0 ? completedSteps / totalSteps : 0;\n const progress = isSuccess ? 1 : Math.min(stepPercent, 0.75);\n\n const title = useMemo(() => {\n if (isSuccess) return \"Deposit successful\";\n if (isError) return \"Deposit failed\";\n return \"Processing your deposit\";\n }, [isSuccess, isError]);\n\n const subtitle = useMemo(() => {\n if (isSuccess) return \"Your funds were successfully deposited.\";\n if (isError) return \"Something went wrong with your deposit.\";\n return \"This may take a few seconds to complete.\";\n }, [isSuccess, isError]);\n\n const totalTime = useMemo(() => {\n if (!isSuccess) return undefined;\n return `${Math.max(1, Math.floor(timer))}s`;\n }, [isSuccess, timer]);\n\n const statusValue = useMemo(() => {\n if (isSuccess) {\n return Successful;\n }\n if (isError) {\n return Failed;\n }\n return (\n \n \n Processing\n \n );\n }, [isSuccess, isError]);\n\n // Format live timer display\n const liveTimer = useMemo(() => {\n const seconds = Math.floor(timer);\n const ms = String(Math.floor((timer % 1) * 1000)).padStart(3, \"0\");\n return `${seconds}.${ms}s`;\n }, [timer]);\n\n return (\n
\n
\n \n\n
\n

{title}

\n

{subtitle}

\n {isExecuting && (\n

\n {liveTimer}\n

\n )}\n
\n\n \n \n {isSuccess && totalTime && (\n \n )}\n \n\n \n \n \n \n\n {(explorerUrls.intentUrl || explorerUrls.executeUrl) && (\n \n {explorerUrls.intentUrl && (\n \n View\n \n \n }\n />\n )}\n {explorerUrls.executeUrl && (\n \n View\n \n \n }\n />\n )}\n \n )}\n\n {feeBreakdown && (\n \n \n Fee breakdown\n \n \n \n \n {feeBreakdown.bridgeFormatted && (\n \n )}\n {feeBreakdown.gasFormatted && (\n \n )}\n \n \n \n \n )}\n
\n\n
\n \n Close\n \n \n {isSuccess || isError ? (\n \"New Deposit\"\n ) : (\n \n )}\n \n
\n
\n );\n};\n\nexport default DepositTransactionStatusStep;\n", + "content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport { StepIndicator } from \"./step-indicator\";\nimport { InfoRow, InfoCard } from \"./info\";\nimport { Button } from \"../../ui/button\";\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger,\n} from \"../../ui/collapsible\";\nimport { ChevronRight, ExternalLink, Loader2 } from \"lucide-react\";\nimport { type DepositStatus } from \"../hooks/useDeposit\";\n// v2: BridgeStepType is locally defined\ntype BridgeStepType = { typeID?: string; type?: string; stepType?: string; state?: string; [key: string]: unknown };\n\ninterface DepositTransactionStatusProps {\n status: DepositStatus;\n timer: number;\n steps: Array<{ id: number; completed: boolean; step: BridgeStepType }>;\n tokenSymbol: string;\n amount: string;\n destinationLabel: string;\n explorerUrls: {\n intentUrl: string | null;\n executeUrl: string | null;\n };\n feeBreakdown?: {\n totalGasFee: string | number;\n gasFormatted?: string;\n bridgeFormatted?: string;\n };\n onClose: () => void;\n}\n\nconst DepositTransactionStatusStep = ({\n status,\n timer,\n steps,\n tokenSymbol,\n amount,\n destinationLabel,\n explorerUrls,\n feeBreakdown,\n onClose,\n}: DepositTransactionStatusProps) => {\n const [open, setOpen] = useState(false);\n\n const isSuccess = status === \"success\";\n const isError = status === \"error\";\n const isExecuting = status === \"executing\";\n const totalSteps = Array.isArray(steps) ? steps.length : 0;\n const completedSteps = Array.isArray(steps)\n ? steps.reduce((acc, s) => acc + (s?.completed ? 1 : 0), 0)\n : 0;\n const stepPercent = totalSteps > 0 ? completedSteps / totalSteps : 0;\n const progress = isSuccess ? 1 : Math.min(stepPercent, 0.75);\n\n const title = useMemo(() => {\n if (isSuccess) return \"Deposit successful\";\n if (isError) return \"Deposit failed\";\n return \"Processing your deposit\";\n }, [isSuccess, isError]);\n\n const subtitle = useMemo(() => {\n if (isSuccess) return \"Your funds were successfully deposited.\";\n if (isError) return \"Something went wrong with your deposit.\";\n return \"This may take a few seconds to complete.\";\n }, [isSuccess, isError]);\n\n const totalTime = useMemo(() => {\n if (!isSuccess) return undefined;\n return `${Math.max(1, Math.floor(timer))}s`;\n }, [isSuccess, timer]);\n\n const statusValue = useMemo(() => {\n if (isSuccess) {\n return Successful;\n }\n if (isError) {\n return Failed;\n }\n return (\n \n \n Processing\n \n );\n }, [isSuccess, isError]);\n\n // Format live timer display\n const liveTimer = useMemo(() => {\n const seconds = Math.floor(timer);\n const ms = String(Math.floor((timer % 1) * 1000)).padStart(3, \"0\");\n return `${seconds}.${ms}s`;\n }, [timer]);\n\n return (\n
\n
\n \n\n
\n

{title}

\n

{subtitle}

\n {isExecuting && (\n

\n {liveTimer}\n

\n )}\n
\n\n \n \n {isSuccess && totalTime && (\n \n )}\n \n\n \n \n \n \n\n {(explorerUrls.intentUrl || explorerUrls.executeUrl) && (\n \n {explorerUrls.intentUrl && (\n \n View\n \n \n }\n />\n )}\n {explorerUrls.executeUrl && (\n \n View\n \n \n }\n />\n )}\n \n )}\n\n {feeBreakdown && (\n \n \n Fee breakdown\n \n \n \n \n {feeBreakdown.bridgeFormatted && (\n \n )}\n {feeBreakdown.gasFormatted && (\n \n )}\n \n \n \n \n )}\n
\n\n
\n \n Close\n \n \n {isSuccess || isError ? (\n \"New Deposit\"\n ) : (\n \n )}\n \n
\n
\n );\n};\n\nexport default DepositTransactionStatusStep;\n", "type": "registry:component", "target": "components/bridge-deposit/components/transaction-status-step.tsx" }, { "path": "registry/nexus-elements/bridge-deposit/deposit.tsx", - "content": "import {\n type SUPPORTED_CHAINS_IDS,\n type SUPPORTED_TOKENS,\n type ExecuteParams,\n} from \"@avail-project/nexus-core\";\nimport DepositModal from \"./components/deposit-modal\";\nimport { type Address } from \"viem\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"../ui/card\";\nimport { useNexus } from \"../nexus/NexusProvider\";\nimport SimpleDeposit from \"./components/simple-deposit\";\n\nexport interface BaseDepositProps {\n address: Address;\n token?: SUPPORTED_TOKENS;\n chain: SUPPORTED_CHAINS_IDS;\n chainOptions?: {\n id: number;\n name: string;\n logo: string;\n }[];\n depositExecute: (\n token: SUPPORTED_TOKENS,\n amount: string,\n chainId: SUPPORTED_CHAINS_IDS,\n userAddress: `0x${string}`\n ) => Omit;\n}\n\ninterface NexusDepositProps extends BaseDepositProps {\n heading?: string;\n embed?: boolean;\n destinationLabel?: string;\n}\n\nconst NexusDeposit = ({\n address,\n token = \"USDC\",\n chain,\n chainOptions, // pass to customise sources displayed, if not provided, all sources will be shown\n heading = \"Deposit USDC\",\n embed = false,\n destinationLabel,\n depositExecute,\n}: NexusDepositProps) => {\n const { supportedChainsAndTokens } = useNexus();\n const formatedChainOptions =\n chainOptions ??\n supportedChainsAndTokens?.map((chain) => {\n return {\n id: chain.id,\n name: chain.name,\n logo: chain.logo,\n };\n });\n if (embed) {\n return (\n \n \n {heading}\n \n \n \n \n \n );\n }\n return (\n \n );\n};\n\nexport default NexusDeposit;\n", + "content": "import { type ExecuteParams } from \"@avail-project/nexus-sdk-v2\";\nimport DepositModal from \"./components/deposit-modal\";\nimport { type Address } from \"viem\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"../ui/card\";\nimport { useNexus } from \"../nexus/NexusProvider\";\nimport SimpleDeposit from \"./components/simple-deposit\";\n\nexport interface BaseDepositProps {\n address: Address;\n token?: string;\n chain: number;\n chainOptions?: {\n id: number;\n name: string;\n logo: string;\n }[];\n depositExecute: (\n token: string,\n amount: string,\n chainId: number,\n userAddress: `0x${string}`\n ) => Omit;\n}\n\ninterface NexusDepositProps extends BaseDepositProps {\n heading?: string;\n embed?: boolean;\n destinationLabel?: string;\n}\n\nconst NexusDeposit = ({\n address,\n token = \"USDC\",\n chain,\n chainOptions, // pass to customise sources displayed, if not provided, all sources will be shown\n heading = \"Deposit USDC\",\n embed = false,\n destinationLabel,\n depositExecute,\n}: NexusDepositProps) => {\n const { supportedChainsAndTokens } = useNexus();\n const formatedChainOptions =\n chainOptions ??\n supportedChainsAndTokens?.map((chain) => {\n return {\n id: chain.id,\n name: chain.name,\n logo: chain.logo,\n };\n });\n if (embed) {\n return (\n \n \n {heading}\n \n \n \n \n \n );\n }\n return (\n \n );\n};\n\nexport default NexusDeposit;\n", "type": "registry:component", "target": "components/bridge-deposit/deposit.tsx" }, { "path": "registry/nexus-elements/bridge-deposit/hooks/useDeposit.ts", - "content": "\"use client\";\n\nimport {\n type SUPPORTED_CHAINS_IDS,\n type SUPPORTED_TOKENS,\n type UserAsset,\n NexusSDK,\n type OnIntentHookData,\n type OnAllowanceHookData,\n type ExecuteParams,\n type BridgeAndExecuteParams,\n type BridgeAndExecuteResult,\n type BridgeAndExecuteSimulationResult,\n NEXUS_EVENTS,\n type BridgeStepType,\n CHAIN_METADATA,\n formatTokenBalance,\n formatUnits,\n} from \"@avail-project/nexus-core\";\nimport {\n useEffect,\n useMemo,\n useRef,\n useState,\n useReducer,\n useCallback,\n type RefObject,\n} from \"react\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { type Address } from \"viem\";\nimport {\n useDebouncedValue,\n useNexusError,\n usePolling,\n useStopwatch,\n useTransactionSteps,\n} from \"../../common\";\n\nexport type DepositStatus =\n | \"idle\"\n | \"previewing\"\n | \"executing\"\n | \"success\"\n | \"error\";\n\ninterface DepositInputs {\n chain: SUPPORTED_CHAINS_IDS;\n amount?: string;\n selectedSources: number[];\n}\n\ninterface UseDepositProps {\n token: SUPPORTED_TOKENS;\n chain: SUPPORTED_CHAINS_IDS;\n nexusSDK: NexusSDK | null;\n intent: RefObject;\n allowance: RefObject;\n bridgableBalance: UserAsset[] | null;\n fetchBridgableBalance: () => Promise;\n chainOptions?: { id: number; name: string; logo: string }[];\n address: Address;\n executeBuilder?: (\n token: SUPPORTED_TOKENS,\n amount: string,\n chainId: SUPPORTED_CHAINS_IDS,\n userAddress: `0x${string}`,\n ) => Omit;\n executeConfig?: Omit;\n}\n\ntype DepositState = {\n inputs: DepositInputs;\n status: DepositStatus;\n explorerUrls: {\n intentUrl: string | null;\n executeUrl: string | null;\n };\n error: string | null;\n lastResult: BridgeAndExecuteResult | null;\n};\n\ntype Action =\n | { type: \"setInputs\"; payload: Partial }\n | { type: \"resetInputs\" }\n | { type: \"setStatus\"; payload: DepositStatus }\n | { type: \"setExplorerUrls\"; payload: Partial }\n | { type: \"setError\"; payload: string | null }\n | { type: \"setLastResult\"; payload: BridgeAndExecuteResult | null }\n | { type: \"reset\" };\n\nconst useDeposit = ({\n token,\n chain,\n nexusSDK,\n intent,\n bridgableBalance,\n chainOptions,\n address,\n executeBuilder,\n executeConfig,\n allowance,\n fetchBridgableBalance,\n}: UseDepositProps) => {\n const { getFiatValue } = useNexus();\n const handleNexusError = useNexusError();\n\n const allSourceIds = useMemo(\n () => chainOptions?.map((c) => c.id) ?? [],\n [chainOptions],\n );\n\n const createInitialState = useCallback(\n (): DepositState => ({\n inputs: {\n chain,\n amount: undefined,\n selectedSources: allSourceIds,\n },\n status: \"idle\",\n explorerUrls: {\n intentUrl: null,\n executeUrl: null,\n },\n error: null,\n lastResult: null,\n }),\n [chain, allSourceIds],\n );\n\n const initialState = createInitialState();\n\n function reducer(state: DepositState, action: Action): DepositState {\n switch (action.type) {\n case \"setInputs\": {\n const newInputs = { ...state.inputs, ...action.payload };\n let newStatus = state.status;\n if (\n state.status === \"idle\" &&\n newInputs.amount &&\n Number.parseFloat(newInputs.amount) > 0\n ) {\n newStatus = \"previewing\";\n }\n if (\n state.status === \"previewing\" &&\n (!newInputs.amount || Number.parseFloat(newInputs.amount) <= 0)\n ) {\n newStatus = \"idle\";\n }\n return { ...state, inputs: newInputs, status: newStatus };\n }\n case \"resetInputs\":\n return {\n ...state,\n inputs: { chain, amount: undefined, selectedSources: allSourceIds },\n status: \"idle\",\n };\n case \"setStatus\":\n return { ...state, status: action.payload };\n case \"setExplorerUrls\":\n return {\n ...state,\n explorerUrls: { ...state.explorerUrls, ...action.payload },\n };\n case \"setError\":\n return { ...state, error: action.payload };\n case \"setLastResult\":\n return { ...state, lastResult: action.payload };\n case \"reset\":\n return createInitialState();\n default:\n return state;\n }\n }\n\n const [state, dispatch] = useReducer(reducer, initialState);\n const { inputs, status, explorerUrls, error: txError, lastResult } = state;\n\n const setInputs = (next: Partial) => {\n dispatch({ type: \"setInputs\", payload: next });\n };\n\n const setTxError = (error: string | null) => {\n dispatch({ type: \"setError\", payload: error });\n };\n\n const loading = status === \"executing\";\n const isProcessing = status === \"executing\";\n const isSuccess = status === \"success\";\n const isError = status === \"error\";\n\n // Simulation state (useState)\n const [simulation, setSimulation] =\n useState(null);\n const [simulating, setSimulating] = useState(false);\n\n // Derived: refreshing = simulating while we already have a simulation\n const refreshing = simulating && simulation !== null;\n\n // Refs for non-rendering state\n const autoAllowRef = useRef(false);\n const transactionStartedRef = useRef(false);\n const simulationRequestIdRef = useRef(0);\n const activeSimulationIdRef = useRef(null);\n\n const {\n steps,\n onStepsList,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n\n const unfilteredBridgableBalance = useMemo(() => {\n const tokenBalance = bridgableBalance?.find((bal) => bal?.symbol === token);\n if (!tokenBalance) return undefined;\n\n const nonZeroBreakdown = tokenBalance.breakdown.filter(\n (chain) => Number.parseFloat(chain.balance) > 0,\n );\n\n const totalBalance = nonZeroBreakdown.reduce(\n (sum, chain) => sum + Number.parseFloat(chain.balance),\n 0,\n );\n\n const totalBalanceInFiat = nonZeroBreakdown.reduce(\n (sum, chain) => sum + chain.balanceInFiat,\n 0,\n );\n\n return {\n ...tokenBalance,\n balance: totalBalance.toString(),\n balanceInFiat: totalBalanceInFiat,\n breakdown: nonZeroBreakdown,\n };\n }, [bridgableBalance, token]);\n\n const filteredBridgableBalance = useMemo(() => {\n const tokenBalance = bridgableBalance?.find((bal) => bal?.symbol === token);\n if (!tokenBalance) return undefined;\n\n const selectedSourcesSet = new Set(inputs.selectedSources);\n const filteredBreakdown = tokenBalance.breakdown.filter(\n (chain) =>\n selectedSourcesSet.has(chain.chain.id) &&\n Number.parseFloat(chain.balance) > 0,\n );\n\n const totalBalance = filteredBreakdown.reduce(\n (sum, chain) => sum + Number.parseFloat(chain.balance),\n 0,\n );\n\n const totalBalanceInFiat = filteredBreakdown.reduce(\n (sum, chain) => sum + chain.balanceInFiat,\n 0,\n );\n\n return {\n ...tokenBalance,\n balance: totalBalance.toString(),\n balanceInFiat: totalBalanceInFiat,\n breakdown: filteredBreakdown,\n };\n }, [bridgableBalance, token, inputs.selectedSources]);\n\n const allCompleted = useMemo(\n () => (steps?.length ?? 0) > 0 && steps.every((s) => s.completed),\n [steps],\n );\n\n const stopwatch = useStopwatch({\n running: isProcessing && !allCompleted && transactionStartedRef.current,\n intervalMs: 100,\n });\n\n const debouncedAmount = useDebouncedValue(inputs?.amount ?? \"\", 1200);\n\n const feeBreakdown = useMemo(() => {\n if (!nexusSDK || !simulation || !token)\n return {\n totalGasFee: 0,\n bridgeUsd: 0,\n bridgeFormatted: \"0\",\n gasUsd: 0,\n gasFormatted: \"0\",\n };\n const native = CHAIN_METADATA[chain]?.nativeCurrency;\n const nativeSymbol = native.symbol;\n const nativeDecimals = native.decimals;\n\n const gasFormatted =\n formatTokenBalance(simulation?.executeSimulation?.gasFee, {\n symbol: nativeSymbol,\n decimals: nativeDecimals,\n }) ?? \"0\";\n const gasUnits = Number.parseFloat(\n formatUnits(simulation?.executeSimulation?.gasFee, nativeDecimals),\n );\n\n const gasUsd = getFiatValue(gasUnits, nativeSymbol);\n if (simulation?.bridgeSimulation) {\n const tokenDecimals =\n simulation?.bridgeSimulation?.intent?.token?.decimals;\n const bridgeFormatted =\n formatTokenBalance(simulation?.bridgeSimulation?.intent?.fees?.total, {\n symbol: token,\n decimals: tokenDecimals,\n }) ?? \"0\";\n const bridgeUsd = getFiatValue(\n Number.parseFloat(simulation?.bridgeSimulation?.intent?.fees?.total),\n token,\n );\n\n const totalGasFee = bridgeUsd + gasUsd;\n\n return {\n totalGasFee: `$${totalGasFee.toFixed(4)} USD`,\n bridgeUsd,\n bridgeFormatted,\n gasUsd,\n gasFormatted,\n };\n }\n return {\n totalGasFee: gasFormatted,\n gasUsd,\n gasFormatted,\n };\n }, [nexusSDK, simulation, chain, token, getFiatValue]);\n\n const handleTransaction = async () => {\n if (!inputs?.amount || !inputs?.chain) return;\n if (!inputs.selectedSources?.length) {\n dispatch({\n type: \"setError\",\n payload: \"Select at least 1 source chain to continue.\",\n });\n return;\n }\n dispatch({ type: \"setStatus\", payload: \"executing\" });\n dispatch({ type: \"setError\", payload: null });\n try {\n if (!nexusSDK) throw new Error(\"Nexus SDK not initialized\");\n const amountBigInt = nexusSDK.convertTokenReadableAmountToBigInt(\n inputs.amount,\n token,\n inputs.chain,\n );\n const executeParams: Omit | undefined =\n executeBuilder\n ? executeBuilder(token, inputs.amount, inputs.chain, address)\n : executeConfig;\n const params: BridgeAndExecuteParams = {\n token,\n amount: amountBigInt,\n toChainId: inputs.chain,\n sourceChains: inputs.selectedSources,\n execute: executeParams as Omit,\n waitForReceipt: true,\n };\n\n const result: BridgeAndExecuteResult = await nexusSDK.bridgeAndExecute(\n params,\n {\n onEvent: (event) => {\n if (event.name === NEXUS_EVENTS.STEPS_LIST) {\n const list = Array.isArray(event.args) ? event.args : [];\n onStepsList(list);\n }\n if (event.name === NEXUS_EVENTS.STEP_COMPLETE) {\n if (\n !transactionStartedRef.current &&\n event.args.type === \"INTENT_HASH_SIGNED\"\n ) {\n transactionStartedRef.current = true;\n }\n onStepComplete(event.args);\n }\n },\n },\n );\n\n if (!result) {\n dispatch({ type: \"setError\", payload: \"Transaction rejected by user\" });\n dispatch({ type: \"setStatus\", payload: \"error\" });\n return;\n }\n dispatch({ type: \"setLastResult\", payload: result });\n dispatch({\n type: \"setExplorerUrls\",\n payload: {\n intentUrl: result.bridgeExplorerUrl ?? null,\n executeUrl: result.executeExplorerUrl ?? null,\n },\n });\n await onSuccess();\n } catch (error) {\n const { message } = handleNexusError(error);\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n dispatch({ type: \"setError\", payload: message });\n dispatch({ type: \"setStatus\", payload: \"error\" });\n }\n };\n\n const simulate = async (overrideAmount?: string) => {\n if (!nexusSDK || isProcessing || isSuccess) return;\n\n const amountToUse = overrideAmount ?? inputs?.amount;\n\n if (!amountToUse || !inputs?.chain) {\n activeSimulationIdRef.current = null;\n setSimulation(null);\n return;\n }\n if (\n Number.parseFloat(amountToUse) >\n Number.parseFloat(filteredBridgableBalance?.balance ?? \"0\")\n ) {\n activeSimulationIdRef.current = null;\n dispatch({ type: \"setError\", payload: \"Insufficient balance\" });\n setSimulation(null);\n return;\n }\n if (!inputs.selectedSources?.length) {\n activeSimulationIdRef.current = null;\n dispatch({\n type: \"setError\",\n payload: \"Select at least 1 source chain to continue.\",\n });\n setSimulation(null);\n return;\n }\n const requestId = ++simulationRequestIdRef.current;\n activeSimulationIdRef.current = requestId;\n setSimulating(true);\n try {\n const amountBigInt = nexusSDK.convertTokenReadableAmountToBigInt(\n amountToUse,\n token,\n inputs.chain,\n );\n const executeParams: Omit | undefined =\n executeBuilder\n ? executeBuilder(token, amountToUse, inputs.chain, address)\n : executeConfig;\n const params: BridgeAndExecuteParams = {\n token,\n amount: amountBigInt,\n toChainId: inputs.chain,\n sourceChains: inputs.selectedSources,\n execute: executeParams as Omit,\n waitForReceipt: false,\n };\n const sim = await nexusSDK.simulateBridgeAndExecute(params);\n if (activeSimulationIdRef.current !== requestId) {\n return;\n }\n if (sim) {\n dispatch({ type: \"setError\", payload: null });\n setSimulation(sim);\n } else {\n setSimulation(null);\n dispatch({ type: \"setError\", payload: \"Simulation failed\" });\n }\n } catch (error) {\n if (activeSimulationIdRef.current !== requestId) {\n return;\n }\n setSimulation(null);\n const { message } = handleNexusError(error);\n dispatch({ type: \"setError\", payload: message });\n } finally {\n if (activeSimulationIdRef.current === requestId) {\n setSimulating(false);\n }\n }\n };\n\n const refreshSimulation = async () => {\n if (simulating) return;\n if (!simulation?.bridgeSimulation?.intent) return;\n if (!inputs?.amount) return;\n await simulate(inputs?.amount);\n };\n\n const onSuccess = async () => {\n stopwatch.stop();\n dispatch({ type: \"setStatus\", payload: \"success\" });\n await fetchBridgableBalance();\n };\n\n const resetState = useCallback(() => {\n allowance.current = null;\n intent.current = null;\n setSimulation(null);\n setSimulating(false);\n transactionStartedRef.current = false;\n autoAllowRef.current = false;\n activeSimulationIdRef.current = null;\n resetSteps();\n stopwatch.stop();\n stopwatch.reset();\n dispatch({ type: \"reset\" });\n }, [allowance, intent, resetSteps, stopwatch]);\n\n const reset = useCallback(() => {\n intent.current?.deny();\n resetState();\n }, [intent, resetState]);\n\n const startTransaction = useCallback(() => {\n // Prevent re-entrancy while a transaction is already executing\n if (isProcessing) return;\n activeSimulationIdRef.current = null;\n setSimulating(false);\n dispatch({ type: \"setError\", payload: null });\n autoAllowRef.current = true;\n void handleTransaction();\n }, [handleTransaction, isProcessing]);\n\n useEffect(() => {\n const hasRequiredInputs =\n Boolean(debouncedAmount) && Boolean(inputs?.chain) && Boolean(token);\n if (!hasRequiredInputs || isProcessing || isSuccess) return;\n void simulate(debouncedAmount);\n }, [debouncedAmount, inputs?.chain, token, isProcessing, isSuccess]);\n\n useEffect(() => {\n if (autoAllowRef.current && intent.current) {\n intent.current.allow();\n autoAllowRef.current = false;\n }\n }, [intent.current]);\n\n usePolling(\n Boolean(simulation?.bridgeSimulation?.intent) &&\n !isProcessing &&\n !isSuccess,\n async () => {\n await refreshSimulation();\n },\n 15000,\n );\n\n return {\n // State\n inputs,\n setInputs,\n status,\n explorerUrls,\n\n // Derived state\n loading,\n isProcessing,\n isSuccess,\n isError,\n simulating,\n refreshing,\n\n // Error handling\n txError,\n setTxError,\n\n // Timer\n timer: stopwatch.seconds,\n\n // Balance data\n filteredBridgableBalance,\n unfilteredBridgableBalance,\n\n // Simulation data\n simulation,\n lastResult,\n steps,\n feeBreakdown,\n\n // Actions\n handleTransaction,\n startTransaction,\n reset,\n simulate,\n cancelSimulation: () => {\n activeSimulationIdRef.current = null;\n setSimulating(false);\n setSimulation(null);\n },\n };\n};\n\nexport default useDeposit;\n", + "content": "\"use client\";\n\nimport type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport {\n type OnIntentHookData,\n type OnAllowanceHookData,\n type ExecuteParams,\n type BridgeAndExecuteParams,\n type BridgeAndExecuteResult,\n type BridgeAndExecuteSimulationResult,\n type TokenBalance,\n type ChainBalance,\n} from \"@avail-project/nexus-sdk-v2\";\nimport { formatTokenBalance } from \"@avail-project/nexus-sdk-v2/utils\";\nimport {\n useEffect,\n useMemo,\n useRef,\n useState,\n useReducer,\n useCallback,\n type RefObject,\n} from \"react\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { formatUnits, type Address } from \"viem\";\nimport {\n useDebouncedValue,\n useNexusError,\n usePolling,\n useStopwatch,\n useTransactionSteps,\n} from \"../../common\";\n\ntype NexusClient = ReturnType;\n\n// v2 uses a generic step shape\ntype BridgeStepType = {\n typeID?: string;\n type?: string;\n [key: string]: unknown;\n};\n\nexport type DepositStatus =\n | \"idle\"\n | \"previewing\"\n | \"executing\"\n | \"success\"\n | \"error\";\n\ninterface DepositInputs {\n chain: number;\n amount?: string;\n selectedSources: number[];\n}\n\ninterface UseDepositProps {\n token: string;\n chain: number;\n nexusSDK: NexusClient | null;\n intent: RefObject;\n allowance: RefObject;\n bridgableBalance: TokenBalance[] | null;\n fetchBridgableBalance: () => Promise;\n chainOptions?: { id: number; name: string; logo: string }[];\n address: Address;\n executeBuilder?: (\n token: string,\n amount: string,\n chainId: number,\n userAddress: `0x${string}`,\n ) => Omit;\n executeConfig?: Omit;\n}\n\ntype DepositState = {\n inputs: DepositInputs;\n status: DepositStatus;\n explorerUrls: {\n intentUrl: string | null;\n executeUrl: string | null;\n };\n error: string | null;\n lastResult: BridgeAndExecuteResult | null;\n};\n\ntype Action =\n | { type: \"setInputs\"; payload: Partial }\n | { type: \"resetInputs\" }\n | { type: \"setStatus\"; payload: DepositStatus }\n | { type: \"setExplorerUrls\"; payload: Partial }\n | { type: \"setError\"; payload: string | null }\n | { type: \"setLastResult\"; payload: BridgeAndExecuteResult | null }\n | { type: \"reset\" };\n\nconst useDeposit = ({\n token,\n chain,\n nexusSDK,\n intent,\n bridgableBalance,\n chainOptions,\n address,\n executeBuilder,\n executeConfig,\n allowance,\n fetchBridgableBalance,\n}: UseDepositProps) => {\n const { getFiatValue } = useNexus();\n const handleNexusError = useNexusError();\n\n const allSourceIds = useMemo(\n () => chainOptions?.map((c) => c.id) ?? [],\n [chainOptions],\n );\n\n const createInitialState = useCallback(\n (): DepositState => ({\n inputs: {\n chain,\n amount: undefined,\n selectedSources: allSourceIds,\n },\n status: \"idle\",\n explorerUrls: {\n intentUrl: null,\n executeUrl: null,\n },\n error: null,\n lastResult: null,\n }),\n [chain, allSourceIds],\n );\n\n const initialState = createInitialState();\n\n function reducer(state: DepositState, action: Action): DepositState {\n switch (action.type) {\n case \"setInputs\": {\n const newInputs = { ...state.inputs, ...action.payload };\n let newStatus = state.status;\n if (\n state.status === \"idle\" &&\n newInputs.amount &&\n Number.parseFloat(newInputs.amount) > 0\n ) {\n newStatus = \"previewing\";\n }\n if (\n state.status === \"previewing\" &&\n (!newInputs.amount || Number.parseFloat(newInputs.amount) <= 0)\n ) {\n newStatus = \"idle\";\n }\n return { ...state, inputs: newInputs, status: newStatus };\n }\n case \"resetInputs\":\n return {\n ...state,\n inputs: { chain, amount: undefined, selectedSources: allSourceIds },\n status: \"idle\",\n };\n case \"setStatus\":\n return { ...state, status: action.payload };\n case \"setExplorerUrls\":\n return {\n ...state,\n explorerUrls: { ...state.explorerUrls, ...action.payload },\n };\n case \"setError\":\n return { ...state, error: action.payload };\n case \"setLastResult\":\n return { ...state, lastResult: action.payload };\n case \"reset\":\n return createInitialState();\n default:\n return state;\n }\n }\n\n const [state, dispatch] = useReducer(reducer, initialState);\n const { inputs, status, explorerUrls, error: txError, lastResult } = state;\n\n const setInputs = (next: Partial) => {\n dispatch({ type: \"setInputs\", payload: next });\n };\n\n const setTxError = (error: string | null) => {\n dispatch({ type: \"setError\", payload: error });\n };\n\n const loading = status === \"executing\";\n const isProcessing = status === \"executing\";\n const isSuccess = status === \"success\";\n const isError = status === \"error\";\n\n // Simulation state (useState)\n const [simulation, setSimulation] =\n useState(null);\n const [simulating, setSimulating] = useState(false);\n\n // Derived: refreshing = simulating while we already have a simulation\n const refreshing = simulating && simulation !== null;\n\n // Refs for non-rendering state\n const autoAllowRef = useRef(false);\n const transactionStartedRef = useRef(false);\n const simulationRequestIdRef = useRef(0);\n const activeSimulationIdRef = useRef(null);\n\n const {\n steps,\n onStepsList,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n\n const unfilteredBridgableBalance = useMemo(() => {\n const tokenBalance = bridgableBalance?.find((bal) => bal?.symbol === token);\n if (!tokenBalance) return undefined;\n\n // v2: chainBalances replaces breakdown\n const nonZeroChainBalances = tokenBalance.chainBalances.filter(\n (chain) => Number.parseFloat(chain.balance) > 0,\n );\n\n const totalBalance = nonZeroChainBalances.reduce(\n (sum, chain) => sum + Number.parseFloat(chain.balance),\n 0,\n );\n\n const totalValue = nonZeroChainBalances.reduce(\n (sum, chain) => sum + Number.parseFloat(chain.value ?? \"0\"),\n 0,\n );\n\n return {\n ...tokenBalance,\n balance: totalBalance.toString(),\n value: totalValue.toString(),\n chainBalances: nonZeroChainBalances,\n };\n }, [bridgableBalance, token]);\n\n const filteredBridgableBalance = useMemo(() => {\n const tokenBalance = bridgableBalance?.find((bal) => bal?.symbol === token);\n if (!tokenBalance) return undefined;\n\n const selectedSourcesSet = new Set(inputs.selectedSources);\n // v2: chainBalances replaces breakdown\n const filteredChainBalances = tokenBalance.chainBalances.filter(\n (chain) =>\n selectedSourcesSet.has(chain.chain.id) &&\n Number.parseFloat(chain.balance) > 0,\n );\n\n const totalBalance = filteredChainBalances.reduce(\n (sum, chain) => sum + Number.parseFloat(chain.balance),\n 0,\n );\n\n const totalValue = filteredChainBalances.reduce(\n (sum, chain) => sum + Number.parseFloat(chain.value ?? \"0\"),\n 0,\n );\n\n return {\n ...tokenBalance,\n balance: totalBalance.toString(),\n value: totalValue.toString(),\n chainBalances: filteredChainBalances,\n };\n }, [bridgableBalance, token, inputs.selectedSources]);\n\n const allCompleted = useMemo(\n () => (steps?.length ?? 0) > 0 && steps.every((s) => s.completed),\n [steps],\n );\n\n const stopwatch = useStopwatch({\n running: isProcessing && !allCompleted && transactionStartedRef.current,\n intervalMs: 100,\n });\n\n const debouncedAmount = useDebouncedValue(inputs?.amount ?? \"\", 1200);\n\n const feeBreakdown = useMemo(() => {\n if (!nexusSDK || !simulation || !token)\n return {\n totalGasFee: 0,\n bridgeUsd: 0,\n bridgeFormatted: \"0\",\n gasUsd: 0,\n gasFormatted: \"0\",\n };\n // v2: ExecuteSimulation.estimatedTotalCost (bigint) replaces gasFee\n const costRaw = simulation?.executeSimulation?.estimatedTotalCost;\n const nativeDecimals = 18;\n const nativeSymbol = \"ETH\";\n\n const gasFormatted =\n formatTokenBalance(costRaw, {\n symbol: nativeSymbol,\n decimals: nativeDecimals,\n }) ?? \"0\";\n const gasUnits = Number.parseFloat(\n formatUnits(costRaw ?? BigInt(0), nativeDecimals),\n );\n\n const gasUsd = getFiatValue(gasUnits, nativeSymbol);\n if (simulation?.bridgeSimulation) {\n const tokenDecimals =\n simulation?.bridgeSimulation?.intent?.destination?.token?.decimals;\n const bridgeFormatted =\n formatTokenBalance(simulation?.bridgeSimulation?.intent?.fees?.total, {\n symbol: token,\n decimals: tokenDecimals,\n }) ?? \"0\";\n const bridgeUsd = getFiatValue(\n Number.parseFloat(simulation?.bridgeSimulation?.intent?.fees?.total ?? \"0\"),\n token,\n );\n\n const totalGasFee = bridgeUsd + gasUsd;\n\n return {\n totalGasFee: `$${totalGasFee.toFixed(4)} USD`,\n bridgeUsd,\n bridgeFormatted,\n gasUsd,\n gasFormatted,\n };\n }\n return {\n totalGasFee: gasFormatted,\n gasUsd,\n gasFormatted,\n };\n }, [nexusSDK, simulation, chain, token, getFiatValue]);\n\n const handleTransaction = async () => {\n if (!inputs?.amount || !inputs?.chain) return;\n if (!inputs.selectedSources?.length) {\n dispatch({\n type: \"setError\",\n payload: \"Select at least 1 source chain to continue.\",\n });\n return;\n }\n dispatch({ type: \"setStatus\", payload: \"executing\" });\n dispatch({ type: \"setError\", payload: null });\n try {\n if (!nexusSDK) throw new Error(\"Nexus SDK not initialized\");\n const amountBigInt = nexusSDK.convertTokenReadableAmountToBigInt(\n inputs.amount,\n token,\n inputs.chain,\n );\n const executeParams: Omit | undefined =\n executeBuilder\n ? executeBuilder(token, inputs.amount, inputs.chain, address)\n : executeConfig;\n // v2: BridgeAndExecuteParams uses toTokenSymbol, toAmountRaw, sources\n const params: BridgeAndExecuteParams = {\n toTokenSymbol: token,\n toAmountRaw: amountBigInt,\n toChainId: inputs.chain,\n sources: inputs.selectedSources,\n execute: executeParams as Omit,\n waitForReceipt: true,\n };\n\n const result: BridgeAndExecuteResult = await nexusSDK.bridgeAndExecute(\n params,\n {\n onEvent: (event) => {\n // v2: events use event.type (not event.name)\n if (event.type === \"plan_preview\") {\n const planEvent = event as { type: string; plan: { steps?: unknown[] } };\n const list = planEvent.plan?.steps ?? [];\n onStepsList(list as Parameters[0]);\n }\n if (event.type === \"plan_progress\") {\n if (\n !transactionStartedRef.current &&\n (event as { stepType?: string })?.stepType === \"bridge_request_signing\"\n ) {\n transactionStartedRef.current = true;\n }\n onStepComplete(event as Parameters[0]);\n }\n },\n },\n );\n\n if (!result) {\n dispatch({ type: \"setError\", payload: \"Transaction rejected by user\" });\n dispatch({ type: \"setStatus\", payload: \"error\" });\n return;\n }\n dispatch({ type: \"setLastResult\", payload: result });\n dispatch({\n type: \"setExplorerUrls\",\n payload: {\n intentUrl: result.bridgeExplorerUrl ?? null,\n executeUrl: result.executeExplorerUrl ?? null,\n },\n });\n await onSuccess();\n } catch (error) {\n const { message } = handleNexusError(error);\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n dispatch({ type: \"setError\", payload: message });\n dispatch({ type: \"setStatus\", payload: \"error\" });\n }\n };\n\n const simulate = async (overrideAmount?: string) => {\n if (!nexusSDK || isProcessing || isSuccess) return;\n\n const amountToUse = overrideAmount ?? inputs?.amount;\n\n if (!amountToUse || !inputs?.chain) {\n activeSimulationIdRef.current = null;\n setSimulation(null);\n return;\n }\n if (\n Number.parseFloat(amountToUse) >\n Number.parseFloat(filteredBridgableBalance?.balance ?? \"0\")\n ) {\n activeSimulationIdRef.current = null;\n dispatch({ type: \"setError\", payload: \"Insufficient balance\" });\n setSimulation(null);\n return;\n }\n if (!inputs.selectedSources?.length) {\n activeSimulationIdRef.current = null;\n dispatch({\n type: \"setError\",\n payload: \"Select at least 1 source chain to continue.\",\n });\n setSimulation(null);\n return;\n }\n const requestId = ++simulationRequestIdRef.current;\n activeSimulationIdRef.current = requestId;\n setSimulating(true);\n try {\n const amountBigInt = nexusSDK.convertTokenReadableAmountToBigInt(\n amountToUse,\n token,\n inputs.chain,\n );\n const executeParams: Omit | undefined =\n executeBuilder\n ? executeBuilder(token, amountToUse, inputs.chain, address)\n : executeConfig;\n // v2: BridgeAndExecuteParams uses toTokenSymbol, toAmountRaw, sources\n const params: BridgeAndExecuteParams = {\n toTokenSymbol: token,\n toAmountRaw: amountBigInt,\n toChainId: inputs.chain,\n sources: inputs.selectedSources,\n execute: executeParams as Omit,\n waitForReceipt: false,\n };\n const sim = await nexusSDK.simulateBridgeAndExecute(params);\n if (activeSimulationIdRef.current !== requestId) {\n return;\n }\n if (sim) {\n dispatch({ type: \"setError\", payload: null });\n setSimulation(sim);\n } else {\n setSimulation(null);\n dispatch({ type: \"setError\", payload: \"Simulation failed\" });\n }\n } catch (error) {\n if (activeSimulationIdRef.current !== requestId) {\n return;\n }\n setSimulation(null);\n const { message } = handleNexusError(error);\n dispatch({ type: \"setError\", payload: message });\n } finally {\n if (activeSimulationIdRef.current === requestId) {\n setSimulating(false);\n }\n }\n };\n\n const refreshSimulation = async () => {\n if (simulating) return;\n if (!simulation?.bridgeSimulation?.intent) return;\n if (!inputs?.amount) return;\n await simulate(inputs?.amount);\n };\n\n const onSuccess = async () => {\n stopwatch.stop();\n dispatch({ type: \"setStatus\", payload: \"success\" });\n await fetchBridgableBalance();\n };\n\n const resetState = useCallback(() => {\n allowance.current = null;\n intent.current = null;\n setSimulation(null);\n setSimulating(false);\n transactionStartedRef.current = false;\n autoAllowRef.current = false;\n activeSimulationIdRef.current = null;\n resetSteps();\n stopwatch.stop();\n stopwatch.reset();\n dispatch({ type: \"reset\" });\n }, [allowance, intent, resetSteps, stopwatch]);\n\n const reset = useCallback(() => {\n intent.current?.deny();\n resetState();\n }, [intent, resetState]);\n\n const startTransaction = useCallback(() => {\n // Prevent re-entrancy while a transaction is already executing\n if (isProcessing) return;\n activeSimulationIdRef.current = null;\n setSimulating(false);\n dispatch({ type: \"setError\", payload: null });\n autoAllowRef.current = true;\n void handleTransaction();\n }, [handleTransaction, isProcessing]);\n\n useEffect(() => {\n const hasRequiredInputs =\n Boolean(debouncedAmount) && Boolean(inputs?.chain) && Boolean(token);\n if (!hasRequiredInputs || isProcessing || isSuccess) return;\n void simulate(debouncedAmount);\n }, [debouncedAmount, inputs?.chain, token, isProcessing, isSuccess]);\n\n useEffect(() => {\n if (autoAllowRef.current && intent.current) {\n intent.current.allow();\n autoAllowRef.current = false;\n }\n }, [intent.current]);\n\n usePolling(\n Boolean(simulation?.bridgeSimulation?.intent) &&\n !isProcessing &&\n !isSuccess,\n async () => {\n await refreshSimulation();\n },\n 15000,\n );\n\n return {\n // State\n inputs,\n setInputs,\n status,\n explorerUrls,\n\n // Derived state\n loading,\n isProcessing,\n isSuccess,\n isError,\n simulating,\n refreshing,\n\n // Error handling\n txError,\n setTxError,\n\n // Timer\n timer: stopwatch.seconds,\n\n // Balance data\n filteredBridgableBalance,\n unfilteredBridgableBalance,\n\n // Simulation data\n simulation,\n lastResult,\n steps,\n feeBreakdown,\n\n // Actions\n handleTransaction,\n startTransaction,\n reset,\n simulate,\n cancelSimulation: () => {\n activeSimulationIdRef.current = null;\n setSimulating(false);\n setSimulation(null);\n },\n };\n};\n\nexport default useDeposit;\n", "type": "registry:component", "target": "components/bridge-deposit/hooks/useDeposit.ts" }, @@ -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-sdk-v2\";\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.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.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.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 // v2: RFF_FEE_EXPIRED was removed; use string key for forward compat\n [\"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 );\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 as unknown as { data?: { context?: unknown } })?.data?.context,\n details: (err as unknown as { data?: { details?: unknown } })?.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" }, @@ -160,13 +160,13 @@ }, { "path": "registry/nexus-elements/common/hooks/useTransactionExecution.ts", - "content": "import {\n type BridgeStepType,\n NEXUS_EVENTS,\n type NexusSDK,\n type OnAllowanceHookData,\n type OnIntentHookData,\n} from \"@avail-project/nexus-core\";\nimport {\n type Dispatch,\n type RefObject,\n type SetStateAction,\n useCallback,\n useRef,\n} from \"react\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport {\n type SourceSelectionValidation,\n type TransactionFlowEvent,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n} from \"../types/transaction-flow\";\n\ninterface NexusErrorInfo {\n code: string;\n message: string;\n context?: unknown;\n details?: unknown;\n}\n\ntype NexusErrorHandler = (error: unknown) => NexusErrorInfo;\n\ninterface UseTransactionExecutionProps {\n operationName: \"bridge\" | \"transfer\";\n nexusSDK: NexusSDK | null;\n intent: RefObject;\n allowance: RefObject;\n inputs: TransactionFlowInputs;\n configuredMaxAmount?: string;\n allAvailableSourceChainIds: number[];\n sourceChainsForSdk?: number[];\n sourceSelectionKey: string;\n sourceSelection: SourceSelectionValidation;\n loading: boolean;\n txError: string | null;\n areInputsValid: boolean;\n executeTransaction: TransactionFlowExecutor;\n getMaxForCurrentSelection: () => Promise;\n onStepsList: (steps: BridgeStepType[]) => void;\n onStepComplete: (step: BridgeStepType) => void;\n resetSteps: () => void;\n setStatus: (status: TransactionStatus) => void;\n resetInputs: () => void;\n setRefreshing: Dispatch>;\n setIsDialogOpen: Dispatch>;\n setTxError: Dispatch>;\n setLastExplorerUrl: Dispatch>;\n setSelectedSourceChains: Dispatch>;\n setAppliedSourceSelectionKey: Dispatch>;\n stopwatch: {\n start: () => void;\n stop: () => void;\n reset: () => void;\n };\n handleNexusError: NexusErrorHandler;\n onStart?: () => void;\n onComplete?: (explorerUrl?: string) => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n notifyHistoryRefresh?: () => void;\n}\n\nexport function useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n}: UseTransactionExecutionProps) {\n const commitLockRef = useRef(false);\n const runIdRef = useRef(0);\n\n const refreshIntent = async (options?: { reportError?: boolean }) => {\n if (!intent.current) return false;\n const activeRunId = runIdRef.current;\n setRefreshing(true);\n try {\n const updated = await intent.current.refresh(sourceChainsForSdk);\n if (activeRunId !== runIdRef.current) return false;\n if (updated) {\n intent.current.intent = updated;\n }\n setAppliedSourceSelectionKey(sourceSelectionKey);\n return true;\n } catch (error) {\n if (activeRunId !== runIdRef.current) return false;\n console.error(\"Transaction failed:\", error);\n if (options?.reportError) {\n const message = \"Unable to refresh source selection. Please try again.\";\n setTxError(message);\n onError?.(message);\n }\n return false;\n } finally {\n if (activeRunId === runIdRef.current) {\n setRefreshing(false);\n }\n }\n };\n\n const onSuccess = async (explorerUrl?: string) => {\n stopwatch.stop();\n setStatus(\"success\");\n onComplete?.(explorerUrl);\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n await fetchBalance();\n notifyHistoryRefresh?.();\n };\n\n const handleTransaction = async () => {\n if (commitLockRef.current) return;\n commitLockRef.current = true;\n const currentRunId = ++runIdRef.current;\n let didEnterExecutingState = false;\n const cleanupSupersededExecution = () => {\n if (!didEnterExecutingState) return;\n setRefreshing(false);\n setIsDialogOpen(false);\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n setStatus(\"idle\");\n };\n\n try {\n if (\n !inputs?.amount ||\n !inputs?.recipient ||\n !inputs?.chain ||\n !inputs?.token\n ) {\n console.error(\"Missing required inputs\");\n return;\n }\n if (!nexusSDK) {\n const message = \"Nexus SDK not initialized\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n const parsedAmount = Number(inputs.amount);\n if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) {\n const message = \"Enter a valid amount greater than 0.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n const amountBigInt = nexusSDK.convertTokenReadableAmountToBigInt(\n inputs.amount,\n inputs.token,\n inputs.chain,\n );\n\n if (configuredMaxAmount) {\n const configuredMaxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n configuredMaxAmount,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > configuredMaxRaw) {\n const message = `Amount exceeds maximum limit of ${configuredMaxAmount} ${inputs.token}.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n }\n\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (currentRunId !== runIdRef.current) return;\n if (!maxForCurrentSelection) {\n const message = `Unable to determine max ${operationName} amount for selected sources. Please try again.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n const maxForSelectionRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxForCurrentSelection,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > maxForSelectionRaw) {\n const message = `Selected sources can provide up to ${maxForCurrentSelection} ${inputs.token}. Reduce amount or enable more sources.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n setStatus(\"executing\");\n didEnterExecutingState = true;\n setTxError(null);\n onStart?.();\n setLastExplorerUrl(\"\");\n setAppliedSourceSelectionKey(sourceSelectionKey);\n\n const onEvent = (event: TransactionFlowEvent) => {\n if (currentRunId !== runIdRef.current) return;\n if (event.name === NEXUS_EVENTS.STEPS_LIST) {\n const list = Array.isArray(event.args) ? event.args : [];\n onStepsList(list as BridgeStepType[]);\n }\n if (event.name === NEXUS_EVENTS.STEP_COMPLETE) {\n if (\n !Array.isArray(event.args) &&\n \"type\" in event.args &&\n event.args.type === \"INTENT_HASH_SIGNED\"\n ) {\n stopwatch.start();\n }\n if (!Array.isArray(event.args)) {\n onStepComplete(event.args as BridgeStepType);\n }\n }\n };\n\n const transactionResult = await executeTransaction({\n token: inputs.token,\n amount: amountBigInt,\n toChainId: inputs.chain,\n recipient: inputs.recipient,\n sourceChains: sourceChainsForSdk,\n onEvent,\n });\n\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution();\n return;\n }\n if (!transactionResult) {\n throw new Error(\"Transaction rejected by user\");\n }\n setLastExplorerUrl(transactionResult.explorerUrl);\n await onSuccess(transactionResult.explorerUrl);\n } catch (error) {\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution();\n return;\n }\n const { message, code, context, details } = handleNexusError(error);\n console.error(`Fast ${operationName} transaction failed:`, {\n code,\n message,\n context,\n details,\n });\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n setTxError(message);\n onError?.(message);\n setIsDialogOpen(false);\n setSelectedSourceChains(null);\n setRefreshing(false);\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n void fetchBalance();\n setStatus(\"error\");\n } finally {\n commitLockRef.current = false;\n }\n };\n\n const reset = () => {\n runIdRef.current += 1;\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setStatus(\"idle\");\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n };\n\n const startTransaction = () => {\n if (!intent.current) return;\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (sourceSelection.isBelowRequired && inputs?.token) {\n const message = `Selected sources are not enough. Add ${sourceSelection.missingToProceed} ${inputs.token} more to make this transaction.`;\n setTxError(message);\n onError?.(message);\n return;\n }\n void (async () => {\n const refreshed = await refreshIntent({ reportError: true });\n if (!refreshed || !intent.current) return;\n intent.current.allow();\n setIsDialogOpen(true);\n setTxError(null);\n })();\n };\n\n const commitAmount = async () => {\n if (intent.current || loading || txError || !areInputsValid) return;\n await handleTransaction();\n };\n\n const invalidatePendingExecution = useCallback(() => {\n runIdRef.current += 1;\n if (intent.current) {\n intent.current.deny();\n intent.current = null;\n }\n setRefreshing(false);\n setAppliedSourceSelectionKey(\"ALL\");\n }, [intent, setAppliedSourceSelectionKey, setRefreshing]);\n\n return {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n };\n}\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type { OnAllowanceHookData, OnIntentHookData } from \"@avail-project/nexus-sdk-v2\";\nimport {\n type Dispatch,\n type RefObject,\n type SetStateAction,\n useCallback,\n useRef,\n} from \"react\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport {\n type SourceSelectionValidation,\n type TransactionFlowEvent,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n} from \"../types/transaction-flow\";\n\ntype NexusClient = ReturnType;\n\ninterface NexusErrorInfo {\n code: string;\n message: string;\n context?: unknown;\n details?: unknown;\n}\n\ntype NexusErrorHandler = (error: unknown) => NexusErrorInfo;\n\n// v2 plan_progress step types for bridge\nconst BRIDGE_STEP_INTENT_SIGNED = \"request_signing\";\n\ninterface UseTransactionExecutionProps {\n operationName: \"bridge\" | \"transfer\";\n nexusSDK: NexusClient | null;\n intent: RefObject;\n allowance: RefObject;\n inputs: TransactionFlowInputs;\n configuredMaxAmount?: string;\n allAvailableSourceChainIds: number[];\n sourceChainsForSdk?: number[];\n sourceSelectionKey: string;\n sourceSelection: SourceSelectionValidation;\n loading: boolean;\n txError: string | null;\n areInputsValid: boolean;\n executeTransaction: TransactionFlowExecutor;\n getMaxForCurrentSelection: () => Promise;\n onStepsList: (steps: { typeID?: string; type?: string; [key: string]: unknown }[]) => void;\n onStepComplete: (step: { typeID?: string; type?: string; [key: string]: unknown }) => void;\n resetSteps: () => void;\n setStatus: (status: TransactionStatus) => void;\n resetInputs: () => void;\n setRefreshing: Dispatch>;\n setIsDialogOpen: Dispatch>;\n setTxError: Dispatch>;\n setLastExplorerUrl: Dispatch>;\n setSelectedSourceChains: Dispatch>;\n setAppliedSourceSelectionKey: Dispatch>;\n stopwatch: {\n start: () => void;\n stop: () => void;\n reset: () => void;\n };\n handleNexusError: NexusErrorHandler;\n onStart?: () => void;\n onComplete?: (explorerUrl?: string) => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n notifyHistoryRefresh?: () => void;\n}\n\nexport function useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n}: UseTransactionExecutionProps) {\n const commitLockRef = useRef(false);\n const runIdRef = useRef(0);\n\n const refreshIntent = async (options?: { reportError?: boolean }) => {\n if (!intent.current) return false;\n const activeRunId = runIdRef.current;\n setRefreshing(true);\n try {\n const updated = await intent.current.refresh(sourceChainsForSdk);\n if (activeRunId !== runIdRef.current) return false;\n if (updated) {\n intent.current.intent = updated;\n }\n setAppliedSourceSelectionKey(sourceSelectionKey);\n return true;\n } catch (error) {\n if (activeRunId !== runIdRef.current) return false;\n console.error(\"Transaction failed:\", error);\n if (options?.reportError) {\n const message = \"Unable to refresh source selection. Please try again.\";\n setTxError(message);\n onError?.(message);\n }\n return false;\n } finally {\n if (activeRunId === runIdRef.current) {\n setRefreshing(false);\n }\n }\n };\n\n const onSuccess = async (explorerUrl?: string) => {\n stopwatch.stop();\n setStatus(\"success\");\n onComplete?.(explorerUrl);\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n await fetchBalance();\n notifyHistoryRefresh?.();\n };\n\n const handleTransaction = async () => {\n if (commitLockRef.current) return;\n commitLockRef.current = true;\n const currentRunId = ++runIdRef.current;\n let didEnterExecutingState = false;\n // Declared here (outside try/catch) so both the event handler and the catch block\n // can read/write it — prevents the catch from clobbering event-driven completions\n let completedFromEvent = false;\n const cleanupSupersededExecution = () => {\n if (!didEnterExecutingState) return;\n // Don't tear down the dialog if an event already handled success/failure —\n // resetInputs() inside onSuccess triggers invalidatePendingExecution which\n // increments runIdRef, making this branch fire spuriously.\n if (completedFromEvent) return;\n setRefreshing(false);\n setIsDialogOpen(false);\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n setStatus(\"idle\");\n };\n\n\n try {\n if (\n !inputs?.amount ||\n !inputs?.recipient ||\n !inputs?.chain ||\n !inputs?.token\n ) {\n console.error(\"Missing required inputs\");\n return;\n }\n if (!nexusSDK) {\n const message = \"Nexus SDK not initialized\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n const parsedAmount = Number(inputs.amount);\n if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) {\n const message = \"Enter a valid amount greater than 0.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n // v2: convertTokenReadableAmountToBigInt(amount, tokenSymbol, chainId)\n const amountBigInt = nexusSDK.convertTokenReadableAmountToBigInt(\n inputs.amount,\n inputs.token,\n inputs.chain,\n );\n\n if (configuredMaxAmount) {\n const configuredMaxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n configuredMaxAmount,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > configuredMaxRaw) {\n const message = `Amount exceeds maximum limit of ${configuredMaxAmount} ${inputs.token}.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n }\n\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (currentRunId !== runIdRef.current) return;\n if (!maxForCurrentSelection) {\n const message = `Unable to determine max ${operationName} amount for selected sources. Please try again.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n const maxForSelectionRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxForCurrentSelection,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > maxForSelectionRaw) {\n const message = `Selected sources can provide up to ${maxForCurrentSelection} ${inputs.token}. Reduce amount or enable more sources.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n setStatus(\"executing\");\n didEnterExecutingState = true;\n setTxError(null);\n onStart?.();\n setLastExplorerUrl(\"\");\n setAppliedSourceSelectionKey(sourceSelectionKey);\n\n // Terminal step types — when state:\"completed\" fires on these, the operation is done\n const TERMINAL_STEP_TYPES = new Set([\n \"bridge_fill\", // bridge & transfer final fill\n \"destination_swap\", // swap final step\n ]);\n\n // v2 onEvent uses typed discriminated union: { type, ... }\n const onEvent = (event: TransactionFlowEvent) => {\n if (currentRunId !== runIdRef.current) return;\n\n if (event.type === \"plan_preview\") {\n // Seed UI with the step list from the plan\n type StepShape = { typeID?: string; type?: string; [key: string]: unknown };\n const steps = ((event as { type: string; plan: { steps: StepShape[] } }).plan?.steps ?? []) as StepShape[];\n onStepsList(steps);\n }\n\n if (event.type === \"plan_progress\") {\n const progressEvent = event as {\n type: string;\n stepType: string;\n state: string;\n step: { typeID?: string; type?: string; [key: string]: unknown };\n error?: string;\n };\n\n // Always mark step as complete/updated in UI\n onStepComplete(progressEvent.step);\n\n const isTerminal = TERMINAL_STEP_TYPES.has(progressEvent.stepType);\n\n if (progressEvent.state === \"failed\") {\n // Any step failure → abort\n if (!completedFromEvent) {\n completedFromEvent = true;\n const errorMessage = progressEvent.error ?? \"Transaction failed\";\n stopwatch.stop();\n setTxError(errorMessage);\n onError?.(errorMessage);\n setStatus(\"error\");\n }\n return;\n }\n\n if (isTerminal && progressEvent.state === \"completed\") {\n // Terminal step completed → success\n if (!completedFromEvent) {\n completedFromEvent = true;\n stopwatch.stop();\n // explorerUrl is on the event itself, not the step object\n const explorerUrl = (event as { explorerUrl?: string }).explorerUrl;\n if (explorerUrl) setLastExplorerUrl(explorerUrl);\n void onSuccess(explorerUrl);\n }\n }\n }\n\n if (event.type === \"status\") {\n const statusEvent = event as { type: string; status: string };\n if (statusEvent.status === \"completed\" && !completedFromEvent) {\n completedFromEvent = true;\n stopwatch.stop();\n void onSuccess(undefined);\n }\n }\n };\n\n const transactionResult = await executeTransaction({\n token: inputs.token,\n amount: amountBigInt,\n toChainId: inputs.chain,\n recipient: inputs.recipient,\n sources: sourceChainsForSdk,\n onEvent,\n });\n\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution(); // no-op when completedFromEvent=true\n if (!completedFromEvent) return; // only bail if not already completed\n // else fall through — still want to capture explorerUrl from the result\n }\n if (!transactionResult) {\n if (!completedFromEvent) {\n throw new Error(\"Transaction rejected by user\");\n }\n // Already handled via events\n return;\n }\n\n // SDK promise resolved — use result for explorerUrl if event-driven success didn't set it\n if (!completedFromEvent) {\n // Fallback: SDK resolved but we never got a terminal event (e.g. single-step flows)\n setLastExplorerUrl(transactionResult.explorerUrl ?? \"\");\n await onSuccess(transactionResult.explorerUrl);\n } else {\n // Event-driven success already ran — capture the explorerUrl from the resolved result\n if (transactionResult.explorerUrl) {\n setLastExplorerUrl(transactionResult.explorerUrl);\n }\n }\n\n } catch (error) {\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution();\n return;\n }\n // If event-driven success/failure already handled this transaction, ignore SDK-level errors\n // (the SDK may throw or return oddly after a successful fill event)\n if (completedFromEvent) return;\n const { message, code, context, details } = handleNexusError(error);\n console.error(`Fast ${operationName} transaction failed:`, {\n code,\n message,\n context,\n details,\n });\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n setTxError(message);\n onError?.(message);\n setIsDialogOpen(false);\n setSelectedSourceChains(null);\n setRefreshing(false);\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n void fetchBalance();\n setStatus(\"error\");\n } finally {\n commitLockRef.current = false;\n }\n };\n\n const reset = () => {\n runIdRef.current += 1;\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setStatus(\"idle\");\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n };\n\n const startTransaction = () => {\n if (!intent.current) return;\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (sourceSelection.isBelowRequired && inputs?.token) {\n const message = `Selected sources are not enough. Add ${sourceSelection.missingToProceed} ${inputs.token} more to make this transaction.`;\n setTxError(message);\n onError?.(message);\n return;\n }\n void (async () => {\n const refreshed = await refreshIntent({ reportError: true });\n if (!refreshed || !intent.current) return;\n intent.current.allow();\n setIsDialogOpen(true);\n // Start the stopwatch AFTER the dialog opens so the isDialogOpen effect\n // does not immediately reset it (the effect only resets when dialog is closed)\n stopwatch.reset();\n stopwatch.start();\n setTxError(null);\n })();\n };\n\n const commitAmount = async () => {\n if (intent.current || loading || txError || !areInputsValid) return;\n await handleTransaction();\n };\n\n const invalidatePendingExecution = useCallback(() => {\n runIdRef.current += 1;\n if (intent.current) {\n intent.current.deny();\n intent.current = null;\n }\n setRefreshing(false);\n setAppliedSourceSelectionKey(\"ALL\");\n }, [intent, setAppliedSourceSelectionKey, setRefreshing]);\n\n return {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n };\n}\n", "type": "registry:component", "target": "components/common/hooks/useTransactionExecution.ts" }, { "path": "registry/nexus-elements/common/hooks/useTransactionFlow.ts", - "content": "import {\n type BridgeStepType,\n type NexusNetwork,\n NexusSDK,\n type OnAllowanceHookData,\n type OnIntentHookData,\n parseUnits,\n type UserAsset,\n} from \"@avail-project/nexus-core\";\nimport {\n useEffect,\n useMemo,\n useCallback,\n useRef,\n useState,\n useReducer,\n type RefObject,\n} from \"react\";\nimport { type Address, isAddress } from \"viem\";\nimport { useNexusError } from \"./useNexusError\";\nimport { useTransactionExecution } from \"./useTransactionExecution\";\nimport { usePolling } from \"./usePolling\";\nimport { useStopwatch } from \"./useStopwatch\";\nimport { useDebouncedCallback } from \"./useDebouncedCallback\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport { useTransactionSteps } from \"../tx/useTransactionSteps\";\nimport {\n type SourceCoverageState,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n type TransactionFlowPrefill,\n type TransactionFlowType,\n} from \"../types/transaction-flow\";\nimport {\n MAX_AMOUNT_DEBOUNCE_MS,\n buildInitialInputs,\n clampAmountToMax,\n formatAmountForDisplay,\n getCoverageDecimals,\n normalizeMaxAmount,\n} from \"../utils/transaction-flow\";\n\ninterface BaseTransactionFlowProps {\n type: TransactionFlowType;\n network: NexusNetwork;\n nexusSDK: NexusSDK | null;\n intent: RefObject;\n allowance: RefObject;\n bridgableBalance: UserAsset[] | null;\n prefill?: TransactionFlowPrefill;\n onComplete?: (explorerUrl?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n maxAmount?: string | number;\n isSourceMenuOpen?: boolean;\n notifyHistoryRefresh?: () => void;\n executeTransaction: TransactionFlowExecutor;\n}\n\nexport interface UseTransactionFlowProps extends BaseTransactionFlowProps {\n connectedAddress?: Address;\n}\n\ntype State = {\n inputs: TransactionFlowInputs;\n status: TransactionStatus;\n};\n\ntype Action =\n | { type: \"setInputs\"; payload: Partial }\n | { type: \"resetInputs\" }\n | { type: \"setStatus\"; payload: TransactionStatus };\n\nexport function useTransactionFlow(props: UseTransactionFlowProps) {\n const {\n type,\n network,\n nexusSDK,\n intent,\n bridgableBalance,\n prefill,\n onComplete,\n onStart,\n onError,\n fetchBalance,\n allowance,\n maxAmount,\n isSourceMenuOpen = false,\n notifyHistoryRefresh,\n executeTransaction,\n } = props;\n\n const connectedAddress = props.connectedAddress;\n const operationName = type === \"bridge\" ? \"bridge\" : \"transfer\";\n const handleNexusError = useNexusError();\n const initialState: State = {\n inputs: buildInitialInputs({ type, network, connectedAddress, prefill }),\n status: \"idle\",\n };\n\n function reducer(state: State, action: Action): State {\n switch (action.type) {\n case \"setInputs\":\n return { ...state, inputs: { ...state.inputs, ...action.payload } };\n case \"resetInputs\":\n return {\n ...state,\n inputs: buildInitialInputs({\n type,\n network,\n connectedAddress,\n prefill,\n }),\n };\n case \"setStatus\":\n return { ...state, status: action.payload };\n default:\n return state;\n }\n }\n\n const [state, dispatch] = useReducer(reducer, initialState);\n const inputs = state.inputs;\n const setInputs = (\n next: TransactionFlowInputs | Partial,\n ) => {\n dispatch({\n type: \"setInputs\",\n payload: next as Partial,\n });\n };\n\n const loading = state.status === \"executing\";\n const [refreshing, setRefreshing] = useState(false);\n const [isDialogOpen, setIsDialogOpen] = useState(false);\n const [txError, setTxError] = useState(null);\n const [lastExplorerUrl, setLastExplorerUrl] = useState(\"\");\n const previousConnectedAddressRef = useRef
(\n connectedAddress,\n );\n const maxAmountRequestIdRef = useRef(0);\n const [selectedSourceChains, setSelectedSourceChains] = useState<\n number[] | null\n >(null);\n const [selectedSourcesMaxAmount, setSelectedSourcesMaxAmount] = useState<\n string | null\n >(null);\n const [appliedSourceSelectionKey, setAppliedSourceSelectionKey] =\n useState(\"ALL\");\n const {\n steps,\n onStepsList,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n const configuredMaxAmount = useMemo(\n () => normalizeMaxAmount(maxAmount),\n [maxAmount],\n );\n\n const areInputsValid = useMemo(() => {\n const hasToken = inputs?.token !== undefined && inputs?.token !== null;\n const hasChain = inputs?.chain !== undefined && inputs?.chain !== null;\n const hasAmount = Boolean(inputs?.amount) && Number(inputs?.amount) > 0;\n const hasValidRecipient =\n Boolean(inputs?.recipient) && isAddress(inputs.recipient as string);\n return hasToken && hasChain && hasAmount && hasValidRecipient;\n }, [inputs]);\n\n const filteredBridgableBalance = useMemo(() => {\n return bridgableBalance?.find((bal) =>\n inputs?.token === \"USDM\"\n ? bal?.symbol === \"USDC\"\n : bal?.symbol === inputs?.token,\n );\n }, [bridgableBalance, inputs?.token]);\n\n const availableSources = useMemo(() => {\n const breakdown = filteredBridgableBalance?.breakdown ?? [];\n const destinationChainId = inputs?.chain;\n const nonZero = breakdown.filter((source) => {\n if (Number.parseFloat(source.balance ?? \"0\") <= 0) return false;\n if (typeof destinationChainId === \"number\") {\n return source.chain.id !== destinationChainId;\n }\n return true;\n });\n const decimals = filteredBridgableBalance?.decimals;\n if (!nexusSDK || typeof decimals !== \"number\") {\n return nonZero.sort(\n (a, b) => Number.parseFloat(b.balance) - Number.parseFloat(a.balance),\n );\n }\n return nonZero.sort((a, b) => {\n try {\n const aRaw = parseUnits(a.balance ?? \"0\", decimals);\n const bRaw = parseUnits(b.balance ?? \"0\", decimals);\n if (aRaw === bRaw) return 0;\n return aRaw > bRaw ? -1 : 1;\n } catch {\n return Number.parseFloat(b.balance) - Number.parseFloat(a.balance);\n }\n });\n }, [\n inputs?.chain,\n filteredBridgableBalance?.breakdown,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n ]);\n\n const allAvailableSourceChainIds = useMemo(\n () => availableSources.map((source) => source.chain.id),\n [availableSources],\n );\n\n const effectiveSelectedSourceChains = useMemo(() => {\n if (selectedSourceChains && selectedSourceChains.length > 0) {\n const availableSet = new Set(allAvailableSourceChainIds);\n const filteredSelection = selectedSourceChains.filter((id) =>\n availableSet.has(id),\n );\n if (filteredSelection.length > 0) {\n return filteredSelection;\n }\n }\n return allAvailableSourceChainIds;\n }, [selectedSourceChains, allAvailableSourceChainIds]);\n\n const sourceChainsForSdk =\n effectiveSelectedSourceChains.length > 0\n ? effectiveSelectedSourceChains\n : undefined;\n\n const sourceSelectionKey = useMemo(() => {\n if (allAvailableSourceChainIds.length === 0) return \"NONE\";\n if (!selectedSourceChains || selectedSourceChains.length === 0) {\n return \"ALL\";\n }\n return [...effectiveSelectedSourceChains].sort((a, b) => a - b).join(\"|\");\n }, [\n allAvailableSourceChainIds.length,\n effectiveSelectedSourceChains,\n selectedSourceChains,\n ]);\n const hasPendingSourceSelectionChanges =\n sourceSelectionKey !== appliedSourceSelectionKey;\n const intentSourceSpendAmount = intent.current?.intent?.sourcesTotal;\n\n const getMaxForCurrentSelection = useCallback(async () => {\n if (!nexusSDK || !inputs?.token || !inputs?.chain) return undefined;\n const maxBalAvailable = await nexusSDK.calculateMaxForBridge({\n token: inputs.token,\n toChainId: inputs.chain,\n recipient: inputs.recipient,\n sourceChains: sourceChainsForSdk,\n });\n if (!maxBalAvailable?.amount) return \"0\";\n return clampAmountToMax({\n amount: maxBalAvailable.amount,\n maxAmount: configuredMaxAmount,\n nexusSDK,\n token: inputs.token,\n chainId: inputs.chain,\n });\n }, [\n configuredMaxAmount,\n inputs?.chain,\n inputs?.recipient,\n inputs?.token,\n nexusSDK,\n sourceChainsForSdk,\n ]);\n\n const toggleSourceChain = useCallback(\n (chainId: number) => {\n setSelectedSourceChains((prev) => {\n if (allAvailableSourceChainIds.length === 0) return prev;\n const current =\n prev && prev.length > 0 ? prev : allAvailableSourceChainIds;\n const next = current.includes(chainId)\n ? current.filter((id) => id !== chainId)\n : [...current, chainId];\n if (next.length === 0) {\n return current;\n }\n const isAllSelected =\n next.length === allAvailableSourceChainIds.length &&\n allAvailableSourceChainIds.every((id) => next.includes(id));\n return isAllSelected ? null : next;\n });\n },\n [allAvailableSourceChainIds],\n );\n\n const sourceSelection = useMemo(() => {\n const amount =\n intentSourceSpendAmount?.trim() ?? inputs?.amount?.trim() ?? \"\";\n const decimals = getCoverageDecimals({\n type,\n token: inputs?.token,\n chainId: inputs?.chain,\n fallback: filteredBridgableBalance?.decimals,\n });\n const selectedChainSet = new Set(effectiveSelectedSourceChains);\n const selectedTotalRaw =\n !nexusSDK || typeof decimals !== \"number\"\n ? BigInt(0)\n : availableSources.reduce((sum, source) => {\n if (!selectedChainSet.has(source.chain.id)) return sum;\n try {\n return sum + parseUnits(source.balance ?? \"0\", decimals);\n } catch {\n return sum;\n }\n }, BigInt(0));\n const selectedTotal =\n !nexusSDK || typeof decimals !== \"number\"\n ? \"0\"\n : formatAmountForDisplay(selectedTotalRaw, decimals, nexusSDK);\n const baseSelection = {\n selectedTotal,\n requiredTotal: amount || \"0\",\n requiredSafetyTotal: amount || \"0\",\n missingToProceed: \"0\",\n missingToSafety: \"0\",\n coverageState: \"healthy\" as SourceCoverageState,\n coverageToSafetyPercent: 100,\n isBelowRequired: false,\n isBelowSafetyBuffer: false,\n };\n\n if (!nexusSDK || !inputs?.token || !inputs?.chain || !amount) {\n return baseSelection;\n }\n\n try {\n const requiredRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n inputs.token,\n inputs.chain,\n );\n if (requiredRaw <= BigInt(0)) {\n return baseSelection;\n }\n\n const missingToProceedRaw =\n selectedTotalRaw >= requiredRaw\n ? BigInt(0)\n : requiredRaw - selectedTotalRaw;\n const missingToSafetyRaw = missingToProceedRaw;\n\n const coverageState: SourceCoverageState =\n selectedTotalRaw < requiredRaw ? \"error\" : \"healthy\";\n\n const coverageBasisPoints =\n requiredRaw === BigInt(0)\n ? 10_000\n : selectedTotalRaw >= requiredRaw\n ? 10_000\n : Number((selectedTotalRaw * BigInt(10_000)) / requiredRaw);\n\n return {\n selectedTotal,\n requiredTotal: amount,\n requiredSafetyTotal: amount,\n missingToProceed: formatAmountForDisplay(\n missingToProceedRaw,\n decimals,\n nexusSDK,\n ),\n missingToSafety: formatAmountForDisplay(\n missingToSafetyRaw,\n decimals,\n nexusSDK,\n ),\n coverageState,\n coverageToSafetyPercent: coverageBasisPoints / 100,\n isBelowRequired: coverageState === \"error\",\n isBelowSafetyBuffer: coverageState === \"error\",\n };\n } catch {\n return baseSelection;\n }\n }, [\n type,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n inputs?.chain,\n inputs?.amount,\n inputs?.token,\n intentSourceSpendAmount,\n availableSources,\n effectiveSelectedSourceChains,\n ]);\n\n const stopwatch = useStopwatch({ intervalMs: 100 });\n const setStatus = useCallback(\n (status: TransactionStatus) =>\n dispatch({ type: \"setStatus\", payload: status }),\n [],\n );\n\n const resetInputs = useCallback(() => {\n dispatch({ type: \"resetInputs\" });\n }, []);\n\n const {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n } = useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n });\n\n usePolling(\n Boolean(intent.current) &&\n !isDialogOpen &&\n !isSourceMenuOpen &&\n !hasPendingSourceSelectionChanges,\n async () => {\n await refreshIntent();\n },\n 15000,\n );\n\n const debouncedRefreshMaxForSelection = useDebouncedCallback(\n async (requestId: number) => {\n try {\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (requestId !== maxAmountRequestIdRef.current) return;\n setSelectedSourcesMaxAmount(maxForCurrentSelection ?? \"0\");\n } catch (error) {\n if (requestId !== maxAmountRequestIdRef.current) return;\n console.error(\"Unable to calculate max for selected sources:\", error);\n setSelectedSourcesMaxAmount(\"0\");\n }\n },\n MAX_AMOUNT_DEBOUNCE_MS,\n );\n\n useEffect(() => {\n debouncedRefreshMaxForSelection.cancel();\n if (!nexusSDK || !inputs?.token || !inputs?.chain) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(null);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(\"0\");\n return;\n }\n const requestId = ++maxAmountRequestIdRef.current;\n debouncedRefreshMaxForSelection(requestId);\n }, [\n allAvailableSourceChainIds.length,\n configuredMaxAmount,\n debouncedRefreshMaxForSelection,\n inputs?.recipient,\n sourceSelectionKey,\n inputs?.chain,\n inputs?.token,\n nexusSDK,\n ]);\n\n useEffect(() => {\n if (type !== \"bridge\" || !connectedAddress) return;\n const previousConnectedAddress = previousConnectedAddressRef.current;\n if (!previousConnectedAddress) {\n previousConnectedAddressRef.current = connectedAddress;\n return;\n }\n if (connectedAddress === previousConnectedAddress) return;\n previousConnectedAddressRef.current = connectedAddress;\n if (prefill?.recipient) return;\n if (!inputs?.recipient || inputs.recipient === previousConnectedAddress) {\n dispatch({ type: \"setInputs\", payload: { recipient: connectedAddress } });\n }\n }, [type, connectedAddress, inputs?.recipient, prefill?.recipient]);\n\n useEffect(() => {\n invalidatePendingExecution();\n }, [inputs, invalidatePendingExecution]);\n\n useEffect(() => {\n setSelectedSourceChains(null);\n }, [inputs?.token]);\n\n useEffect(() => {\n if (isDialogOpen) return;\n stopwatch.stop();\n stopwatch.reset();\n if (state.status === \"success\" || state.status === \"error\") {\n resetSteps();\n setLastExplorerUrl(\"\");\n setStatus(\"idle\");\n }\n }, [isDialogOpen, resetSteps, setStatus, state.status, stopwatch]);\n\n useEffect(() => {\n if (txError) {\n setTxError(null);\n }\n }, [inputs, txError]);\n\n return {\n inputs,\n setInputs,\n timer: stopwatch.seconds,\n setIsDialogOpen,\n setTxError,\n loading,\n refreshing,\n isDialogOpen,\n txError,\n handleTransaction,\n reset,\n filteredBridgableBalance,\n startTransaction,\n commitAmount,\n lastExplorerUrl,\n steps,\n status: state.status,\n availableSources,\n selectedSourceChains: effectiveSelectedSourceChains,\n toggleSourceChain,\n isSourceSelectionInsufficient: sourceSelection.isBelowRequired,\n isSourceSelectionBelowSafetyBuffer: sourceSelection.isBelowSafetyBuffer,\n isSourceSelectionReadyForAccept:\n sourceSelection.coverageState === \"healthy\",\n sourceCoverageState: sourceSelection.coverageState,\n sourceCoveragePercent: sourceSelection.coverageToSafetyPercent,\n missingToProceed: sourceSelection.missingToProceed,\n missingToSafety: sourceSelection.missingToSafety,\n selectedTotal: sourceSelection.selectedTotal,\n requiredTotal: sourceSelection.requiredTotal,\n requiredSafetyTotal: sourceSelection.requiredSafetyTotal,\n maxAvailableAmount: selectedSourcesMaxAmount ?? undefined,\n isInputsValid: areInputsValid,\n };\n}\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type {\n NexusNetwork,\n OnAllowanceHookData,\n OnIntentHookData,\n TokenBalance,\n ChainBalance,\n} from \"@avail-project/nexus-sdk-v2\";\nimport { parseUnits } from \"viem\";\nimport {\n useEffect,\n useMemo,\n useCallback,\n useRef,\n useState,\n useReducer,\n type RefObject,\n} from \"react\";\nimport { type Address, isAddress } from \"viem\";\nimport { useNexusError } from \"./useNexusError\";\nimport { useTransactionExecution } from \"./useTransactionExecution\";\nimport { usePolling } from \"./usePolling\";\nimport { useStopwatch } from \"./useStopwatch\";\nimport { useDebouncedCallback } from \"./useDebouncedCallback\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport { useTransactionSteps } from \"../tx/useTransactionSteps\";\nimport {\n type SourceCoverageState,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n type TransactionFlowPrefill,\n type TransactionFlowType,\n} from \"../types/transaction-flow\";\nimport {\n MAX_AMOUNT_DEBOUNCE_MS,\n buildInitialInputs,\n clampAmountToMax,\n formatAmountForDisplay,\n getCoverageDecimals,\n normalizeMaxAmount,\n} from \"../utils/transaction-flow\";\n\ntype NexusClient = ReturnType;\n\n// v2 uses a generic step shape; minimal type to satisfy getStepKey constraint\ntype BridgePlanStep = {\n typeID?: string;\n type?: string;\n [key: string]: unknown;\n};\n\ninterface BaseTransactionFlowProps {\n type: TransactionFlowType;\n network: NexusNetwork;\n nexusSDK: NexusClient | null;\n intent: RefObject;\n allowance: RefObject;\n bridgableBalance: TokenBalance[] | null;\n prefill?: TransactionFlowPrefill;\n onComplete?: (explorerUrl?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n maxAmount?: string | number;\n isSourceMenuOpen?: boolean;\n notifyHistoryRefresh?: () => void;\n executeTransaction: TransactionFlowExecutor;\n}\n\nexport interface UseTransactionFlowProps extends BaseTransactionFlowProps {\n connectedAddress?: Address;\n}\n\ntype State = {\n inputs: TransactionFlowInputs;\n status: TransactionStatus;\n};\n\ntype Action =\n | { type: \"setInputs\"; payload: Partial }\n | { type: \"resetInputs\" }\n | { type: \"setStatus\"; payload: TransactionStatus };\n\nexport function useTransactionFlow(props: UseTransactionFlowProps) {\n const {\n type,\n network,\n nexusSDK,\n intent,\n bridgableBalance,\n prefill,\n onComplete,\n onStart,\n onError,\n fetchBalance,\n allowance,\n maxAmount,\n isSourceMenuOpen = false,\n notifyHistoryRefresh,\n executeTransaction,\n } = props;\n\n const connectedAddress = props.connectedAddress;\n const operationName = type === \"bridge\" ? \"bridge\" : \"transfer\";\n const handleNexusError = useNexusError();\n const initialState: State = {\n inputs: buildInitialInputs({ type, network, connectedAddress, prefill }),\n status: \"idle\",\n };\n\n function reducer(state: State, action: Action): State {\n switch (action.type) {\n case \"setInputs\":\n return { ...state, inputs: { ...state.inputs, ...action.payload } };\n case \"resetInputs\":\n return {\n ...state,\n inputs: buildInitialInputs({\n type,\n network,\n connectedAddress,\n prefill,\n }),\n };\n case \"setStatus\":\n return { ...state, status: action.payload };\n default:\n return state;\n }\n }\n\n const [state, dispatch] = useReducer(reducer, initialState);\n const inputs = state.inputs;\n const setInputs = (\n next: TransactionFlowInputs | Partial,\n ) => {\n dispatch({\n type: \"setInputs\",\n payload: next as Partial,\n });\n };\n\n const loading = state.status === \"executing\";\n const [refreshing, setRefreshing] = useState(false);\n const [isDialogOpen, setIsDialogOpen] = useState(false);\n const [txError, setTxError] = useState(null);\n const [lastExplorerUrl, setLastExplorerUrl] = useState(\"\");\n const previousConnectedAddressRef = useRef
(\n connectedAddress,\n );\n const maxAmountRequestIdRef = useRef(0);\n const [selectedSourceChains, setSelectedSourceChains] = useState<\n number[] | null\n >(null);\n const [selectedSourcesMaxAmount, setSelectedSourcesMaxAmount] = useState<\n string | null\n >(null);\n const [appliedSourceSelectionKey, setAppliedSourceSelectionKey] =\n useState(\"ALL\");\n const {\n steps,\n onStepsList,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n const configuredMaxAmount = useMemo(\n () => normalizeMaxAmount(maxAmount),\n [maxAmount],\n );\n\n const areInputsValid = useMemo(() => {\n const hasToken = inputs?.token !== undefined && inputs?.token !== null;\n const hasChain = inputs?.chain !== undefined && inputs?.chain !== null;\n const hasAmount = Boolean(inputs?.amount) && Number(inputs?.amount) > 0;\n const hasValidRecipient =\n Boolean(inputs?.recipient) && isAddress(inputs.recipient as string);\n return hasToken && hasChain && hasAmount && hasValidRecipient;\n }, [inputs]);\n\n const filteredBridgableBalance = useMemo(() => {\n return bridgableBalance?.find((bal) =>\n inputs?.token === \"USDM\"\n ? bal?.symbol === \"USDC\"\n : bal?.symbol === inputs?.token,\n );\n }, [bridgableBalance, inputs?.token]);\n\n const availableSources = useMemo(() => {\n // v2: chainBalances replaces breakdown\n const chainBalances = filteredBridgableBalance?.chainBalances ?? [];\n const destinationChainId = inputs?.chain;\n const nonZero = chainBalances.filter((source: ChainBalance) => {\n if (Number.parseFloat(source.balance ?? \"0\") <= 0) return false;\n if (typeof destinationChainId === \"number\") {\n return source.chain.id !== destinationChainId;\n }\n return true;\n });\n const decimals = filteredBridgableBalance?.decimals;\n if (!nexusSDK || typeof decimals !== \"number\") {\n return nonZero.sort(\n (a, b) => Number.parseFloat(b.balance) - Number.parseFloat(a.balance),\n );\n }\n return nonZero.sort((a: ChainBalance, b: ChainBalance) => {\n try {\n const aRaw = parseUnits(a.balance ?? \"0\", decimals);\n const bRaw = parseUnits(b.balance ?? \"0\", decimals);\n if (aRaw === bRaw) return 0;\n return aRaw > bRaw ? -1 : 1;\n } catch {\n return Number.parseFloat(b.balance) - Number.parseFloat(a.balance);\n }\n });\n }, [\n inputs?.chain,\n filteredBridgableBalance?.chainBalances,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n ]);\n\n const allAvailableSourceChainIds = useMemo(\n () => availableSources.map((source: ChainBalance) => source.chain.id),\n [availableSources],\n );\n\n const effectiveSelectedSourceChains = useMemo(() => {\n if (selectedSourceChains && selectedSourceChains.length > 0) {\n const availableSet = new Set(allAvailableSourceChainIds);\n const filteredSelection = selectedSourceChains.filter((id: number) =>\n availableSet.has(id),\n );\n if (filteredSelection.length > 0) {\n return filteredSelection;\n }\n }\n return allAvailableSourceChainIds;\n }, [selectedSourceChains, allAvailableSourceChainIds]);\n\n const sourceChainsForSdk =\n effectiveSelectedSourceChains.length > 0\n ? effectiveSelectedSourceChains\n : undefined;\n\n const sourceSelectionKey = useMemo(() => {\n if (allAvailableSourceChainIds.length === 0) return \"NONE\";\n if (!selectedSourceChains || selectedSourceChains.length === 0) {\n return \"ALL\";\n }\n return [...effectiveSelectedSourceChains].sort((a: number, b: number) => a - b).join(\"|\");\n }, [\n allAvailableSourceChainIds.length,\n effectiveSelectedSourceChains,\n selectedSourceChains,\n ]);\n const hasPendingSourceSelectionChanges =\n sourceSelectionKey !== appliedSourceSelectionKey;\n const intentSourceSpendAmount = intent.current?.intent?.sourcesTotal;\n\n /**\n * v2: calculateMaxForBridge is removed. Use simulateBridge to get the max amount,\n * or fall back to summing available source balances directly.\n */\n const getMaxForCurrentSelection = useCallback(async () => {\n if (!nexusSDK || !inputs?.token || !inputs?.chain) return undefined;\n\n // Sum balances from selected sources as a direct proxy for max\n const decimals = filteredBridgableBalance?.decimals;\n if (typeof decimals !== \"number\") return \"0\";\n\n const selectedSet = new Set(\n sourceChainsForSdk ?? allAvailableSourceChainIds,\n );\n const totalRaw = availableSources.reduce((sum: bigint, source: ChainBalance) => {\n if (!selectedSet.has(source.chain.id)) return sum;\n try {\n return sum + parseUnits(source.balance ?? \"0\", decimals);\n } catch {\n return sum;\n }\n }, BigInt(0));\n\n const totalReadable = formatToBigIntReadable(totalRaw, decimals);\n if (!totalReadable) return \"0\";\n\n return clampAmountToMax({\n amount: totalReadable,\n maxAmount: configuredMaxAmount,\n nexusSDK,\n token: inputs.token,\n chainId: inputs.chain,\n });\n }, [\n configuredMaxAmount,\n inputs?.chain,\n inputs?.token,\n nexusSDK,\n sourceChainsForSdk,\n allAvailableSourceChainIds,\n availableSources,\n filteredBridgableBalance?.decimals,\n ]);\n\n const toggleSourceChain = useCallback(\n (chainId: number) => {\n setSelectedSourceChains((prev) => {\n if (allAvailableSourceChainIds.length === 0) return prev;\n const current =\n prev && prev.length > 0 ? prev : allAvailableSourceChainIds;\n const next = current.includes(chainId)\n ? current.filter((id: number) => id !== chainId)\n : [...current, chainId];\n if (next.length === 0) {\n return current;\n }\n const isAllSelected =\n next.length === allAvailableSourceChainIds.length &&\n allAvailableSourceChainIds.every((id) => next.includes(id));\n return isAllSelected ? null : next;\n });\n },\n [allAvailableSourceChainIds],\n );\n\n const sourceSelection = useMemo(() => {\n const amount =\n intentSourceSpendAmount?.trim() ?? inputs?.amount?.trim() ?? \"\";\n const decimals = getCoverageDecimals({\n type,\n token: inputs?.token,\n chainId: inputs?.chain,\n fallback: filteredBridgableBalance?.decimals,\n });\n const selectedChainSet = new Set(effectiveSelectedSourceChains);\n const selectedTotalRaw =\n !nexusSDK || typeof decimals !== \"number\"\n ? BigInt(0)\n : availableSources.reduce((sum: bigint, source: ChainBalance) => {\n if (!selectedChainSet.has(source.chain.id)) return sum;\n try {\n return sum + parseUnits(source.balance ?? \"0\", decimals);\n } catch {\n return sum;\n }\n }, BigInt(0));\n const selectedTotal =\n !nexusSDK || typeof decimals !== \"number\"\n ? \"0\"\n : formatAmountForDisplay(selectedTotalRaw, decimals, nexusSDK);\n const baseSelection = {\n selectedTotal,\n requiredTotal: amount || \"0\",\n requiredSafetyTotal: amount || \"0\",\n missingToProceed: \"0\",\n missingToSafety: \"0\",\n coverageState: \"healthy\" as SourceCoverageState,\n coverageToSafetyPercent: 100,\n isBelowRequired: false,\n isBelowSafetyBuffer: false,\n };\n\n if (!nexusSDK || !inputs?.token || !inputs?.chain || !amount) {\n return baseSelection;\n }\n\n try {\n // v2: convertTokenReadableAmountToBigInt(amount, tokenSymbol, chainId)\n const requiredRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n inputs.token,\n inputs.chain,\n );\n if (requiredRaw <= BigInt(0)) {\n return baseSelection;\n }\n\n const missingToProceedRaw =\n selectedTotalRaw >= requiredRaw\n ? BigInt(0)\n : requiredRaw - selectedTotalRaw;\n const missingToSafetyRaw = missingToProceedRaw;\n\n const coverageState: SourceCoverageState =\n selectedTotalRaw < requiredRaw ? \"error\" : \"healthy\";\n\n const coverageBasisPoints =\n requiredRaw === BigInt(0)\n ? 10_000\n : selectedTotalRaw >= requiredRaw\n ? 10_000\n : Number((selectedTotalRaw * BigInt(10_000)) / requiredRaw);\n\n return {\n selectedTotal,\n requiredTotal: amount,\n requiredSafetyTotal: amount,\n missingToProceed: formatAmountForDisplay(\n missingToProceedRaw,\n decimals,\n nexusSDK,\n ),\n missingToSafety: formatAmountForDisplay(\n missingToSafetyRaw,\n decimals,\n nexusSDK,\n ),\n coverageState,\n coverageToSafetyPercent: coverageBasisPoints / 100,\n isBelowRequired: coverageState === \"error\",\n isBelowSafetyBuffer: coverageState === \"error\",\n };\n } catch {\n return baseSelection;\n }\n }, [\n type,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n inputs?.chain,\n inputs?.amount,\n inputs?.token,\n intentSourceSpendAmount,\n availableSources,\n effectiveSelectedSourceChains,\n ]);\n\n const stopwatch = useStopwatch({ intervalMs: 100 });\n const setStatus = useCallback(\n (status: TransactionStatus) =>\n dispatch({ type: \"setStatus\", payload: status }),\n [],\n );\n\n const resetInputs = useCallback(() => {\n dispatch({ type: \"resetInputs\" });\n }, []);\n\n const {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n } = useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n });\n\n usePolling(\n Boolean(intent.current) &&\n !isDialogOpen &&\n !isSourceMenuOpen &&\n !hasPendingSourceSelectionChanges,\n async () => {\n await refreshIntent();\n },\n 15000,\n );\n\n const debouncedRefreshMaxForSelection = useDebouncedCallback(\n async (requestId: number) => {\n try {\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (requestId !== maxAmountRequestIdRef.current) return;\n setSelectedSourcesMaxAmount(maxForCurrentSelection ?? \"0\");\n } catch (error) {\n if (requestId !== maxAmountRequestIdRef.current) return;\n console.error(\"Unable to calculate max for selected sources:\", error);\n setSelectedSourcesMaxAmount(\"0\");\n }\n },\n MAX_AMOUNT_DEBOUNCE_MS,\n );\n\n useEffect(() => {\n debouncedRefreshMaxForSelection.cancel();\n if (!nexusSDK || !inputs?.token || !inputs?.chain) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(null);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(\"0\");\n return;\n }\n const requestId = ++maxAmountRequestIdRef.current;\n debouncedRefreshMaxForSelection(requestId);\n }, [\n allAvailableSourceChainIds.length,\n configuredMaxAmount,\n debouncedRefreshMaxForSelection,\n inputs?.recipient,\n sourceSelectionKey,\n inputs?.chain,\n inputs?.token,\n nexusSDK,\n ]);\n\n useEffect(() => {\n if (type !== \"bridge\" || !connectedAddress) return;\n const previousConnectedAddress = previousConnectedAddressRef.current;\n if (!previousConnectedAddress) {\n previousConnectedAddressRef.current = connectedAddress;\n return;\n }\n if (connectedAddress === previousConnectedAddress) return;\n previousConnectedAddressRef.current = connectedAddress;\n if (prefill?.recipient) return;\n if (!inputs?.recipient || inputs.recipient === previousConnectedAddress) {\n dispatch({ type: \"setInputs\", payload: { recipient: connectedAddress } });\n }\n }, [type, connectedAddress, inputs?.recipient, prefill?.recipient]);\n\n useEffect(() => {\n invalidatePendingExecution();\n }, [inputs, invalidatePendingExecution]);\n\n useEffect(() => {\n setSelectedSourceChains(null);\n }, [inputs?.token]);\n\n // Safety-net: stop the stopwatch as soon as status reaches a terminal state.\n // This ensures the timer freezes even if the onEvent closure's stopwatch.stop()\n // didn't fire (e.g. stale closure reference or SDK promise resolved oddly).\n useEffect(() => {\n if (state.status === \"success\" || state.status === \"error\") {\n stopwatch.stop();\n }\n }, [state.status, stopwatch]);\n\n useEffect(() => {\n if (isDialogOpen) return;\n stopwatch.stop();\n stopwatch.reset();\n if (state.status === \"success\" || state.status === \"error\") {\n resetSteps();\n setLastExplorerUrl(\"\");\n setStatus(\"idle\");\n }\n }, [isDialogOpen, resetSteps, setStatus, state.status, stopwatch]);\n\n useEffect(() => {\n if (txError) {\n setTxError(null);\n }\n }, [inputs, txError]);\n\n\n return {\n inputs,\n setInputs,\n timer: stopwatch.seconds,\n setIsDialogOpen,\n setTxError,\n loading,\n refreshing,\n isDialogOpen,\n txError,\n handleTransaction,\n reset,\n filteredBridgableBalance,\n startTransaction,\n commitAmount,\n lastExplorerUrl,\n steps,\n status: state.status,\n availableSources,\n selectedSourceChains: effectiveSelectedSourceChains,\n toggleSourceChain,\n isSourceSelectionInsufficient: sourceSelection.isBelowRequired,\n isSourceSelectionBelowSafetyBuffer: sourceSelection.isBelowSafetyBuffer,\n isSourceSelectionReadyForAccept:\n sourceSelection.coverageState === \"healthy\",\n sourceCoverageState: sourceSelection.coverageState,\n sourceCoveragePercent: sourceSelection.coverageToSafetyPercent,\n missingToProceed: sourceSelection.missingToProceed,\n missingToSafety: sourceSelection.missingToSafety,\n selectedTotal: sourceSelection.selectedTotal,\n requiredTotal: sourceSelection.requiredTotal,\n requiredSafetyTotal: sourceSelection.requiredSafetyTotal,\n maxAvailableAmount: selectedSourcesMaxAmount ?? undefined,\n isInputsValid: areInputsValid,\n };\n}\n\n/** Helper: format a bigint rawAmount with decimals into a readable decimal string. */\nfunction formatToBigIntReadable(raw: bigint, decimals: number): string {\n if (raw <= BigInt(0)) return \"0\";\n const divisor = BigInt(10 ** decimals);\n const whole = raw / divisor;\n const fraction = raw % divisor;\n if (fraction === BigInt(0)) return whole.toString();\n const fractionStr = fraction.toString().padStart(decimals, \"0\").replace(/0+$/, \"\");\n return `${whole}.${fractionStr}`;\n}\n", "type": "registry:component", "target": "components/common/hooks/useTransactionFlow.ts" }, @@ -178,7 +178,7 @@ }, { "path": "registry/nexus-elements/common/tx/steps.ts", - "content": "import type { SwapStepType } from \"@avail-project/nexus-core\";\nimport type { GenericStep } from \"./types\";\nimport { getStepKey } from \"./types\";\n\n/**\n * Predefined expected steps for swaps to seed UI before events arrive.\n * Kept here to avoid duplication across exact-in and exact-out hooks.\n */\nexport const SWAP_EXPECTED_STEPS: SwapStepType[] = [\n { type: \"SWAP_START\", typeID: \"SWAP_START\" } as SwapStepType,\n { type: \"DETERMINING_SWAP\", typeID: \"DETERMINING_SWAP\" } as SwapStepType,\n {\n type: \"CREATE_PERMIT_FOR_SOURCE_SWAP\",\n typeID:\n \"CREATE_PERMIT_FOR_SOURCE_SWAP\" as unknown as SwapStepType[\"typeID\"],\n } as SwapStepType,\n {\n type: \"SOURCE_SWAP_BATCH_TX\",\n typeID: \"SOURCE_SWAP_BATCH_TX\",\n } as SwapStepType,\n {\n type: \"SOURCE_SWAP_HASH\",\n typeID: \"SOURCE_SWAP_HASH\" as unknown as SwapStepType[\"typeID\"],\n } as SwapStepType,\n { type: \"RFF_ID\", typeID: \"RFF_ID\" } as SwapStepType,\n {\n type: \"DESTINATION_SWAP_BATCH_TX\",\n typeID: \"DESTINATION_SWAP_BATCH_TX\",\n } as SwapStepType,\n {\n type: \"DESTINATION_SWAP_HASH\",\n typeID: \"DESTINATION_SWAP_HASH\" as unknown as SwapStepType[\"typeID\"],\n } as SwapStepType,\n { type: \"SWAP_COMPLETE\", typeID: \"SWAP_COMPLETE\" } as SwapStepType,\n];\n\nexport function seedSteps(expected: T[]): Array> {\n return expected.map((st, index) => ({\n id: index,\n completed: false,\n step: st,\n }));\n}\n\nexport function computeAllCompleted(steps: Array>): boolean {\n return steps.length > 0 && steps.every((s) => s.completed);\n}\n\n/**\n * Replace the current list of steps with a new list, preserving completion\n * for any steps that were already marked completed (matched by key).\n */\nexport function mergeStepsList(\n prev: Array>,\n list: T[]\n): Array> {\n const completedKeys = new Set();\n for (const prevStep of prev) {\n if (prevStep.completed) {\n completedKeys.add(getStepKey(prevStep.step));\n }\n }\n const next: Array> = [];\n for (let index = 0; index < list.length; index++) {\n const step = list[index];\n const key = getStepKey(step);\n next.push({\n id: index,\n completed: completedKeys.has(key),\n step,\n });\n }\n return next;\n}\n\n/**\n * Mark a step complete in-place; if the step doesn't yet exist, append it.\n */\nexport function mergeStepComplete(\n prev: Array>,\n step: T\n): Array> {\n const key = getStepKey(step);\n const updated: Array> = [];\n let found = false;\n for (const s of prev) {\n if (getStepKey(s.step) === key) {\n updated.push({ ...s, completed: true, step: { ...s.step, ...step } });\n found = true;\n } else {\n updated.push(s);\n }\n }\n if (!found) {\n updated.push({\n id: updated.length,\n completed: true,\n step,\n });\n }\n return updated;\n}\n", + "content": "// v2: SwapStepType is no longer exported from the SDK — use a local step shape\n// that matches v2 SwapPlanStep discriminator pattern\nexport type SwapStepType = {\n typeID?: string;\n type?: string;\n [key: string]: unknown;\n};\n\nimport type { GenericStep } from \"./types\";\nimport { getStepKey } from \"./types\";\n\n/**\n * Predefined expected steps for swaps to seed UI before events arrive.\n * Uses v2 stepType names that match SwapPlanProgressEvent.stepType discriminators.\n */\nexport const SWAP_EXPECTED_STEPS: SwapStepType[] = [\n { type: \"source_swap\", typeID: \"source_swap\" },\n { type: \"eoa_to_ephemeral_transfer\", typeID: \"eoa_to_ephemeral_transfer\" },\n { type: \"bridge_deposit\", typeID: \"bridge_deposit\" },\n { type: \"bridge_intent_submission\", typeID: \"bridge_intent_submission\" },\n { type: \"bridge_fill\", typeID: \"bridge_fill\" },\n { type: \"destination_swap\", typeID: \"destination_swap\" },\n];\n\nexport function seedSteps(expected: T[]): Array> {\n return expected.map((st, index) => ({\n id: index,\n completed: false,\n step: st,\n }));\n}\n\nexport function computeAllCompleted(steps: Array>): boolean {\n return steps.length > 0 && steps.every((s) => s.completed);\n}\n\n/**\n * Replace the current list of steps with a new list, preserving completion\n * for any steps that were already marked completed (matched by key).\n */\nexport function mergeStepsList(\n prev: Array>,\n list: T[]\n): Array> {\n const completedKeys = new Set();\n for (const prevStep of prev) {\n if (prevStep.completed) {\n completedKeys.add(getStepKey(prevStep.step));\n }\n }\n const next: Array> = [];\n for (let index = 0; index < list.length; index++) {\n const step = list[index];\n const key = getStepKey(step);\n next.push({\n id: index,\n completed: completedKeys.has(key),\n step,\n });\n }\n return next;\n}\n\n/**\n * Mark a step complete in-place; if the step doesn't yet exist, append it.\n */\nexport function mergeStepComplete(\n prev: Array>,\n step: T\n): Array> {\n const key = getStepKey(step);\n const updated: Array> = [];\n let found = false;\n for (const s of prev) {\n if (getStepKey(s.step) === key) {\n updated.push({ ...s, completed: true, step: { ...s.step, ...step } });\n found = true;\n } else {\n updated.push(s);\n }\n }\n if (!found) {\n updated.push({\n id: updated.length,\n completed: true,\n step,\n });\n }\n return updated;\n}\n", "type": "registry:component", "target": "components/common/tx/steps.ts" }, @@ -196,25 +196,25 @@ }, { "path": "registry/nexus-elements/common/types/transaction-flow.ts", - "content": "import {\n type NexusSDK,\n type SUPPORTED_CHAINS_IDS,\n type SUPPORTED_TOKENS,\n} from \"@avail-project/nexus-core\";\nimport { type Address } from \"viem\";\n\nexport type TransactionFlowType = \"bridge\" | \"transfer\";\n\nexport interface TransactionFlowInputs {\n chain: SUPPORTED_CHAINS_IDS;\n token: SUPPORTED_TOKENS;\n amount?: string;\n recipient?: `0x${string}`;\n}\n\nexport interface TransactionFlowPrefill {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n}\n\ntype BridgeOptions = NonNullable[1]>;\n\nexport type TransactionFlowEvent =\n NonNullable extends (event: infer E) => void\n ? E\n : never;\n\nexport type TransactionFlowOnEvent = NonNullable;\n\nexport interface TransactionFlowExecuteParams {\n token: SUPPORTED_TOKENS;\n amount: bigint;\n toChainId: SUPPORTED_CHAINS_IDS;\n recipient: `0x${string}`;\n sourceChains?: number[];\n onEvent: TransactionFlowOnEvent;\n}\n\nexport type TransactionFlowExecutor = (\n params: TransactionFlowExecuteParams,\n) => Promise<{ explorerUrl: string } | null>;\n\nexport type SourceCoverageState = \"healthy\" | \"warning\" | \"error\";\n\nexport interface SourceSelectionValidation {\n coverageState: SourceCoverageState;\n isBelowRequired: boolean;\n missingToProceed: string;\n missingToSafety: string;\n}\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport { type Address } from \"viem\";\n\ntype NexusClient = ReturnType;\n\n// v2 uses string token symbols (toTokenSymbol) with number chain IDs\nexport type TransactionFlowType = \"bridge\" | \"transfer\";\n\nexport interface TransactionFlowInputs {\n chain: number;\n token: string;\n amount?: string;\n recipient?: `0x${string}`;\n}\n\nexport interface TransactionFlowPrefill {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n}\n\n// v2 bridge onEvent uses typed discriminated union, not NEXUS_EVENTS\nexport type TransactionFlowEvent =\n | { type: \"status\"; status: string }\n | { type: \"plan_preview\"; plan: { steps: unknown[] } }\n | { type: \"plan_confirmed\"; plan: { steps: unknown[] } }\n | { type: \"plan_progress\"; stepType: string; state: string; step: unknown };\n\nexport type TransactionFlowOnEvent = (event: TransactionFlowEvent) => void;\n\nexport interface TransactionFlowExecuteParams {\n token: string;\n amount: bigint;\n toChainId: number;\n recipient: `0x${string}`;\n sources?: number[];\n onEvent: TransactionFlowOnEvent;\n}\n\nexport type TransactionFlowExecutor = (\n params: TransactionFlowExecuteParams,\n) => Promise<{ explorerUrl: string } | null>;\n\nexport type SourceCoverageState = \"healthy\" | \"warning\" | \"error\";\n\nexport interface SourceSelectionValidation {\n coverageState: SourceCoverageState;\n isBelowRequired: boolean;\n missingToProceed: string;\n missingToSafety: string;\n}\n", "type": "registry:component", "target": "components/common/types/transaction-flow.ts" }, { "path": "registry/nexus-elements/common/utils/constant.ts", - "content": "import { SUPPORTED_CHAINS } from \"@avail-project/nexus-core\";\nimport { formatUnits, parseUnits } from \"viem\";\n\nexport const SHORT_CHAIN_NAME: Record = {\n [SUPPORTED_CHAINS.ETHEREUM]: \"Ethereum\",\n [SUPPORTED_CHAINS.BASE]: \"Base\",\n [SUPPORTED_CHAINS.ARBITRUM]: \"Arbitrum\",\n [SUPPORTED_CHAINS.OPTIMISM]: \"Optimism\",\n [SUPPORTED_CHAINS.POLYGON]: \"Polygon\",\n [SUPPORTED_CHAINS.AVALANCHE]: \"Avalanche\",\n [SUPPORTED_CHAINS.SCROLL]: \"Scroll\",\n [SUPPORTED_CHAINS.MEGAETH]: \"MegaETH\",\n [SUPPORTED_CHAINS.KAIA]: \"Kaia\",\n [SUPPORTED_CHAINS.BNB]: \"BNB\",\n [SUPPORTED_CHAINS.MONAD]: \"Monad\",\n [SUPPORTED_CHAINS.HYPEREVM]: \"HyperEVM\",\n [SUPPORTED_CHAINS.CITREA]: \"Citrea\",\n // [SUPPORTED_CHAINS.TRON]: \"Tron\",\n [SUPPORTED_CHAINS.SEPOLIA]: \"Sepolia\",\n [SUPPORTED_CHAINS.BASE_SEPOLIA]: \"Base Sepolia\",\n [SUPPORTED_CHAINS.ARBITRUM_SEPOLIA]: \"Arbitrum Sepolia\",\n [SUPPORTED_CHAINS.OPTIMISM_SEPOLIA]: \"Optimism Sepolia\",\n [SUPPORTED_CHAINS.POLYGON_AMOY]: \"Polygon Amoy\",\n [SUPPORTED_CHAINS.MONAD_TESTNET]: \"Monad Testnet\",\n // [SUPPORTED_CHAINS.TRON_SHASTA]: \"Tron Shasta\",\n} as const;\n\nconst DEFAULT_SAFETY_MARGIN = 0.01; // 1%\n\n/**\n * Compute an amount string for fraction buttons (25%, 50%, 75%, 100%).\n *\n * @param balanceStr - user's balance as a human decimal string (e.g. \"12.345\") OR as base-unit integer string if `balanceIsBaseUnits` true\n * @param fraction - fraction e.g. 0.25, 0.5, 0.75, 1\n * @param decimals - token decimals (6 for USDC/USDT, 18 for ETH)\n * @param safetyMargin - 0.01 for 1% default\n * @param balanceIsBaseUnits - if true, balanceStr is already base units integer string (wei / smallest unit)\n * @returns decimal string clipped to token decimals (rounded down)\n */\nexport function computeAmountFromFraction(\n balanceStr: string,\n fraction: number,\n decimals: number,\n safetyMargin = DEFAULT_SAFETY_MARGIN,\n balanceIsBaseUnits = false,\n): string {\n if (!balanceStr) return \"0\";\n\n // parse balance into base units (BigInt)\n const balanceUnits: bigint = balanceIsBaseUnits\n ? BigInt(balanceStr)\n : parseUnits(balanceStr, decimals);\n\n if (balanceUnits === BigInt(0)) return \"0\";\n\n // Use an integer precision multiplier to avoid FP issues\n const PREC = 1_000_000; // 1e6 precision for fraction & safety margin\n const safetyMul = BigInt(Math.max(0, Math.floor((1 - safetyMargin) * PREC))); // (1 - safetyMargin) * PREC\n const fractionMul = BigInt(Math.max(0, Math.floor(fraction * PREC))); // fraction * PREC\n\n // Apply safety margin: floor(balance * (1 - safetyMargin))\n const maxAfterSafety = (balanceUnits * safetyMul) / BigInt(PREC);\n\n // Apply fraction and floor: floor(maxAfterSafety * fraction)\n let desiredUnits = (maxAfterSafety * fractionMul) / BigInt(PREC);\n\n // Extra clamp just in case\n if (desiredUnits > balanceUnits) desiredUnits = balanceUnits;\n if (desiredUnits < BigInt(0)) desiredUnits = BigInt(0);\n\n // format back to human readable decimal string with token decimals (formatUnits truncates/keeps decimals)\n // formatUnits will produce exactly decimals digits if fractional part exists; we'll strip trailing zeros.\n const raw = formatUnits(desiredUnits, decimals);\n // strip trailing zeros and possible trailing dot\n return raw\n .replace(/(\\.\\d*?[1-9])0+$/u, \"$1\")\n .replace(/\\.0+$/u, \"\")\n .replace(/^\\.$/u, \"0\");\n}\n\nexport const usdFormatter = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n});\n\nconst usdFormatterPrecise = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 3,\n maximumFractionDigits: 3,\n});\n\n/**\n * Formats USD values for UI.\n * Values between 0 and 0.001 are shown as \"< $0.001\".\n * Values between 0.001 and 0.01 are shown with 3 decimals.\n */\nexport function formatUsdForDisplay(value: number): string {\n if (!Number.isFinite(value)) return usdFormatter.format(0);\n const absValue = Math.abs(value);\n\n if (absValue === 0) return usdFormatter.format(0);\n if (absValue < 0.001) return \"< $0.001\";\n if (absValue < 0.01) {\n return usdFormatterPrecise.format(value);\n }\n\n return usdFormatter.format(value);\n}\n", + "content": "import { formatUnits, parseUnits } from \"viem\";\n\n// v2: SUPPORTED_CHAINS removed — using literal EVM chain IDs\nexport const SHORT_CHAIN_NAME: Record = {\n 1: \"Ethereum\",\n 8453: \"Base\",\n 42161: \"Arbitrum\",\n 10: \"Optimism\",\n 137: \"Polygon\",\n 43114: \"Avalanche\",\n 534352: \"Scroll\",\n 6342: \"MegaETH\",\n 8217: \"Kaia\",\n 56: \"BNB\",\n 10143: \"Monad\",\n 999: \"HyperEVM\",\n 5115: \"Citrea\",\n 11155111: \"Sepolia\",\n 84532: \"Base Sepolia\",\n 421614: \"Arbitrum Sepolia\",\n 11155420: \"Optimism Sepolia\",\n 80002: \"Polygon Amoy\",\n} as const;\n\nconst DEFAULT_SAFETY_MARGIN = 0.01; // 1%\n\n/**\n * Compute an amount string for fraction buttons (25%, 50%, 75%, 100%).\n *\n * @param balanceStr - user's balance as a human decimal string (e.g. \"12.345\") OR as base-unit integer string if `balanceIsBaseUnits` true\n * @param fraction - fraction e.g. 0.25, 0.5, 0.75, 1\n * @param decimals - token decimals (6 for USDC/USDT, 18 for ETH)\n * @param safetyMargin - 0.01 for 1% default\n * @param balanceIsBaseUnits - if true, balanceStr is already base units integer string (wei / smallest unit)\n * @returns decimal string clipped to token decimals (rounded down)\n */\nexport function computeAmountFromFraction(\n balanceStr: string,\n fraction: number,\n decimals: number,\n safetyMargin = DEFAULT_SAFETY_MARGIN,\n balanceIsBaseUnits = false,\n): string {\n if (!balanceStr) return \"0\";\n\n // parse balance into base units (BigInt)\n const balanceUnits: bigint = balanceIsBaseUnits\n ? BigInt(balanceStr)\n : parseUnits(balanceStr, decimals);\n\n if (balanceUnits === BigInt(0)) return \"0\";\n\n // Use an integer precision multiplier to avoid FP issues\n const PREC = 1_000_000; // 1e6 precision for fraction & safety margin\n const safetyMul = BigInt(Math.max(0, Math.floor((1 - safetyMargin) * PREC))); // (1 - safetyMargin) * PREC\n const fractionMul = BigInt(Math.max(0, Math.floor(fraction * PREC))); // fraction * PREC\n\n // Apply safety margin: floor(balance * (1 - safetyMargin))\n const maxAfterSafety = (balanceUnits * safetyMul) / BigInt(PREC);\n\n // Apply fraction and floor: floor(maxAfterSafety * fraction)\n let desiredUnits = (maxAfterSafety * fractionMul) / BigInt(PREC);\n\n // Extra clamp just in case\n if (desiredUnits > balanceUnits) desiredUnits = balanceUnits;\n if (desiredUnits < BigInt(0)) desiredUnits = BigInt(0);\n\n // format back to human readable decimal string with token decimals (formatUnits truncates/keeps decimals)\n // formatUnits will produce exactly decimals digits if fractional part exists; we'll strip trailing zeros.\n const raw = formatUnits(desiredUnits, decimals);\n // strip trailing zeros and possible trailing dot\n return raw\n .replace(/(\\.\\d*?[1-9])0+$/u, \"$1\")\n .replace(/\\.0+$/u, \"\")\n .replace(/^\\.$/u, \"0\");\n}\n\nexport const usdFormatter = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n});\n\nconst usdFormatterPrecise = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 3,\n maximumFractionDigits: 3,\n});\n\n/**\n * Formats USD values for UI.\n * Values between 0 and 0.001 are shown as \"< $0.001\".\n * Values between 0.001 and 0.01 are shown with 3 decimals.\n */\nexport function formatUsdForDisplay(value: number): string {\n if (!Number.isFinite(value)) return usdFormatter.format(0);\n const absValue = Math.abs(value);\n\n if (absValue === 0) return usdFormatter.format(0);\n if (absValue < 0.001) return \"< $0.001\";\n if (absValue < 0.01) {\n return usdFormatterPrecise.format(value);\n }\n\n return usdFormatter.format(value);\n}\n", "type": "registry:component", "target": "components/common/utils/constant.ts" }, { "path": "registry/nexus-elements/common/utils/token-pricing.ts", - "content": "import type { SupportedChainsAndTokensResult } from \"@avail-project/nexus-core\";\n\nconst COINBASE_SPOT_API_BASE = \"https://api.coinbase.com/v2/prices\";\nconst COINBASE_EXCHANGE_RATES_API_BASE =\n \"https://api.coinbase.com/v2/exchange-rates\";\n\nexport const DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS = 4_000;\nexport const USD_PEGGED_FALLBACK_RATE = 1;\nexport const DEFAULT_USD_PEGGED_TOKEN_SYMBOLS = [\n \"USDT\",\n \"USDC\",\n \"USDS\",\n \"DAI\",\n \"USDM\",\n \"FDUSD\",\n \"BUSD\",\n \"TUSD\",\n \"PYUSD\",\n \"GUSD\",\n \"LUSD\",\n \"USDE\",\n \"USDP\",\n] as const;\n\ntype CoinbaseSpotPriceResponse = {\n data?: {\n amount?: string | number;\n };\n};\n\ntype CoinbaseExchangeRatesResponse = {\n data?: {\n rates?: Record;\n };\n};\n\ntype SupportedTokenMetadata = {\n symbol?: string;\n equivalentCurrency?: string;\n};\n\ntype SupportedChainMetadata = {\n tokens?: SupportedTokenMetadata[];\n};\n\nexport function normalizeTokenSymbol(tokenSymbol: string): string {\n return tokenSymbol.trim().toUpperCase();\n}\n\nexport function toFinitePositiveNumber(value: unknown): number | null {\n const parsed = Number.parseFloat(String(value));\n if (!Number.isFinite(parsed) || parsed <= 0) {\n return null;\n }\n return parsed;\n}\n\nexport function getCoinbaseSymbolCandidates(tokenSymbol: string): string[] {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return [];\n\n const baseSymbol = normalized.split(/[._-]/)[0] ?? normalized;\n const wrappedBase =\n baseSymbol.startsWith(\"W\") && baseSymbol.length > 3\n ? baseSymbol.slice(1)\n : null;\n\n return Array.from(\n new Set(\n [normalized, baseSymbol, wrappedBase].filter(\n (symbol): symbol is string => Boolean(symbol),\n ),\n ),\n );\n}\n\nexport function buildUsdPeggedSymbolSet(\n supportedChains: SupportedChainsAndTokensResult | null,\n baseSymbols: Iterable = DEFAULT_USD_PEGGED_TOKEN_SYMBOLS,\n): Set {\n const symbolSet = new Set(baseSymbols);\n\n for (const chain of (supportedChains ?? []) as SupportedChainMetadata[]) {\n for (const token of chain.tokens ?? []) {\n const symbol = normalizeTokenSymbol(token.symbol ?? \"\");\n const equivalent = normalizeTokenSymbol(token.equivalentCurrency ?? \"\");\n if (!symbol) continue;\n\n if (equivalent && symbolSet.has(equivalent)) {\n symbolSet.add(symbol);\n }\n }\n }\n\n return symbolSet;\n}\n\nasync function fetchJsonWithTimeout(\n url: string,\n requestTimeoutMs: number,\n): Promise {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);\n try {\n const response = await fetch(url, {\n signal: controller.signal,\n });\n if (!response.ok) return null;\n return (await response.json()) as T;\n } catch {\n return null;\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nexport async function fetchCoinbaseUsdRate(\n tokenSymbol: string,\n requestTimeoutMs = DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS,\n): Promise {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return null;\n\n for (const candidate of getCoinbaseSymbolCandidates(normalized)) {\n const spotBody = await fetchJsonWithTimeout(\n `${COINBASE_SPOT_API_BASE}/${encodeURIComponent(candidate)}-USD/spot`,\n requestTimeoutMs,\n );\n const spotAmount = toFinitePositiveNumber(spotBody?.data?.amount);\n if (spotAmount) return spotAmount;\n\n const exchangeRatesBody =\n await fetchJsonWithTimeout(\n `${COINBASE_EXCHANGE_RATES_API_BASE}?currency=${encodeURIComponent(candidate)}`,\n requestTimeoutMs,\n );\n const exchangeRatesAmount = toFinitePositiveNumber(\n exchangeRatesBody?.data?.rates?.USD,\n );\n if (exchangeRatesAmount) return exchangeRatesAmount;\n }\n\n return null;\n}\n", + "content": "// v2: getSupportedChains() return type is inferred directly; define a structural type\ntype SupportedChainsAndTokensResult = readonly {\n tokens?: { symbol?: string; equivalentCurrency?: string }[];\n [key: string]: unknown;\n}[];\n\nconst COINBASE_SPOT_API_BASE = \"https://api.coinbase.com/v2/prices\";\nconst COINBASE_EXCHANGE_RATES_API_BASE =\n \"https://api.coinbase.com/v2/exchange-rates\";\n\nexport const DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS = 4_000;\nexport const USD_PEGGED_FALLBACK_RATE = 1;\nexport const DEFAULT_USD_PEGGED_TOKEN_SYMBOLS = [\n \"USDT\",\n \"USDC\",\n \"USDS\",\n \"DAI\",\n \"USDM\",\n \"FDUSD\",\n \"BUSD\",\n \"TUSD\",\n \"PYUSD\",\n \"GUSD\",\n \"LUSD\",\n \"USDE\",\n \"USDP\",\n] as const;\n\ntype CoinbaseSpotPriceResponse = {\n data?: {\n amount?: string | number;\n };\n};\n\ntype CoinbaseExchangeRatesResponse = {\n data?: {\n rates?: Record;\n };\n};\n\ntype SupportedTokenMetadata = {\n symbol?: string;\n equivalentCurrency?: string;\n};\n\ntype SupportedChainMetadata = {\n tokens?: SupportedTokenMetadata[];\n};\n\nexport function normalizeTokenSymbol(tokenSymbol: string): string {\n return tokenSymbol.trim().toUpperCase();\n}\n\nexport function toFinitePositiveNumber(value: unknown): number | null {\n const parsed = Number.parseFloat(String(value));\n if (!Number.isFinite(parsed) || parsed <= 0) {\n return null;\n }\n return parsed;\n}\n\nexport function getCoinbaseSymbolCandidates(tokenSymbol: string): string[] {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return [];\n\n const baseSymbol = normalized.split(/[._-]/)[0] ?? normalized;\n const wrappedBase =\n baseSymbol.startsWith(\"W\") && baseSymbol.length > 3\n ? baseSymbol.slice(1)\n : null;\n\n return Array.from(\n new Set(\n [normalized, baseSymbol, wrappedBase].filter(\n (symbol): symbol is string => Boolean(symbol),\n ),\n ),\n );\n}\n\nexport function buildUsdPeggedSymbolSet(\n supportedChains: SupportedChainsAndTokensResult | null,\n baseSymbols: Iterable = DEFAULT_USD_PEGGED_TOKEN_SYMBOLS,\n): Set {\n const symbolSet = new Set(baseSymbols);\n\n for (const chain of (supportedChains ?? []) as SupportedChainMetadata[]) {\n for (const token of chain.tokens ?? []) {\n const symbol = normalizeTokenSymbol(token.symbol ?? \"\");\n const equivalent = normalizeTokenSymbol(token.equivalentCurrency ?? \"\");\n if (!symbol) continue;\n\n if (equivalent && symbolSet.has(equivalent)) {\n symbolSet.add(symbol);\n }\n }\n }\n\n return symbolSet;\n}\n\nasync function fetchJsonWithTimeout(\n url: string,\n requestTimeoutMs: number,\n): Promise {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);\n try {\n const response = await fetch(url, {\n signal: controller.signal,\n });\n if (!response.ok) return null;\n return (await response.json()) as T;\n } catch {\n return null;\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nexport async function fetchCoinbaseUsdRate(\n tokenSymbol: string,\n requestTimeoutMs = DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS,\n): Promise {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return null;\n\n for (const candidate of getCoinbaseSymbolCandidates(normalized)) {\n const spotBody = await fetchJsonWithTimeout(\n `${COINBASE_SPOT_API_BASE}/${encodeURIComponent(candidate)}-USD/spot`,\n requestTimeoutMs,\n );\n const spotAmount = toFinitePositiveNumber(spotBody?.data?.amount);\n if (spotAmount) return spotAmount;\n\n const exchangeRatesBody =\n await fetchJsonWithTimeout(\n `${COINBASE_EXCHANGE_RATES_API_BASE}?currency=${encodeURIComponent(candidate)}`,\n requestTimeoutMs,\n );\n const exchangeRatesAmount = toFinitePositiveNumber(\n exchangeRatesBody?.data?.rates?.USD,\n );\n if (exchangeRatesAmount) return exchangeRatesAmount;\n }\n\n return null;\n}\n", "type": "registry:component", "target": "components/common/utils/token-pricing.ts" }, { "path": "registry/nexus-elements/common/utils/transaction-flow.ts", - "content": "import {\n formatUnits,\n type NexusNetwork,\n NexusSDK,\n SUPPORTED_CHAINS,\n type SUPPORTED_CHAINS_IDS,\n type SUPPORTED_TOKENS,\n} from \"@avail-project/nexus-core\";\nimport { type Address } from \"viem\";\n\nconst MAX_AMOUNT_REGEX = /^\\d*\\.?\\d+$/;\n\nexport const MAX_AMOUNT_DEBOUNCE_MS = 300;\n\nexport const normalizeMaxAmount = (\n maxAmount?: string | number,\n): string | undefined => {\n if (maxAmount === undefined || maxAmount === null) return undefined;\n const value = String(maxAmount).trim();\n if (!value || value === \".\" || !MAX_AMOUNT_REGEX.test(value)) {\n return undefined;\n }\n const parsed = Number.parseFloat(value);\n if (!Number.isFinite(parsed) || parsed <= 0) return undefined;\n return value;\n};\n\nexport const clampAmountToMax = ({\n amount,\n maxAmount,\n nexusSDK,\n token,\n chainId,\n}: {\n amount: string;\n maxAmount?: string;\n nexusSDK: NexusSDK;\n token: SUPPORTED_TOKENS;\n chainId: SUPPORTED_CHAINS_IDS;\n}): string => {\n if (!maxAmount) return amount;\n try {\n const amountRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n token,\n chainId,\n );\n const maxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxAmount,\n token,\n chainId,\n );\n return amountRaw > maxRaw ? maxAmount : amount;\n } catch {\n return amount;\n }\n};\n\nexport const formatAmountForDisplay = (\n amount: bigint,\n decimals: number | undefined,\n nexusSDK: NexusSDK,\n): string => {\n if (typeof decimals !== \"number\") return amount.toString();\n const formatted = formatUnits(amount, decimals);\n if (!formatted.includes(\".\")) return formatted;\n const [whole, fraction] = formatted.split(\".\");\n const trimmedFraction = fraction.slice(0, 6).replace(/0+$/, \"\");\n if (!trimmedFraction && whole === \"0\" && amount > BigInt(0)) {\n return \"0.000001\";\n }\n return trimmedFraction ? `${whole}.${trimmedFraction}` : whole;\n};\n\nexport const buildInitialInputs = ({\n type,\n network,\n connectedAddress,\n prefill,\n}: {\n type: \"bridge\" | \"transfer\";\n network: NexusNetwork;\n connectedAddress?: Address;\n prefill?: {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n };\n}) => {\n return {\n chain:\n (prefill?.chainId as SUPPORTED_CHAINS_IDS) ??\n (network === \"testnet\"\n ? SUPPORTED_CHAINS.SEPOLIA\n : SUPPORTED_CHAINS.ETHEREUM),\n token: (prefill?.token as SUPPORTED_TOKENS) ?? \"USDC\",\n amount: prefill?.amount ?? undefined,\n recipient:\n (prefill?.recipient as `0x${string}`) ??\n (type === \"bridge\" ? connectedAddress : undefined),\n };\n};\n\nexport const getCoverageDecimals = ({\n type,\n token,\n chainId,\n fallback,\n}: {\n type: \"bridge\" | \"transfer\";\n token?: SUPPORTED_TOKENS;\n chainId?: SUPPORTED_CHAINS_IDS;\n fallback: number | undefined;\n}) => {\n if (token === \"USDM\") return 18;\n if (\n type === \"bridge\" &&\n token === \"USDC\" &&\n chainId === SUPPORTED_CHAINS.BNB\n ) {\n return 18;\n }\n return fallback;\n};\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type { NexusNetwork } from \"@avail-project/nexus-sdk-v2\";\nimport { formatUnits, type Address } from \"viem\";\n\ntype NexusClient = ReturnType;\n\nconst MAX_AMOUNT_REGEX = /^\\d*\\.?\\d+$/;\n\n// v2 chain IDs for defaults\nconst SEPOLIA_CHAIN_ID = 11155111;\nconst ETHEREUM_CHAIN_ID = 1;\n// v2: BNB chain ID for edge-case decimal override\nconst BNB_CHAIN_ID = 56;\n\nexport const MAX_AMOUNT_DEBOUNCE_MS = 300;\n\nexport const normalizeMaxAmount = (\n maxAmount?: string | number,\n): string | undefined => {\n if (maxAmount === undefined || maxAmount === null) return undefined;\n const value = String(maxAmount).trim();\n if (!value || value === \".\" || !MAX_AMOUNT_REGEX.test(value)) {\n return undefined;\n }\n const parsed = Number.parseFloat(value);\n if (!Number.isFinite(parsed) || parsed <= 0) return undefined;\n return value;\n};\n\nexport const clampAmountToMax = ({\n amount,\n maxAmount,\n nexusSDK,\n token,\n chainId,\n}: {\n amount: string;\n maxAmount?: string;\n nexusSDK: NexusClient;\n token: string;\n chainId: number;\n}): string => {\n if (!maxAmount) return amount;\n try {\n // v2: convertTokenReadableAmountToBigInt(amount, tokenSymbol, chainId)\n const amountRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n token,\n chainId,\n );\n const maxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxAmount,\n token,\n chainId,\n );\n return amountRaw > maxRaw ? maxAmount : amount;\n } catch {\n return amount;\n }\n};\n\nexport const formatAmountForDisplay = (\n amount: bigint,\n decimals: number | undefined,\n // nexusSDK kept for API compatibility but formatUnits is now imported directly\n _nexusSDK: NexusClient,\n): string => {\n if (typeof decimals !== \"number\") return amount.toString();\n const formatted = formatUnits(amount, decimals);\n if (!formatted.includes(\".\")) return formatted;\n const [whole, fraction] = formatted.split(\".\");\n const trimmedFraction = fraction.slice(0, 6).replace(/0+$/, \"\");\n if (!trimmedFraction && whole === \"0\" && amount > BigInt(0)) {\n return \"0.000001\";\n }\n return trimmedFraction ? `${whole}.${trimmedFraction}` : whole;\n};\n\nexport const buildInitialInputs = ({\n type,\n network,\n connectedAddress,\n prefill,\n}: {\n type: \"bridge\" | \"transfer\";\n network: NexusNetwork;\n connectedAddress?: Address;\n prefill?: {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n };\n}) => {\n return {\n // v2 uses plain number chain IDs and string token symbols\n chain:\n prefill?.chainId ??\n (network === \"testnet\" ? SEPOLIA_CHAIN_ID : ETHEREUM_CHAIN_ID),\n token: prefill?.token ?? \"USDC\",\n amount: prefill?.amount ?? undefined,\n recipient:\n (prefill?.recipient as `0x${string}`) ??\n (type === \"bridge\" ? connectedAddress : undefined),\n };\n};\n\nexport const getCoverageDecimals = ({\n type,\n token,\n chainId,\n fallback,\n}: {\n type: \"bridge\" | \"transfer\";\n token?: string;\n chainId?: number;\n fallback: number | undefined;\n}) => {\n if (token === \"USDM\") return 18;\n if (type === \"bridge\" && token === \"USDC\" && chainId === BNB_CHAIN_ID) {\n return 18;\n }\n return fallback;\n};\n", "type": "registry:component", "target": "components/common/utils/transaction-flow.ts" } diff --git a/public/r/deposit.json b/public/r/deposit.json index 6e25b3c..0814612 100644 --- a/public/r/deposit.json +++ b/public/r/deposit.json @@ -5,7 +5,7 @@ "title": "Deposit", "description": "A simple component built with Nexus to enable deposits", "dependencies": [ - "@avail-project/nexus-core@1.2.0", + "@avail-project/nexus-sdk-v2", "clsx", "lucide-react", "tailwind-merge", @@ -27,13 +27,13 @@ "files": [ { "path": "registry/nexus-elements/deposit/components/amount-card.tsx", - "content": "\"use client\";\n\nimport { useCallback, useRef, useEffect, useState, useMemo } from \"react\";\nimport type { MaxSwapInput } from \"@avail-project/nexus-core\";\nimport { TokenIcon } from \"./token-icon\";\nimport { ErrorBanner } from \"./error-banner\";\nimport { PercentageSelector } from \"./percentage-selector\";\nimport { parseCurrencyInput } from \"../utils\";\nimport { UpDownArrows } from \"./icons\";\nimport { usdFormatter } from \"../../common\";\nimport { type DestinationConfig } from \"../types\";\nimport { Skeleton } from \"../../ui/skeleton\";\nimport {\n BALANCE_SAFETY_MARGIN,\n CHARACTER_ANIMATION_DURATION_MS,\n SHINE_ANIMATION_DURATION_MS,\n MAX_INPUT_WIDTH_PX,\n} from \"../constants/widget\";\nimport { TOKEN_IMAGES } from \"../constants/assets\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\n\n// Hoisted RegExp to avoid recreation on every render (js-hoist-regexp)\nconst NUMERIC_INPUT_REGEX = /^\\d*\\.?\\d*$/;\n\ninterface AmountCardProps {\n amount?: string;\n onAmountChange?: (amount: string) => void;\n selectedTokenAmount?: number;\n maxSwapInput?: MaxSwapInput;\n onErrorStateChange?: (hasError: boolean) => void;\n totalSelectedBalance: number;\n totalBalance: {\n balance: number;\n usdBalance: number;\n };\n destinationConfig: DestinationConfig;\n}\n\nfunction AmountCard({\n amount: externalAmount,\n onAmountChange,\n selectedTokenAmount = 0,\n maxSwapInput,\n onErrorStateChange,\n totalSelectedBalance,\n totalBalance,\n destinationConfig,\n}: AmountCardProps) {\n const [internalAmount, setInternalAmount] = useState(\"\");\n const amount = externalAmount ?? internalAmount;\n const setAmount = onAmountChange ?? setInternalAmount;\n\n const [inputWidth, setInputWidth] = useState(0);\n const [isShining, setIsShining] = useState(false);\n const [isCalculatingMax, setIsCalculatingMax] = useState(false);\n const [animatingIndices, setAnimatingIndices] = useState>(\n new Set(),\n );\n const prevAmountRef = useRef(\"\");\n const prevLengthRef = useRef(0);\n const measureRef = useRef(null);\n const inputRef = useRef(null);\n\n const displayValue = amount || \"\";\n const measureText = displayValue || \"0\";\n\n // Split display value into characters for animation\n const displayChars = displayValue.split(\"\");\n const { nexusSDK, getFiatValue } = useNexus();\n\n // Track which characters should animate (newly added ones)\n useEffect(() => {\n const currentLength = displayValue.length;\n const prevLength = prevLengthRef.current;\n\n // Only animate when characters are added (not removed)\n if (currentLength > prevLength) {\n const newIndices = new Set();\n for (let i = prevLength; i < currentLength; i++) {\n newIndices.add(i);\n }\n setAnimatingIndices(newIndices);\n\n // Clear animation after it completes\n const timer = setTimeout(() => {\n setAnimatingIndices(new Set());\n }, CHARACTER_ANIMATION_DURATION_MS);\n\n prevLengthRef.current = currentLength;\n return () => clearTimeout(timer);\n }\n\n prevLengthRef.current = currentLength;\n }, [displayValue]);\n\n // Calculate numeric amount for USD equivalent\n const numericAmount = useMemo(() => {\n if (!amount) return 0;\n const parsed = parseFloat(amount.replace(/,/g, \"\"));\n return isNaN(parsed) ? 0 : parsed;\n }, [amount]);\n\n // Check if amount exceeds wallet balance\n const exceedsBalance = useMemo(() => {\n if (!amount) return false;\n const numericAmount = parseFloat(amount.replace(/,/g, \"\"));\n return !isNaN(numericAmount) && numericAmount > totalBalance?.usdBalance;\n }, [amount, totalBalance?.usdBalance]);\n\n // Check if amount exceeds selected token amount but is within wallet balance\n const exceedsSelectedTokens = useMemo(() => {\n if (!amount || selectedTokenAmount === 0) return false;\n const numericAmount = parseFloat(amount.replace(/,/g, \"\"));\n return (\n !isNaN(numericAmount) &&\n numericAmount > selectedTokenAmount &&\n numericAmount <= totalBalance?.usdBalance\n );\n }, [amount, selectedTokenAmount, totalBalance?.usdBalance]);\n\n useEffect(() => {\n if (measureRef.current) {\n setInputWidth(measureRef.current.offsetWidth);\n }\n }, [measureText]);\n\n // Trigger shine effect when USD amount changes\n useEffect(() => {\n if (amount && amount !== prevAmountRef.current && numericAmount > 0) {\n setIsShining(true);\n const timer = setTimeout(() => {\n setIsShining(false);\n }, SHINE_ANIMATION_DURATION_MS);\n prevAmountRef.current = amount;\n return () => clearTimeout(timer);\n }\n prevAmountRef.current = amount;\n }, [amount, numericAmount]);\n\n // Notify parent of error state changes\n useEffect(() => {\n const hasError = exceedsBalance || exceedsSelectedTokens;\n onErrorStateChange?.(hasError);\n }, [exceedsBalance, exceedsSelectedTokens, onErrorStateChange]);\n\n const handleInputChange = useCallback(\n (e: React.ChangeEvent) => {\n const rawValue = parseCurrencyInput(e.target.value);\n\n // Validate numeric input (allow unlimited decimals)\n if (rawValue === \"\" || NUMERIC_INPUT_REGEX.test(rawValue)) {\n setAmount(rawValue);\n }\n },\n [setAmount],\n );\n\n const handlePercentageClick = useCallback(\n async (percentage: number) => {\n const safeBalance = selectedTokenAmount * BALANCE_SAFETY_MARGIN;\n const setFallbackAmount = () => {\n const calculatedAmount = safeBalance * percentage;\n const newAmount = usdFormatter\n .format(calculatedAmount)\n .replace(\"$\", \"\");\n setAmount(newAmount);\n };\n\n if (percentage === 1 && nexusSDK && maxSwapInput) {\n setIsCalculatingMax(true);\n try {\n const maxAmountResult = await nexusSDK.calculateMaxForSwap(\n maxSwapInput,\n );\n const maxTokenAmount = Number.parseFloat(maxAmountResult.maxAmount);\n\n if (Number.isFinite(maxTokenAmount) && maxTokenAmount > 0) {\n const maxAmountUsd = getFiatValue(\n maxTokenAmount,\n maxAmountResult.symbol || destinationConfig.tokenSymbol,\n );\n\n if (Number.isFinite(maxAmountUsd) && maxAmountUsd > 0) {\n setAmount(usdFormatter.format(maxAmountUsd).replace(\"$\", \"\"));\n return;\n }\n }\n } catch (error) {\n console.error(\"Failed to calculate max swap amount\", error);\n } finally {\n setIsCalculatingMax(false);\n }\n }\n\n setFallbackAmount();\n },\n [\n setAmount,\n selectedTokenAmount,\n nexusSDK,\n maxSwapInput,\n getFiatValue,\n destinationConfig.tokenSymbol,\n ],\n );\n\n const handleDoubleClick = useCallback(() => {\n // Select all text on double click\n if (inputRef.current) {\n inputRef.current.select();\n }\n }, []);\n\n // Handle keyboard shortcuts like Ctrl+A\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (e.ctrlKey && e.key === \"a\") {\n e.preventDefault();\n if (inputRef.current) {\n inputRef.current.select();\n }\n }\n },\n [],\n );\n\n return (\n
\n {/* Hidden span to measure text width */}\n \n {measureText}\n \n\n {/* Amount Input Section */}\n 0 ? \"-mt-0.5\" : \"mt-1.5\"\n }`}\n >\n \n
\n {/* Animated digits layer (behind input) */}\n {isCalculatingMax ? (\n \n ) : (\n \n {displayChars.map((char, index) => (\n \n {char}\n \n ))}\n {/* Placeholder when empty */}\n {displayValue.length === 0 && (\n \n 0\n \n )}\n
\n )}\n\n {/* Real input overlaid with transparent text (for cursor positioning) */}\n 0\n ? Math.min(inputWidth + 4, MAX_INPUT_WIDTH_PX)\n : undefined,\n maxWidth: \"calc(100vw - 100px)\",\n }}\n className=\"absolute inset-0 font-display text-[32px] font-medium tracking-[0.8px] tabular-nums bg-transparent border-none outline-none min-w-[22px] text-transparent caret-card-foreground placeholder:text-transparent\"\n disabled={isCalculatingMax}\n />\n
\n \n\n {/* USD Equivalent - animated height reveal */}\n 0\n ? \"grid-rows-[1fr] opacity-100\"\n : \"grid-rows-[0fr] opacity-0 mt-2\"\n }`}\n >\n
\n
\n {isCalculatingMax ? (\n \n ) : (\n <>\n \n ~ {usdFormatter.format(numericAmount)}\n \n \n \n )}\n
\n
\n \n\n {/* Percentage Selector */}\n
0 ? \"-mt-px\" : \"\"}>\n \n
\n\n {/* Balance Display */}\n
\n Balance: {usdFormatter.format(totalSelectedBalance)}\n
\n\n {/* Error Banner */}\n {exceedsBalance && (\n
\n \n
\n )}\n {exceedsSelectedTokens && (\n
\n \n
\n )}\n \n );\n}\n\nexport default AmountCard;\n", + "content": "\"use client\";\n\nimport { useCallback, useRef, useEffect, useState, useMemo } from \"react\";\nimport type { SwapMaxParams as MaxSwapInput } from \"@avail-project/nexus-sdk-v2\";\nimport { TokenIcon } from \"./token-icon\";\nimport { ErrorBanner } from \"./error-banner\";\nimport { PercentageSelector } from \"./percentage-selector\";\nimport { parseCurrencyInput } from \"../utils\";\nimport { UpDownArrows } from \"./icons\";\nimport { usdFormatter } from \"../../common\";\nimport { type DestinationConfig } from \"../types\";\nimport { Skeleton } from \"../../ui/skeleton\";\nimport {\n BALANCE_SAFETY_MARGIN,\n CHARACTER_ANIMATION_DURATION_MS,\n SHINE_ANIMATION_DURATION_MS,\n MAX_INPUT_WIDTH_PX,\n} from \"../constants/widget\";\nimport { TOKEN_IMAGES } from \"../constants/assets\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\n\n// Hoisted RegExp to avoid recreation on every render (js-hoist-regexp)\nconst NUMERIC_INPUT_REGEX = /^\\d*\\.?\\d*$/;\n\ninterface AmountCardProps {\n amount?: string;\n onAmountChange?: (amount: string) => void;\n selectedTokenAmount?: number;\n maxSwapInput?: MaxSwapInput;\n onErrorStateChange?: (hasError: boolean) => void;\n totalSelectedBalance: number;\n totalBalance: {\n balance: number;\n usdBalance: number;\n };\n destinationConfig: DestinationConfig;\n}\n\nfunction AmountCard({\n amount: externalAmount,\n onAmountChange,\n selectedTokenAmount = 0,\n maxSwapInput,\n onErrorStateChange,\n totalSelectedBalance,\n totalBalance,\n destinationConfig,\n}: AmountCardProps) {\n const [internalAmount, setInternalAmount] = useState(\"\");\n const amount = externalAmount ?? internalAmount;\n const setAmount = onAmountChange ?? setInternalAmount;\n\n const [inputWidth, setInputWidth] = useState(0);\n const [isShining, setIsShining] = useState(false);\n const [isCalculatingMax, setIsCalculatingMax] = useState(false);\n const [animatingIndices, setAnimatingIndices] = useState>(\n new Set(),\n );\n const prevAmountRef = useRef(\"\");\n const prevLengthRef = useRef(0);\n const measureRef = useRef(null);\n const inputRef = useRef(null);\n\n const displayValue = amount || \"\";\n const measureText = displayValue || \"0\";\n\n // Split display value into characters for animation\n const displayChars = displayValue.split(\"\");\n const { nexusSDK, getFiatValue } = useNexus();\n\n // Track which characters should animate (newly added ones)\n useEffect(() => {\n const currentLength = displayValue.length;\n const prevLength = prevLengthRef.current;\n\n // Only animate when characters are added (not removed)\n if (currentLength > prevLength) {\n const newIndices = new Set();\n for (let i = prevLength; i < currentLength; i++) {\n newIndices.add(i);\n }\n setAnimatingIndices(newIndices);\n\n // Clear animation after it completes\n const timer = setTimeout(() => {\n setAnimatingIndices(new Set());\n }, CHARACTER_ANIMATION_DURATION_MS);\n\n prevLengthRef.current = currentLength;\n return () => clearTimeout(timer);\n }\n\n prevLengthRef.current = currentLength;\n }, [displayValue]);\n\n // Calculate numeric amount for USD equivalent\n const numericAmount = useMemo(() => {\n if (!amount) return 0;\n const parsed = parseFloat(amount.replace(/,/g, \"\"));\n return isNaN(parsed) ? 0 : parsed;\n }, [amount]);\n\n // Check if amount exceeds wallet balance\n const exceedsBalance = useMemo(() => {\n if (!amount) return false;\n const numericAmount = parseFloat(amount.replace(/,/g, \"\"));\n return !isNaN(numericAmount) && numericAmount > totalBalance?.usdBalance;\n }, [amount, totalBalance?.usdBalance]);\n\n // Check if amount exceeds selected token amount but is within wallet balance\n const exceedsSelectedTokens = useMemo(() => {\n if (!amount || selectedTokenAmount === 0) return false;\n const numericAmount = parseFloat(amount.replace(/,/g, \"\"));\n return (\n !isNaN(numericAmount) &&\n numericAmount > selectedTokenAmount &&\n numericAmount <= totalBalance?.usdBalance\n );\n }, [amount, selectedTokenAmount, totalBalance?.usdBalance]);\n\n useEffect(() => {\n if (measureRef.current) {\n setInputWidth(measureRef.current.offsetWidth);\n }\n }, [measureText]);\n\n // Trigger shine effect when USD amount changes\n useEffect(() => {\n if (amount && amount !== prevAmountRef.current && numericAmount > 0) {\n setIsShining(true);\n const timer = setTimeout(() => {\n setIsShining(false);\n }, SHINE_ANIMATION_DURATION_MS);\n prevAmountRef.current = amount;\n return () => clearTimeout(timer);\n }\n prevAmountRef.current = amount;\n }, [amount, numericAmount]);\n\n // Notify parent of error state changes\n useEffect(() => {\n const hasError = exceedsBalance || exceedsSelectedTokens;\n onErrorStateChange?.(hasError);\n }, [exceedsBalance, exceedsSelectedTokens, onErrorStateChange]);\n\n const handleInputChange = useCallback(\n (e: React.ChangeEvent) => {\n const rawValue = parseCurrencyInput(e.target.value);\n\n // Validate numeric input (allow unlimited decimals)\n if (rawValue === \"\" || NUMERIC_INPUT_REGEX.test(rawValue)) {\n setAmount(rawValue);\n }\n },\n [setAmount],\n );\n\n const handlePercentageClick = useCallback(\n async (percentage: number) => {\n const safeBalance = selectedTokenAmount * BALANCE_SAFETY_MARGIN;\n const setFallbackAmount = () => {\n const calculatedAmount = safeBalance * percentage;\n const newAmount = usdFormatter\n .format(calculatedAmount)\n .replace(\"$\", \"\");\n setAmount(newAmount);\n };\n\n if (percentage === 1 && nexusSDK && maxSwapInput) {\n setIsCalculatingMax(true);\n try {\n const maxAmountResult = await nexusSDK.calculateMaxForSwap(\n maxSwapInput,\n );\n const maxTokenAmount = Number.parseFloat(maxAmountResult.maxAmount);\n\n if (Number.isFinite(maxTokenAmount) && maxTokenAmount > 0) {\n const maxAmountUsd = getFiatValue(\n maxTokenAmount,\n maxAmountResult.symbol || destinationConfig.tokenSymbol,\n );\n\n if (Number.isFinite(maxAmountUsd) && maxAmountUsd > 0) {\n setAmount(usdFormatter.format(maxAmountUsd).replace(\"$\", \"\"));\n return;\n }\n }\n } catch (error) {\n console.error(\"Failed to calculate max swap amount\", error);\n } finally {\n setIsCalculatingMax(false);\n }\n }\n\n setFallbackAmount();\n },\n [\n setAmount,\n selectedTokenAmount,\n nexusSDK,\n maxSwapInput,\n getFiatValue,\n destinationConfig.tokenSymbol,\n ],\n );\n\n const handleDoubleClick = useCallback(() => {\n // Select all text on double click\n if (inputRef.current) {\n inputRef.current.select();\n }\n }, []);\n\n // Handle keyboard shortcuts like Ctrl+A\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (e.ctrlKey && e.key === \"a\") {\n e.preventDefault();\n if (inputRef.current) {\n inputRef.current.select();\n }\n }\n },\n [],\n );\n\n return (\n
\n {/* Hidden span to measure text width */}\n \n {measureText}\n \n\n {/* Amount Input Section */}\n 0 ? \"-mt-0.5\" : \"mt-1.5\"\n }`}\n >\n \n
\n {/* Animated digits layer (behind input) */}\n {isCalculatingMax ? (\n \n ) : (\n \n {displayChars.map((char, index) => (\n \n {char}\n \n ))}\n {/* Placeholder when empty */}\n {displayValue.length === 0 && (\n \n 0\n \n )}\n
\n )}\n\n {/* Real input overlaid with transparent text (for cursor positioning) */}\n 0\n ? Math.min(inputWidth + 4, MAX_INPUT_WIDTH_PX)\n : undefined,\n maxWidth: \"calc(100vw - 100px)\",\n }}\n className=\"absolute inset-0 font-display text-[32px] font-medium tracking-[0.8px] tabular-nums bg-transparent border-none outline-none min-w-[22px] text-transparent caret-card-foreground placeholder:text-transparent\"\n disabled={isCalculatingMax}\n />\n
\n \n\n {/* USD Equivalent - animated height reveal */}\n 0\n ? \"grid-rows-[1fr] opacity-100\"\n : \"grid-rows-[0fr] opacity-0 mt-2\"\n }`}\n >\n
\n
\n {isCalculatingMax ? (\n \n ) : (\n <>\n \n ~ {usdFormatter.format(numericAmount)}\n \n \n \n )}\n
\n
\n \n\n {/* Percentage Selector */}\n
0 ? \"-mt-px\" : \"\"}>\n \n
\n\n {/* Balance Display */}\n
\n Balance: {usdFormatter.format(totalSelectedBalance)}\n
\n\n {/* Error Banner */}\n {exceedsBalance && (\n
\n \n
\n )}\n {exceedsSelectedTokens && (\n
\n \n
\n )}\n \n );\n}\n\nexport default AmountCard;\n", "type": "registry:component", "target": "components/deposit/components/amount-card.tsx" }, { "path": "registry/nexus-elements/deposit/components/amount-container.tsx", - "content": "\"use client\";\n\nimport { useCallback, useMemo, useState } from \"react\";\nimport type { MaxSwapInput } from \"@avail-project/nexus-core\";\nimport WidgetHeader from \"./widget-header\";\nimport type { DepositWidgetContextValue } from \"../types\";\nimport AmountCard from \"./amount-card\";\nimport PayUsing from \"./pay-using\";\nimport { ErrorBanner } from \"./error-banner\";\nimport { EmptyBalanceState } from \"./empty-balance-state\";\nimport { Button } from \"../../ui/button\";\nimport { CardContent } from \"../../ui/card\";\nimport { Skeleton } from \"../../ui/skeleton\";\nimport { buildDepositSourcePoolIds, parseSourceId } from \"../utils\";\n\ninterface AmountContainerProps {\n widget: DepositWidgetContextValue;\n heading?: string;\n onClose?: () => void;\n}\n\nconst AmountContainer = ({\n widget,\n heading,\n onClose,\n}: AmountContainerProps) => {\n const [hasAmountError, setHasAmountError] = useState(false);\n const isSwapBalanceLoaded = widget.swapBalance !== null;\n const hasAnySwapAsset = (widget.swapBalance?.length ?? 0) > 0;\n const hasPositiveSwapBalance = useMemo(\n () =>\n (widget.swapBalance ?? []).some((asset) =>\n (asset.breakdown ?? []).some((chain) => {\n const amount = Number.parseFloat(chain.balance ?? \"0\");\n return Number.isFinite(amount) && amount > 0;\n }),\n ),\n [widget.swapBalance],\n );\n const shouldShowEmptyState = isSwapBalanceLoaded && !hasPositiveSwapBalance;\n const amountScreenBalance = useMemo(() => {\n if (widget.assetSelection.filter === \"all\") {\n return widget.totalBalance?.usdBalance ?? 0;\n }\n\n return widget.totalSelectedBalance;\n }, [\n widget.assetSelection.filter,\n widget.totalBalance?.usdBalance,\n widget.totalSelectedBalance,\n ]);\n const maxSwapInput = useMemo(() => {\n const sourcePoolIds = buildDepositSourcePoolIds({\n swapBalance: widget.swapBalance,\n filter: widget.assetSelection.filter,\n selectedSourceIds: widget.assetSelection.selectedChainIds,\n isManualSelection: widget.isManualSelection,\n });\n\n const fromSources = sourcePoolIds\n .map((sourceId) => parseSourceId(sourceId))\n .filter((source): source is NonNullable =>\n Boolean(source),\n );\n\n return {\n toChainId: widget.destination.chainId,\n toTokenAddress: widget.destination.tokenAddress,\n fromSources: fromSources.length > 0 ? fromSources : undefined,\n };\n }, [\n widget.swapBalance,\n widget.assetSelection.filter,\n widget.assetSelection.selectedChainIds,\n widget.isManualSelection,\n widget.destination.chainId,\n widget.destination.tokenAddress,\n ]);\n\n const handleAmountChange = useCallback(\n (amount: string) => {\n widget.setInputs({ amount });\n },\n [widget],\n );\n\n const handleErrorStateChange = useCallback((hasError: boolean) => {\n setHasAmountError(hasError);\n }, []);\n\n return (\n <>\n \n \n
\n {!isSwapBalanceLoaded ? (\n \n ) : shouldShowEmptyState ? (\n {\n void widget.reset();\n }}\n />\n ) : (\n \n )}\n\n {widget.txError && widget.status === \"error\" && (\n \n )}\n {!shouldShowEmptyState && (\n
\n widget.goToStep(\"asset-selection\")}\n selectedChainIds={widget.assetSelection.selectedChainIds}\n filter={widget.assetSelection.filter}\n isManualSelection={widget.isManualSelection}\n amount={widget.inputs.amount}\n swapBalance={widget.swapBalance}\n destination={widget.destination}\n />\n widget.goToStep(\"confirmation\")}\n disabled={\n widget.isProcessing ||\n hasAmountError ||\n !widget.inputs.amount ||\n widget.inputs.amount === \"0\"\n }\n >\n Continue\n \n
\n )}\n
\n
\n \n );\n};\n\nexport default AmountContainer;\n", + "content": "\"use client\";\n\nimport { useCallback, useMemo, useState } from \"react\";\nimport type { SwapMaxParams as MaxSwapInput } from \"@avail-project/nexus-sdk-v2\";\nimport WidgetHeader from \"./widget-header\";\nimport type { DepositWidgetContextValue } from \"../types\";\nimport AmountCard from \"./amount-card\";\nimport PayUsing from \"./pay-using\";\nimport { ErrorBanner } from \"./error-banner\";\nimport { EmptyBalanceState } from \"./empty-balance-state\";\nimport { Button } from \"../../ui/button\";\nimport { CardContent } from \"../../ui/card\";\nimport { Skeleton } from \"../../ui/skeleton\";\nimport { buildDepositSourcePoolIds, parseSourceId } from \"../utils\";\n\ninterface AmountContainerProps {\n widget: DepositWidgetContextValue;\n heading?: string;\n onClose?: () => void;\n}\n\nconst AmountContainer = ({\n widget,\n heading,\n onClose,\n}: AmountContainerProps) => {\n const [hasAmountError, setHasAmountError] = useState(false);\n const isSwapBalanceLoaded = widget.swapBalance !== null;\n const hasAnySwapAsset = (widget.swapBalance?.length ?? 0) > 0;\n const hasPositiveSwapBalance = useMemo(\n () =>\n (widget.swapBalance ?? []).some((asset) =>\n (asset.chainBalances ?? []).some((chain) => {\n const amount = Number.parseFloat(chain.balance ?? \"0\");\n return Number.isFinite(amount) && amount > 0;\n }),\n ),\n [widget.swapBalance],\n );\n const shouldShowEmptyState = isSwapBalanceLoaded && !hasPositiveSwapBalance;\n const amountScreenBalance = useMemo(() => {\n if (widget.assetSelection.filter === \"all\") {\n return widget.totalBalance?.usdBalance ?? 0;\n }\n\n return widget.totalSelectedBalance;\n }, [\n widget.assetSelection.filter,\n widget.totalBalance?.usdBalance,\n widget.totalSelectedBalance,\n ]);\n const maxSwapInput = useMemo(() => {\n const sourcePoolIds = buildDepositSourcePoolIds({\n swapBalance: widget.swapBalance,\n filter: widget.assetSelection.filter,\n selectedSourceIds: widget.assetSelection.selectedChainIds,\n isManualSelection: widget.isManualSelection,\n });\n\n const fromSources = sourcePoolIds\n .map((sourceId) => parseSourceId(sourceId))\n .filter((source): source is NonNullable =>\n Boolean(source),\n );\n\n return {\n toChainId: widget.destination.chainId,\n toTokenAddress: widget.destination.tokenAddress,\n fromSources: fromSources.length > 0 ? fromSources : undefined,\n };\n }, [\n widget.swapBalance,\n widget.assetSelection.filter,\n widget.assetSelection.selectedChainIds,\n widget.isManualSelection,\n widget.destination.chainId,\n widget.destination.tokenAddress,\n ]);\n\n const handleAmountChange = useCallback(\n (amount: string) => {\n widget.setInputs({ amount });\n },\n [widget],\n );\n\n const handleErrorStateChange = useCallback((hasError: boolean) => {\n setHasAmountError(hasError);\n }, []);\n\n return (\n <>\n \n \n
\n {!isSwapBalanceLoaded ? (\n \n ) : shouldShowEmptyState ? (\n {\n void widget.reset();\n }}\n />\n ) : (\n \n )}\n\n {widget.txError && widget.status === \"error\" && (\n \n )}\n {!shouldShowEmptyState && (\n
\n widget.goToStep(\"asset-selection\")}\n selectedChainIds={widget.assetSelection.selectedChainIds}\n filter={widget.assetSelection.filter}\n isManualSelection={widget.isManualSelection}\n amount={widget.inputs.amount}\n swapBalance={widget.swapBalance}\n destination={widget.destination}\n />\n widget.goToStep(\"confirmation\")}\n disabled={\n widget.isProcessing ||\n hasAmountError ||\n !widget.inputs.amount ||\n widget.inputs.amount === \"0\"\n }\n >\n Continue\n \n
\n )}\n
\n
\n \n );\n};\n\nexport default AmountContainer;\n", "type": "registry:component", "target": "components/deposit/components/amount-container.tsx" }, @@ -51,7 +51,7 @@ }, { "path": "registry/nexus-elements/deposit/components/asset-selection-container.tsx", - "content": "\"use client\";\n\nimport {\n useMemo,\n useCallback,\n useState,\n useEffect,\n useRef,\n startTransition,\n useDeferredValue,\n} from \"react\";\nimport { ChevronDownIcon } from \"./icons\";\nimport WidgetHeader from \"./widget-header\";\nimport type { DepositWidgetContextValue, Token, ChainItem } from \"../types\";\nimport { Tabs, TabsList, TabsTrigger } from \"../../ui/tabs\";\nimport { CardContent } from \"../../ui/card\";\nimport { Button } from \"../../ui/button\";\nimport TokenRow from \"./token-row\";\nimport { formatTokenBalance, type UserAsset } from \"@avail-project/nexus-core\";\nimport { usdFormatter } from \"../../common\";\nimport { X } from \"lucide-react\";\nimport {\n SCROLL_THRESHOLD_PX,\n PROGRESS_BAR_ANIMATION_DELAY_MS,\n PROGRESS_BAR_EXIT_DURATION_MS,\n MIN_SELECTABLE_SOURCE_BALANCE_USD,\n} from \"../constants/widget\";\nimport {\n buildSortedFromSources,\n checkIfMatchesPreset,\n isNative,\n isStablecoin,\n} from \"../utils\";\n\ninterface AssetSelectionContainerProps {\n widget: DepositWidgetContextValue;\n heading?: string;\n onClose?: () => void;\n}\n\ninterface TokenWithMeta extends Token {\n totalUsdValue: number;\n priorityRank: number;\n group: \"selectable\" | \"below-minimum\";\n}\n\ntype ChainItemWithTokenMeta = ChainItem & {\n symbol: string;\n decimals: number;\n tokenLogo: string;\n};\n\ntype AssetBreakdownWithOptionalIcon = UserAsset[\"breakdown\"][number] & {\n icon?: string;\n};\n\nfunction parseNonNegativeNumber(value: unknown): number {\n const parsed = Number.parseFloat(String(value));\n if (!Number.isFinite(parsed) || parsed < 0) return 0;\n return parsed;\n}\n\nfunction getBreakdownTokenMeta(\n breakdown: UserAsset[\"breakdown\"][number],\n asset: UserAsset,\n) {\n const breakdownIcon = (breakdown as AssetBreakdownWithOptionalIcon).icon;\n return {\n symbol: breakdown.symbol,\n decimals: breakdown.decimals ?? asset.decimals,\n logo: breakdownIcon || \"\",\n };\n}\n\nfunction transformSwapBalanceToTokens(\n swapBalance: UserAsset[] | null,\n destination: Pick<\n DepositWidgetContextValue[\"destination\"],\n \"chainId\" | \"tokenAddress\" | \"tokenSymbol\"\n >,\n): {\n selectableTokens: TokenWithMeta[];\n belowMinimumTokens: TokenWithMeta[];\n} {\n if (!swapBalance) {\n return {\n selectableTokens: [],\n belowMinimumTokens: [],\n };\n }\n\n const allSourceIds = new Set();\n swapBalance.forEach((asset) => {\n asset.breakdown?.forEach((breakdown) => {\n if (!breakdown.chain?.id || !breakdown.contractAddress) return;\n allSourceIds.add(`${breakdown.contractAddress}-${breakdown.chain.id}`);\n });\n });\n\n const orderedSources = buildSortedFromSources({\n sourceIds: allSourceIds,\n swapBalance,\n destination,\n });\n\n const sourceOrderIndex = new Map();\n orderedSources.forEach((source, index) => {\n sourceOrderIndex.set(\n `${source.tokenAddress.toLowerCase()}-${source.chainId}`,\n index,\n );\n });\n\n const getSourceOrder = (tokenAddress: string, chainId: number) =>\n sourceOrderIndex.get(`${tokenAddress.toLowerCase()}-${chainId}`) ??\n Number.MAX_SAFE_INTEGER;\n\n const buildTokenEntry = (\n tokenMeta: { symbol: string; decimals: number; logo: string },\n chains: ChainItemWithTokenMeta[],\n group: \"selectable\" | \"below-minimum\",\n ): TokenWithMeta | null => {\n if (chains.length === 0) return null;\n\n const totalUsdValue = chains.reduce((sum, c) => sum + c.usdValue, 0);\n const totalAmount = chains.reduce((sum, c) => sum + c.amount, 0);\n const category = isStablecoin(tokenMeta.symbol)\n ? \"stablecoin\"\n : isNative(tokenMeta.symbol)\n ? \"native\"\n : \"memecoin\";\n\n return {\n id: `${tokenMeta.symbol}-${chains[0].tokenAddress}-${group}`,\n symbol: tokenMeta.symbol,\n chainsLabel:\n chains.length > 1\n ? `${chains.length} Chain${chains.length !== 1 ? \"s\" : \"\"}`\n : chains[0].name,\n usdValue: usdFormatter.format(totalUsdValue),\n amount: formatTokenBalance(totalAmount, {\n decimals: tokenMeta.decimals,\n symbol: tokenMeta.symbol,\n }),\n decimals: tokenMeta.decimals,\n logo: tokenMeta.logo,\n category,\n priorityRank: chains.length\n ? getSourceOrder(chains[0].tokenAddress, chains[0].chainId)\n : Number.MAX_SAFE_INTEGER,\n totalUsdValue,\n group,\n chains,\n };\n };\n\n const selectableTokens: TokenWithMeta[] = [];\n const belowMinimumTokens: TokenWithMeta[] = [];\n\n for (const asset of swapBalance) {\n if (!asset.breakdown?.length) continue;\n const chainsBySymbol = new Map();\n\n asset.breakdown\n .filter((b) => b.chain && b.balance)\n .forEach((b) => {\n const balanceNum = parseFloat(b.balance);\n if (!Number.isFinite(balanceNum) || balanceNum <= 0) return;\n\n const usdValue = parseNonNegativeNumber(b.balanceInFiat);\n const tokenMeta = getBreakdownTokenMeta(b, asset);\n const existing = chainsBySymbol.get(tokenMeta.symbol) ?? [];\n existing.push({\n id: `${b.contractAddress}-${b.chain.id}`,\n tokenAddress: b.contractAddress as `0x${string}`,\n chainId: b.chain.id,\n name: b.chain.name,\n usdValue,\n amount: balanceNum,\n symbol: tokenMeta.symbol,\n decimals: tokenMeta.decimals,\n tokenLogo: tokenMeta.logo,\n });\n chainsBySymbol.set(tokenMeta.symbol, existing);\n });\n\n for (const chainsForToken of chainsBySymbol.values()) {\n const sortedChains = chainsForToken.sort((a, b) => {\n const orderDiff =\n getSourceOrder(a.tokenAddress, a.chainId) -\n getSourceOrder(b.tokenAddress, b.chainId);\n if (orderDiff !== 0) return orderDiff;\n return b.usdValue - a.usdValue;\n });\n\n const selectableChains = sortedChains.filter(\n (chain) => chain.usdValue >= MIN_SELECTABLE_SOURCE_BALANCE_USD,\n );\n const belowMinimumChains = sortedChains.filter(\n (chain) => chain.usdValue < MIN_SELECTABLE_SOURCE_BALANCE_USD,\n );\n\n const tokenMeta = {\n symbol: sortedChains[0].symbol,\n decimals: sortedChains[0].decimals,\n logo: sortedChains[0].tokenLogo,\n };\n\n const selectableEntry = buildTokenEntry(\n tokenMeta,\n selectableChains,\n \"selectable\",\n );\n if (selectableEntry) selectableTokens.push(selectableEntry);\n\n const belowMinimumEntry = buildTokenEntry(\n tokenMeta,\n belowMinimumChains,\n \"below-minimum\",\n );\n if (belowMinimumEntry) belowMinimumTokens.push(belowMinimumEntry);\n }\n }\n\n const sortTokenEntries = (a: TokenWithMeta, b: TokenWithMeta) => {\n if (a.priorityRank !== b.priorityRank) {\n return a.priorityRank - b.priorityRank;\n }\n return b.totalUsdValue - a.totalUsdValue;\n };\n\n return {\n selectableTokens: selectableTokens.sort(sortTokenEntries),\n belowMinimumTokens: belowMinimumTokens.sort(sortTokenEntries),\n };\n}\n\nconst AssetSelectionContainer = ({\n widget,\n heading,\n onClose,\n}: AssetSelectionContainerProps) => {\n const { assetSelection, setAssetSelection, swapBalance } = widget;\n\n const [isProgressBarVisible, setIsProgressBarVisible] = useState(false);\n const [isProgressBarEntering, setIsProgressBarEntering] = useState(false);\n const [isProgressBarExiting, setIsProgressBarExiting] = useState(false);\n const [showStickyPopular, setShowStickyPopular] = useState(false);\n const scrollContainerRef = useRef(null);\n const popularSectionRef = useRef(null);\n\n const selectedChainIds = assetSelection.selectedChainIds;\n const filter = assetSelection.filter;\n const expandedTokens = assetSelection.expandedTokens;\n const destinationForSorting = useMemo(\n () => ({\n chainId: widget.destination.chainId,\n tokenAddress: widget.destination.tokenAddress,\n tokenSymbol: widget.destination.tokenSymbol,\n }),\n [\n widget.destination.chainId,\n widget.destination.tokenAddress,\n widget.destination.tokenSymbol,\n ],\n );\n\n // Defer expensive token transformation to avoid blocking UI\n const deferredSwapBalance = useDeferredValue(swapBalance);\n\n const {\n selectableTokens: selectableTokenEntries,\n belowMinimumTokens: belowMinimumTokenEntries,\n } = useMemo(\n () =>\n transformSwapBalanceToTokens(deferredSwapBalance, destinationForSorting),\n [deferredSwapBalance, destinationForSorting],\n );\n\n const allDisplayTokens = useMemo(\n () => [...selectableTokenEntries, ...belowMinimumTokenEntries],\n [selectableTokenEntries, belowMinimumTokenEntries],\n );\n\n const disabledChainIds = useMemo>(() => {\n const disabled = new Set();\n belowMinimumTokenEntries.forEach((token) => {\n token.chains.forEach((chain) => {\n disabled.add(chain.id);\n });\n });\n return disabled;\n }, [belowMinimumTokenEntries]);\n\n const selectableChainIds = useMemo(() => {\n const selectable = new Set();\n selectableTokenEntries.forEach((token) => {\n token.chains.forEach((chain) => {\n if (!disabledChainIds.has(chain.id)) {\n selectable.add(chain.id);\n }\n });\n });\n return selectable;\n }, [selectableTokenEntries, disabledChainIds]);\n\n const selectableTokensForPreset = useMemo(\n () =>\n selectableTokenEntries.map((token) => ({\n ...token,\n chains: token.chains.filter((chain) => !disabledChainIds.has(chain.id)),\n })),\n [selectableTokenEntries, disabledChainIds],\n );\n\n const sortAndGateSelection = useCallback(\n (chainIds: Iterable) => {\n const eligibleSourceIds = [...new Set(chainIds)].filter(\n (id) => !disabledChainIds.has(id),\n );\n\n return new Set(\n buildSortedFromSources({\n sourceIds: eligibleSourceIds,\n swapBalance,\n destination: destinationForSorting,\n }).map((source) => `${source.tokenAddress}-${source.chainId}`),\n );\n },\n [swapBalance, destinationForSorting, disabledChainIds],\n );\n\n // Build index Map for O(1) token lookups (js-index-maps)\n const tokensById = useMemo(\n () => new Map(allDisplayTokens.map((t) => [t.id, t])),\n [allDisplayTokens],\n );\n\n useEffect(() => {\n if (selectedChainIds.size === 0) return;\n const nextSelected = new Set(\n [...selectedChainIds].filter((id) => selectableChainIds.has(id)),\n );\n if (nextSelected.size === selectedChainIds.size) return;\n\n const nextFilter = checkIfMatchesPreset(\n selectableTokensForPreset,\n nextSelected,\n );\n\n setAssetSelection({\n selectedChainIds: sortAndGateSelection(nextSelected),\n filter: nextFilter,\n });\n }, [\n selectedChainIds,\n selectableChainIds,\n selectableTokensForPreset,\n setAssetSelection,\n sortAndGateSelection,\n swapBalance,\n ]);\n\n const selectedAmount = useMemo(() => {\n let total = 0;\n selectableTokenEntries.forEach((token) => {\n token.chains.forEach((chain) => {\n if (selectedChainIds.has(chain.id) && !disabledChainIds.has(chain.id)) {\n total += chain.usdValue;\n }\n });\n });\n return total;\n }, [selectableTokenEntries, selectedChainIds, disabledChainIds]);\n\n const requiredAmount = widget.inputs.amount\n ? parseFloat(widget.inputs.amount.replace(/,/g, \"\"))\n : 0;\n\n const showProgressBar = requiredAmount > 0 && requiredAmount > selectedAmount;\n const progressPercent =\n requiredAmount > 0\n ? Math.min((selectedAmount / requiredAmount) * 100, 100)\n : 0;\n\n useEffect(() => {\n if (showProgressBar) {\n setIsProgressBarVisible(true);\n setIsProgressBarExiting(false);\n setIsProgressBarEntering(true);\n const timer = setTimeout(() => {\n setIsProgressBarEntering(false);\n }, PROGRESS_BAR_ANIMATION_DELAY_MS);\n return () => clearTimeout(timer);\n } else if (isProgressBarVisible) {\n setIsProgressBarExiting(true);\n const timer = setTimeout(() => {\n setIsProgressBarVisible(false);\n setIsProgressBarExiting(false);\n }, PROGRESS_BAR_EXIT_DURATION_MS);\n return () => clearTimeout(timer);\n }\n }, [showProgressBar, isProgressBarVisible]);\n\n useEffect(() => {\n const container = scrollContainerRef.current;\n if (!container) return;\n\n // Use startTransition for non-urgent scroll updates (rerender-transitions)\n const handleScroll = () => {\n const scrollTop = container.scrollTop;\n startTransition(() => {\n setShowStickyPopular(scrollTop > SCROLL_THRESHOLD_PX);\n });\n };\n\n container.addEventListener(\"scroll\", handleScroll, { passive: true });\n return () => container.removeEventListener(\"scroll\", handleScroll);\n }, []);\n\n const scrollToPopular = useCallback(() => {\n scrollContainerRef.current?.scrollTo({\n top: 0,\n behavior: \"smooth\",\n });\n }, []);\n\n const handlePresetClick = useCallback(\n (preset: \"all\" | \"stablecoins\" | \"native\") => {\n if (preset === \"all\") {\n const nextSelected = sortAndGateSelection(selectableChainIds);\n setAssetSelection({\n selectedChainIds: nextSelected,\n filter: \"all\",\n expandedTokens: new Set(),\n });\n return;\n }\n\n const newChainIds = new Set();\n selectableTokenEntries.forEach((token) => {\n const shouldInclude =\n (preset === \"stablecoins\" && token.category === \"stablecoin\") ||\n (preset === \"native\" && token.category === \"native\");\n\n if (shouldInclude) {\n token.chains.forEach((chain) => {\n if (!disabledChainIds.has(chain.id)) {\n newChainIds.add(chain.id);\n }\n });\n }\n });\n const nextSelected = sortAndGateSelection(newChainIds);\n\n setAssetSelection({\n selectedChainIds: nextSelected,\n filter: preset,\n });\n },\n [\n selectableTokenEntries,\n selectableChainIds,\n setAssetSelection,\n disabledChainIds,\n sortAndGateSelection,\n swapBalance,\n ],\n );\n\n const toggleTokenSelection = useCallback(\n (tokenId: string) => {\n const token = tokensById.get(tokenId); // O(1) lookup instead of O(n)\n if (!token) return;\n\n const selectableChains = token.chains.filter(\n (chain) => !disabledChainIds.has(chain.id),\n );\n if (selectableChains.length === 0) return;\n\n const allChainsSelected = selectableChains.every((c) =>\n selectedChainIds.has(c.id),\n );\n const newChainIds = new Set(selectedChainIds);\n\n if (allChainsSelected) {\n selectableChains.forEach((chain) => newChainIds.delete(chain.id));\n } else {\n selectableChains.forEach((chain) => newChainIds.add(chain.id));\n }\n\n const newFilter = checkIfMatchesPreset(\n selectableTokensForPreset,\n newChainIds,\n );\n const nextSelected = sortAndGateSelection(newChainIds);\n\n setAssetSelection({\n selectedChainIds: nextSelected,\n filter: newFilter,\n });\n },\n [\n selectableTokensForPreset,\n tokensById,\n selectedChainIds,\n setAssetSelection,\n disabledChainIds,\n sortAndGateSelection,\n swapBalance,\n ],\n );\n\n const toggleChainSelection = useCallback(\n (chainId: string) => {\n if (disabledChainIds.has(chainId)) return;\n\n const newChainIds = new Set(selectedChainIds);\n if (newChainIds.has(chainId)) {\n newChainIds.delete(chainId);\n } else {\n newChainIds.add(chainId);\n }\n\n const newFilter = checkIfMatchesPreset(\n selectableTokensForPreset,\n newChainIds,\n );\n const nextSelected = sortAndGateSelection(newChainIds);\n\n setAssetSelection({\n selectedChainIds: nextSelected,\n filter: newFilter,\n });\n },\n [\n disabledChainIds,\n selectableTokensForPreset,\n selectedChainIds,\n setAssetSelection,\n sortAndGateSelection,\n swapBalance,\n ],\n );\n\n const toggleExpanded = useCallback(\n (tokenId: string) => {\n let newExpanded = new Set(expandedTokens);\n if (tokenId === \"below-minimum-section\") {\n if (newExpanded.has(\"below-minimum-section\")) {\n newExpanded.delete(\"below-minimum-section\");\n } else {\n newExpanded = new Set(newExpanded);\n newExpanded.add(\"below-minimum-section\");\n setTimeout(() => {\n if (scrollContainerRef.current) {\n const currentScrollTop = scrollContainerRef.current.scrollTop;\n scrollContainerRef.current.scrollTo({\n top: currentScrollTop + 70,\n behavior: \"smooth\",\n });\n }\n }, 100);\n }\n } else {\n const belowMinimumExpanded = newExpanded.has(\"below-minimum-section\");\n if (newExpanded.has(tokenId)) {\n newExpanded = belowMinimumExpanded\n ? new Set([\"below-minimum-section\"])\n : new Set();\n } else {\n newExpanded = belowMinimumExpanded\n ? new Set([\"below-minimum-section\", tokenId])\n : new Set([tokenId]);\n }\n }\n setAssetSelection({ expandedTokens: newExpanded });\n },\n [expandedTokens, setAssetSelection],\n );\n\n const handleDeselectAll = useCallback(() => {\n setAssetSelection({\n selectedChainIds: new Set(),\n filter: \"custom\",\n });\n }, [selectedChainIds, setAssetSelection, swapBalance]);\n\n const handleDone = useCallback(() => {\n widget.goToStep(\"amount\");\n }, [filter, selectedChainIds, swapBalance, widget]);\n\n return (\n <>\n \n \n
\n
\n {\n if (value !== \"custom\") {\n handlePresetClick(value as \"all\" | \"stablecoins\" | \"native\");\n }\n }}\n >\n \n Any token\n Stablecoins\n Native\n {filter === \"custom\" && (\n Custom\n )}\n \n \n \n {filter === \"custom\" ? : \"Deselect all\"}\n \n
\n\n
\n
\n {showStickyPopular && selectableTokenEntries.length > 0 && (\n \n Popular\n \n )}\n \n {selectableTokenEntries.length > 0 && (\n \n
\n \n Popular\n \n
\n {selectableTokenEntries.map((token, index) => (\n toggleExpanded(token.id)}\n onToggleToken={() => toggleTokenSelection(token.id)}\n onToggleChain={toggleChainSelection}\n isFirst={false}\n isLast={index === selectableTokenEntries.length - 1}\n />\n ))}\n
\n )}\n\n {belowMinimumTokenEntries.length > 0 && (\n
\n toggleExpanded(\"below-minimum-section\")}\n >\n \n Tokens Below Minimum Balance (\n {belowMinimumTokenEntries.length})\n \n \n
\n\n {expandedTokens.has(\"below-minimum-section\") && (\n
\n {disabledChainIds.size > 0 && (\n
\n \n Tokens under $\n {MIN_SELECTABLE_SOURCE_BALANCE_USD.toFixed(0)} are\n unavailable for deposits to prevent failed\n transactions\n \n
\n )}\n {belowMinimumTokenEntries.map((token, index) => (\n toggleExpanded(token.id)}\n onToggleToken={() => toggleTokenSelection(token.id)}\n onToggleChain={toggleChainSelection}\n isFirst={index === 0}\n isLast={\n index === belowMinimumTokenEntries.length - 1\n }\n />\n ))}\n
\n )}\n
\n )}\n
\n\n {!showProgressBar && (\n \n )}\n {!showProgressBar && (\n
\n\n {isProgressBarVisible && (\n \n
\n \n Selected / Required\n \n \n \n ${selectedAmount.toLocaleString()}\n \n \n {\" \"}\n / ${requiredAmount.toLocaleString()}\n \n \n
\n
\n \n
\n \n )}\n \n );\n};\n\nexport default AssetSelectionContainer;\n", + "content": "\"use client\";\n\nimport {\n useMemo,\n useCallback,\n useState,\n useEffect,\n useRef,\n startTransition,\n useDeferredValue,\n} from \"react\";\nimport { ChevronDownIcon } from \"./icons\";\nimport WidgetHeader from \"./widget-header\";\nimport type { DepositWidgetContextValue, Token, ChainItem } from \"../types\";\nimport { Tabs, TabsList, TabsTrigger } from \"../../ui/tabs\";\nimport { CardContent } from \"../../ui/card\";\nimport { Button } from \"../../ui/button\";\nimport TokenRow from \"./token-row\";\nimport { formatTokenBalance } from \"@avail-project/nexus-sdk-v2/utils\";\nimport type { TokenBalance, ChainBalance } from \"@avail-project/nexus-sdk-v2\";\nimport { usdFormatter } from \"../../common\";\nimport { X } from \"lucide-react\";\nimport {\n SCROLL_THRESHOLD_PX,\n PROGRESS_BAR_ANIMATION_DELAY_MS,\n PROGRESS_BAR_EXIT_DURATION_MS,\n MIN_SELECTABLE_SOURCE_BALANCE_USD,\n} from \"../constants/widget\";\nimport {\n buildSortedFromSources,\n checkIfMatchesPreset,\n isNative,\n isStablecoin,\n} from \"../utils\";\n\ninterface AssetSelectionContainerProps {\n widget: DepositWidgetContextValue;\n heading?: string;\n onClose?: () => void;\n}\n\ninterface TokenWithMeta extends Token {\n totalUsdValue: number;\n priorityRank: number;\n group: \"selectable\" | \"below-minimum\";\n}\n\ntype ChainItemWithTokenMeta = ChainItem & {\n symbol: string;\n decimals: number;\n tokenLogo: string;\n};\n\n// v2: ChainBalance replaces UserAssetDatum[\"breakdown\"][number]\ntype AssetBreakdownWithOptionalIcon = ChainBalance & {\n icon?: string;\n};\n\nfunction parseNonNegativeNumber(value: unknown): number {\n const parsed = Number.parseFloat(String(value));\n if (!Number.isFinite(parsed) || parsed < 0) return 0;\n return parsed;\n}\n\nfunction getBreakdownTokenMeta(\n breakdown: ChainBalance,\n asset: TokenBalance\n) {\n // v2: logo replaces icon; value (string) replaces balanceInFiat (number)\n const breakdownIcon = (breakdown as AssetBreakdownWithOptionalIcon).icon;\n return {\n symbol: asset.symbol,\n decimals: breakdown.decimals ?? asset.decimals,\n logo: breakdownIcon || breakdown.chain.logo || asset.logo || \"\",\n };\n}\n\nfunction transformSwapBalanceToTokens(\n swapBalance: TokenBalance[] | null,\n destination: Pick<\n DepositWidgetContextValue[\"destination\"],\n \"chainId\" | \"tokenAddress\" | \"tokenSymbol\"\n >,\n): {\n selectableTokens: TokenWithMeta[];\n belowMinimumTokens: TokenWithMeta[];\n} {\n if (!swapBalance) {\n return {\n selectableTokens: [],\n belowMinimumTokens: [],\n };\n }\n\n const allSourceIds = new Set();\n swapBalance.forEach((asset) => {\n // v2: chainBalances replaces breakdown\n asset.chainBalances?.forEach((breakdown) => {\n if (!breakdown.chain?.id || !breakdown.contractAddress) return;\n allSourceIds.add(`${breakdown.contractAddress}-${breakdown.chain.id}`);\n });\n });\n\n const orderedSources = buildSortedFromSources({\n sourceIds: allSourceIds,\n swapBalance,\n destination,\n });\n\n const sourceOrderIndex = new Map();\n orderedSources.forEach((source, index) => {\n sourceOrderIndex.set(\n `${source.tokenAddress.toLowerCase()}-${source.chainId}`,\n index,\n );\n });\n\n const getSourceOrder = (tokenAddress: string, chainId: number) =>\n sourceOrderIndex.get(`${tokenAddress.toLowerCase()}-${chainId}`) ??\n Number.MAX_SAFE_INTEGER;\n\n const buildTokenEntry = (\n tokenMeta: { symbol: string; decimals: number; logo: string },\n chains: ChainItemWithTokenMeta[],\n group: \"selectable\" | \"below-minimum\",\n ): TokenWithMeta | null => {\n if (chains.length === 0) return null;\n\n const totalUsdValue = chains.reduce((sum, c) => sum + c.usdValue, 0);\n const totalAmount = chains.reduce((sum, c) => sum + c.amount, 0);\n const category = isStablecoin(tokenMeta.symbol)\n ? \"stablecoin\"\n : isNative(tokenMeta.symbol)\n ? \"native\"\n : \"memecoin\";\n\n return {\n id: `${tokenMeta.symbol}-${chains[0].tokenAddress}-${group}`,\n symbol: tokenMeta.symbol,\n chainsLabel:\n chains.length > 1\n ? `${chains.length} Chain${chains.length !== 1 ? \"s\" : \"\"}`\n : chains[0].name,\n usdValue: usdFormatter.format(totalUsdValue),\n amount: formatTokenBalance(totalAmount, {\n decimals: tokenMeta.decimals,\n symbol: tokenMeta.symbol,\n }),\n decimals: tokenMeta.decimals,\n logo: tokenMeta.logo,\n category,\n priorityRank: chains.length\n ? getSourceOrder(chains[0].tokenAddress, chains[0].chainId)\n : Number.MAX_SAFE_INTEGER,\n totalUsdValue,\n group,\n chains,\n };\n };\n\n const selectableTokens: TokenWithMeta[] = [];\n const belowMinimumTokens: TokenWithMeta[] = [];\n\n for (const asset of swapBalance) {\n if (!asset.chainBalances?.length) continue;\n const chainsBySymbol = new Map();\n\n asset.chainBalances\n .filter((b: any) => b.chain && b.balance)\n .forEach((b) => {\n const balanceNum = parseFloat(b.balance);\n if (!Number.isFinite(balanceNum) || balanceNum <= 0) return;\n\n const usdValue = parseNonNegativeNumber(parseFloat(b.value ?? \"0\"));\n const tokenMeta = getBreakdownTokenMeta(b, asset);\n const existing = chainsBySymbol.get(tokenMeta.symbol) ?? [];\n existing.push({\n id: `${b.contractAddress}-${b.chain.id}`,\n tokenAddress: b.contractAddress as `0x${string}`,\n chainId: b.chain.id,\n name: b.chain.name,\n usdValue,\n amount: balanceNum,\n symbol: tokenMeta.symbol,\n decimals: tokenMeta.decimals,\n tokenLogo: tokenMeta.logo,\n });\n chainsBySymbol.set(tokenMeta.symbol, existing);\n });\n\n for (const chainsForToken of chainsBySymbol.values()) {\n const sortedChains = chainsForToken.sort((a, b) => {\n const orderDiff =\n getSourceOrder(a.tokenAddress, a.chainId) -\n getSourceOrder(b.tokenAddress, b.chainId);\n if (orderDiff !== 0) return orderDiff;\n return b.usdValue - a.usdValue;\n });\n\n const selectableChains = sortedChains.filter(\n (chain) => chain.usdValue >= MIN_SELECTABLE_SOURCE_BALANCE_USD,\n );\n const belowMinimumChains = sortedChains.filter(\n (chain) => chain.usdValue < MIN_SELECTABLE_SOURCE_BALANCE_USD,\n );\n\n const tokenMeta = {\n symbol: sortedChains[0].symbol,\n decimals: sortedChains[0].decimals,\n logo: sortedChains[0].tokenLogo,\n };\n\n const selectableEntry = buildTokenEntry(\n tokenMeta,\n selectableChains,\n \"selectable\",\n );\n if (selectableEntry) selectableTokens.push(selectableEntry);\n\n const belowMinimumEntry = buildTokenEntry(\n tokenMeta,\n belowMinimumChains,\n \"below-minimum\",\n );\n if (belowMinimumEntry) belowMinimumTokens.push(belowMinimumEntry);\n }\n }\n\n const sortTokenEntries = (a: TokenWithMeta, b: TokenWithMeta) => {\n if (a.priorityRank !== b.priorityRank) {\n return a.priorityRank - b.priorityRank;\n }\n return b.totalUsdValue - a.totalUsdValue;\n };\n\n return {\n selectableTokens: selectableTokens.sort(sortTokenEntries),\n belowMinimumTokens: belowMinimumTokens.sort(sortTokenEntries),\n };\n}\n\nconst AssetSelectionContainer = ({\n widget,\n heading,\n onClose,\n}: AssetSelectionContainerProps) => {\n const { assetSelection, setAssetSelection, swapBalance } = widget;\n\n const [isProgressBarVisible, setIsProgressBarVisible] = useState(false);\n const [isProgressBarEntering, setIsProgressBarEntering] = useState(false);\n const [isProgressBarExiting, setIsProgressBarExiting] = useState(false);\n const [showStickyPopular, setShowStickyPopular] = useState(false);\n const scrollContainerRef = useRef(null);\n const popularSectionRef = useRef(null);\n\n const selectedChainIds = assetSelection.selectedChainIds;\n const filter = assetSelection.filter;\n const expandedTokens = assetSelection.expandedTokens;\n const destinationForSorting = useMemo(\n () => ({\n chainId: widget.destination.chainId,\n tokenAddress: widget.destination.tokenAddress,\n tokenSymbol: widget.destination.tokenSymbol,\n }),\n [\n widget.destination.chainId,\n widget.destination.tokenAddress,\n widget.destination.tokenSymbol,\n ],\n );\n\n // Defer expensive token transformation to avoid blocking UI\n const deferredSwapBalance = useDeferredValue(swapBalance);\n\n const {\n selectableTokens: selectableTokenEntries,\n belowMinimumTokens: belowMinimumTokenEntries,\n } = useMemo(\n () =>\n transformSwapBalanceToTokens(deferredSwapBalance, destinationForSorting),\n [deferredSwapBalance, destinationForSorting],\n );\n\n const allDisplayTokens = useMemo(\n () => [...selectableTokenEntries, ...belowMinimumTokenEntries],\n [selectableTokenEntries, belowMinimumTokenEntries],\n );\n\n const disabledChainIds = useMemo>(() => {\n const disabled = new Set();\n belowMinimumTokenEntries.forEach((token) => {\n token.chains.forEach((chain) => {\n disabled.add(chain.id);\n });\n });\n return disabled;\n }, [belowMinimumTokenEntries]);\n\n const selectableChainIds = useMemo(() => {\n const selectable = new Set();\n selectableTokenEntries.forEach((token) => {\n token.chains.forEach((chain) => {\n if (!disabledChainIds.has(chain.id)) {\n selectable.add(chain.id);\n }\n });\n });\n return selectable;\n }, [selectableTokenEntries, disabledChainIds]);\n\n const selectableTokensForPreset = useMemo(\n () =>\n selectableTokenEntries.map((token) => ({\n ...token,\n chains: token.chains.filter((chain) => !disabledChainIds.has(chain.id)),\n })),\n [selectableTokenEntries, disabledChainIds],\n );\n\n const sortAndGateSelection = useCallback(\n (chainIds: Iterable) => {\n const eligibleSourceIds = [...new Set(chainIds)].filter(\n (id) => !disabledChainIds.has(id),\n );\n\n return new Set(\n buildSortedFromSources({\n sourceIds: eligibleSourceIds,\n swapBalance,\n destination: destinationForSorting,\n }).map((source) => `${source.tokenAddress}-${source.chainId}`),\n );\n },\n [swapBalance, destinationForSorting, disabledChainIds],\n );\n\n // Build index Map for O(1) token lookups (js-index-maps)\n const tokensById = useMemo(\n () => new Map(allDisplayTokens.map((t) => [t.id, t])),\n [allDisplayTokens],\n );\n\n useEffect(() => {\n if (selectedChainIds.size === 0) return;\n const nextSelected = new Set(\n [...selectedChainIds].filter((id) => selectableChainIds.has(id)),\n );\n if (nextSelected.size === selectedChainIds.size) return;\n\n const nextFilter = checkIfMatchesPreset(\n selectableTokensForPreset,\n nextSelected,\n );\n\n setAssetSelection({\n selectedChainIds: sortAndGateSelection(nextSelected),\n filter: nextFilter,\n });\n }, [\n selectedChainIds,\n selectableChainIds,\n selectableTokensForPreset,\n setAssetSelection,\n sortAndGateSelection,\n swapBalance,\n ]);\n\n const selectedAmount = useMemo(() => {\n let total = 0;\n selectableTokenEntries.forEach((token) => {\n token.chains.forEach((chain) => {\n if (selectedChainIds.has(chain.id) && !disabledChainIds.has(chain.id)) {\n total += chain.usdValue;\n }\n });\n });\n return total;\n }, [selectableTokenEntries, selectedChainIds, disabledChainIds]);\n\n const requiredAmount = widget.inputs.amount\n ? parseFloat(widget.inputs.amount.replace(/,/g, \"\"))\n : 0;\n\n const showProgressBar = requiredAmount > 0 && requiredAmount > selectedAmount;\n const progressPercent =\n requiredAmount > 0\n ? Math.min((selectedAmount / requiredAmount) * 100, 100)\n : 0;\n\n useEffect(() => {\n if (showProgressBar) {\n setIsProgressBarVisible(true);\n setIsProgressBarExiting(false);\n setIsProgressBarEntering(true);\n const timer = setTimeout(() => {\n setIsProgressBarEntering(false);\n }, PROGRESS_BAR_ANIMATION_DELAY_MS);\n return () => clearTimeout(timer);\n } else if (isProgressBarVisible) {\n setIsProgressBarExiting(true);\n const timer = setTimeout(() => {\n setIsProgressBarVisible(false);\n setIsProgressBarExiting(false);\n }, PROGRESS_BAR_EXIT_DURATION_MS);\n return () => clearTimeout(timer);\n }\n }, [showProgressBar, isProgressBarVisible]);\n\n useEffect(() => {\n const container = scrollContainerRef.current;\n if (!container) return;\n\n // Use startTransition for non-urgent scroll updates (rerender-transitions)\n const handleScroll = () => {\n const scrollTop = container.scrollTop;\n startTransition(() => {\n setShowStickyPopular(scrollTop > SCROLL_THRESHOLD_PX);\n });\n };\n\n container.addEventListener(\"scroll\", handleScroll, { passive: true });\n return () => container.removeEventListener(\"scroll\", handleScroll);\n }, []);\n\n const scrollToPopular = useCallback(() => {\n scrollContainerRef.current?.scrollTo({\n top: 0,\n behavior: \"smooth\",\n });\n }, []);\n\n const handlePresetClick = useCallback(\n (preset: \"all\" | \"stablecoins\" | \"native\") => {\n if (preset === \"all\") {\n const nextSelected = sortAndGateSelection(selectableChainIds);\n setAssetSelection({\n selectedChainIds: nextSelected,\n filter: \"all\",\n expandedTokens: new Set(),\n });\n return;\n }\n\n const newChainIds = new Set();\n selectableTokenEntries.forEach((token) => {\n const shouldInclude =\n (preset === \"stablecoins\" && token.category === \"stablecoin\") ||\n (preset === \"native\" && token.category === \"native\");\n\n if (shouldInclude) {\n token.chains.forEach((chain) => {\n if (!disabledChainIds.has(chain.id)) {\n newChainIds.add(chain.id);\n }\n });\n }\n });\n const nextSelected = sortAndGateSelection(newChainIds);\n\n setAssetSelection({\n selectedChainIds: nextSelected,\n filter: preset,\n });\n },\n [\n selectableTokenEntries,\n selectableChainIds,\n setAssetSelection,\n disabledChainIds,\n sortAndGateSelection,\n swapBalance,\n ],\n );\n\n const toggleTokenSelection = useCallback(\n (tokenId: string) => {\n const token = tokensById.get(tokenId); // O(1) lookup instead of O(n)\n if (!token) return;\n\n const selectableChains = token.chains.filter(\n (chain) => !disabledChainIds.has(chain.id),\n );\n if (selectableChains.length === 0) return;\n\n const allChainsSelected = selectableChains.every((c) =>\n selectedChainIds.has(c.id),\n );\n const newChainIds = new Set(selectedChainIds);\n\n if (allChainsSelected) {\n selectableChains.forEach((chain) => newChainIds.delete(chain.id));\n } else {\n selectableChains.forEach((chain) => newChainIds.add(chain.id));\n }\n\n const newFilter = checkIfMatchesPreset(\n selectableTokensForPreset,\n newChainIds,\n );\n const nextSelected = sortAndGateSelection(newChainIds);\n\n setAssetSelection({\n selectedChainIds: nextSelected,\n filter: newFilter,\n });\n },\n [\n selectableTokensForPreset,\n tokensById,\n selectedChainIds,\n setAssetSelection,\n disabledChainIds,\n sortAndGateSelection,\n swapBalance,\n ],\n );\n\n const toggleChainSelection = useCallback(\n (chainId: string) => {\n if (disabledChainIds.has(chainId)) return;\n\n const newChainIds = new Set(selectedChainIds);\n if (newChainIds.has(chainId)) {\n newChainIds.delete(chainId);\n } else {\n newChainIds.add(chainId);\n }\n\n const newFilter = checkIfMatchesPreset(\n selectableTokensForPreset,\n newChainIds,\n );\n const nextSelected = sortAndGateSelection(newChainIds);\n\n setAssetSelection({\n selectedChainIds: nextSelected,\n filter: newFilter,\n });\n },\n [\n disabledChainIds,\n selectableTokensForPreset,\n selectedChainIds,\n setAssetSelection,\n sortAndGateSelection,\n swapBalance,\n ],\n );\n\n const toggleExpanded = useCallback(\n (tokenId: string) => {\n let newExpanded = new Set(expandedTokens);\n if (tokenId === \"below-minimum-section\") {\n if (newExpanded.has(\"below-minimum-section\")) {\n newExpanded.delete(\"below-minimum-section\");\n } else {\n newExpanded = new Set(newExpanded);\n newExpanded.add(\"below-minimum-section\");\n setTimeout(() => {\n if (scrollContainerRef.current) {\n const currentScrollTop = scrollContainerRef.current.scrollTop;\n scrollContainerRef.current.scrollTo({\n top: currentScrollTop + 70,\n behavior: \"smooth\",\n });\n }\n }, 100);\n }\n } else {\n const belowMinimumExpanded = newExpanded.has(\"below-minimum-section\");\n if (newExpanded.has(tokenId)) {\n newExpanded = belowMinimumExpanded\n ? new Set([\"below-minimum-section\"])\n : new Set();\n } else {\n newExpanded = belowMinimumExpanded\n ? new Set([\"below-minimum-section\", tokenId])\n : new Set([tokenId]);\n }\n }\n setAssetSelection({ expandedTokens: newExpanded });\n },\n [expandedTokens, setAssetSelection],\n );\n\n const handleDeselectAll = useCallback(() => {\n setAssetSelection({\n selectedChainIds: new Set(),\n filter: \"custom\",\n });\n }, [selectedChainIds, setAssetSelection, swapBalance]);\n\n const handleDone = useCallback(() => {\n widget.goToStep(\"amount\");\n }, [filter, selectedChainIds, swapBalance, widget]);\n\n return (\n <>\n \n \n
\n
\n {\n if (value !== \"custom\") {\n handlePresetClick(value as \"all\" | \"stablecoins\" | \"native\");\n }\n }}\n >\n \n Any token\n Stablecoins\n Native\n {filter === \"custom\" && (\n Custom\n )}\n \n \n \n {filter === \"custom\" ? : \"Deselect all\"}\n \n
\n\n
\n
\n {showStickyPopular && selectableTokenEntries.length > 0 && (\n \n Popular\n \n )}\n \n {selectableTokenEntries.length > 0 && (\n \n
\n \n Popular\n \n
\n {selectableTokenEntries.map((token, index) => (\n toggleExpanded(token.id)}\n onToggleToken={() => toggleTokenSelection(token.id)}\n onToggleChain={toggleChainSelection}\n isFirst={false}\n isLast={index === selectableTokenEntries.length - 1}\n />\n ))}\n
\n )}\n\n {belowMinimumTokenEntries.length > 0 && (\n
\n toggleExpanded(\"below-minimum-section\")}\n >\n \n Tokens Below Minimum Balance (\n {belowMinimumTokenEntries.length})\n \n \n
\n\n {expandedTokens.has(\"below-minimum-section\") && (\n
\n {disabledChainIds.size > 0 && (\n
\n \n Tokens under $\n {MIN_SELECTABLE_SOURCE_BALANCE_USD.toFixed(0)} are\n unavailable for deposits to prevent failed\n transactions\n \n
\n )}\n {belowMinimumTokenEntries.map((token, index) => (\n toggleExpanded(token.id)}\n onToggleToken={() => toggleTokenSelection(token.id)}\n onToggleChain={toggleChainSelection}\n isFirst={index === 0}\n isLast={\n index === belowMinimumTokenEntries.length - 1\n }\n />\n ))}\n
\n )}\n
\n )}\n
\n\n {!showProgressBar && (\n \n )}\n {!showProgressBar && (\n
\n\n {isProgressBarVisible && (\n \n
\n \n Selected / Required\n \n \n \n ${selectedAmount.toLocaleString()}\n \n \n {\" \"}\n / ${requiredAmount.toLocaleString()}\n \n \n
\n
\n \n
\n \n )}\n \n );\n};\n\nexport default AssetSelectionContainer;\n", "type": "registry:component", "target": "components/deposit/components/asset-selection-container.tsx" }, @@ -63,7 +63,7 @@ }, { "path": "registry/nexus-elements/deposit/components/confirmation-container.tsx", - "content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport SummaryCard from \"./summary-card\";\nimport { GasPumpIcon, CoinIcon } from \"./icons\";\nimport WidgetHeader from \"./widget-header\";\nimport { ReceiveAmountDisplay } from \"./receive-amount-display\";\nimport { ErrorBanner } from \"./error-banner\";\nimport type { DepositWidgetContextValue } from \"../types\";\nimport { Button } from \"../../ui/button\";\nimport { CardContent } from \"../../ui/card\";\nimport { usdFormatter } from \"../../common\";\nimport { formatTokenBalance } from \"@avail-project/nexus-core\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { BadgePercent, ShieldCheck } from \"lucide-react\";\nimport { formatFeeUsd, formatImpactPercent, formatSignedUsd } from \"../utils\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../../ui/tooltip\";\n\ninterface ConfirmationContainerProps {\n widget: DepositWidgetContextValue;\n heading?: string;\n onClose?: () => void;\n}\n\nconst ConfirmationContainer = ({\n widget,\n heading,\n onClose,\n}: ConfirmationContainerProps) => {\n const [showSpendDetails, setShowSpendDetails] = useState(false);\n const [showFeeDetails, setShowFeeDetails] = useState(false);\n const { getFiatValue } = useNexus();\n\n const {\n confirmationDetails,\n feeBreakdown,\n handleConfirmOrder,\n isProcessing,\n txError,\n activeIntent,\n simulationLoading,\n } = widget;\n\n const isLoading = simulationLoading || !activeIntent;\n\n const receiveAmount =\n confirmationDetails?.receiveAmountAfterSwapUsd?.toFixed(2) ?? \"0\";\n const timeLabel = confirmationDetails?.estimatedTime ?? \"~30s\";\n const amountSpent = confirmationDetails?.amountSpent ?? 0;\n // TODO: Ensure unique names are displayed\n const tokenNames = confirmationDetails?.sources\n .filter((s) => s)\n .map((s) => s?.symbol)\n .slice(0, 2)\n .join(\", \");\n const moreCount =\n (confirmationDetails?.sources.filter((s) => s).length ?? 0) - 2;\n const tokenNamesSummary =\n moreCount > 0 ? `${tokenNames} + ${moreCount} more` : tokenNames;\n\n // Combined filter + map into single iteration (js-combine-iterations)\n const sourceDetails = useMemo(() => {\n if (!confirmationDetails?.sources) return [];\n const result: Array<{\n chainName: string;\n chainLogo: string | undefined;\n tokenSymbol: string;\n tokenDecimals: number;\n amount: string;\n amountUsd?: number;\n isDestinationBalance: boolean;\n }> = [];\n for (const source of confirmationDetails.sources) {\n if (!source) continue;\n result.push({\n chainName: source.chainName ?? \"\",\n chainLogo: source.chainLogo,\n tokenSymbol: source.symbol ?? \"\",\n tokenDecimals: source.decimals ?? 6,\n amount: source.balance ?? \"0\",\n amountUsd: source.balanceInFiat,\n isDestinationBalance: source.isDestinationBalance ?? false,\n });\n }\n return result;\n }, [confirmationDetails]);\n\n const feeDetailRows = useMemo(\n () =>\n [\n { label: \"Gas sponsorship\", amountUsd: feeBreakdown.gasSponsorshipUsd },\n {\n label: \"Execution Gas fee\",\n amountUsd:\n feeBreakdown.executionGasFeeUsd + feeBreakdown.otherBridgeFeeUsd,\n },\n { label: \"Protocol fee\", amountUsd: feeBreakdown.protocolFeeUsd },\n { label: \"Solver fee\", amountUsd: feeBreakdown.solverFeeUsd },\n ].filter((row) => row.amountUsd > 0),\n [feeBreakdown],\n );\n\n const showFeeBreakdown = !isLoading && feeDetailRows.length > 0;\n const showPriceImpactBreakdown =\n !isLoading &&\n (Math.abs(feeBreakdown.swapImpactUsd) > 0 || feeBreakdown.bufferUsd > 0);\n\n return (\n <>\n \n \n
\n
\n \n
\n }\n title=\"You spend\"\n subtitle={\n isLoading\n ? \"Calculating...\"\n : tokenNamesSummary || \"Selected assets\"\n }\n value={String(amountSpent)}\n showBreakdown={!isLoading && sourceDetails.length > 0}\n loading={isLoading}\n expanded={showSpendDetails}\n onToggleExpand={() => setShowSpendDetails(!showSpendDetails)}\n >\n
\n {sourceDetails.map((source, index) => {\n const amountUsd =\n source.amountUsd ??\n getFiatValue(\n parseFloat(source.amount),\n source.tokenSymbol,\n );\n return (\n \n
\n {source.chainLogo && (\n \n )}\n
\n \n {source.tokenSymbol}\n \n \n {source.chainName}\n \n
\n
\n
\n \n {usdFormatter.format(amountUsd)}\n \n \n {formatTokenBalance(parseFloat(source.amount), {\n decimals: source.tokenDecimals,\n symbol: source.tokenSymbol,\n })}\n \n
\n
\n );\n })}\n
\n \n\n }\n title=\"Total fees\"\n value={formatFeeUsd(feeBreakdown.totalFeeUsd)}\n showBreakdown={showFeeBreakdown}\n loading={isLoading}\n expanded={showFeeDetails}\n onToggleExpand={() => setShowFeeDetails(!showFeeDetails)}\n >\n
\n {feeDetailRows.map((row) => (\n \n \n {row.label}\n \n \n {formatFeeUsd(row.amountUsd)}\n \n
\n ))}\n
\n \n\n {showPriceImpactBreakdown && (\n \n \n \n \n \n

Estimated market impact from multiple swap routes

\n
\n \n }\n title={\n
\n
\n Price impact\n
\n
\n }\n subText={\n
\n
\n
\n \n \n Swap buffer\n \n
\n \n {formatFeeUsd(feeBreakdown.bufferUsd)}\n \n
\n\n

\n Temporary buffer collected to ensure swaps succeed.{\" \"}\n
\n Excess funds are refunded.\n

\n
\n }\n value={`${formatSignedUsd(feeBreakdown.swapImpactUsd)} (${formatImpactPercent(feeBreakdown.swapImpactPercent)})`}\n showBreakdown={false}\n loading={isLoading}\n />\n )}\n
\n \n {txError && widget.status === \"error\" && (\n \n )}\n \n {isProcessing\n ? \"Fetching quote\"\n : isLoading\n ? \"Fetching quote\"\n : \"Confirm and deposit\"}\n \n \n
\n \n );\n};\n\nexport default ConfirmationContainer;\n", + "content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport SummaryCard from \"./summary-card\";\nimport { GasPumpIcon, CoinIcon } from \"./icons\";\nimport WidgetHeader from \"./widget-header\";\nimport { ReceiveAmountDisplay } from \"./receive-amount-display\";\nimport { ErrorBanner } from \"./error-banner\";\nimport type { DepositWidgetContextValue } from \"../types\";\nimport { Button } from \"../../ui/button\";\nimport { CardContent } from \"../../ui/card\";\nimport { usdFormatter } from \"../../common\";\nimport { formatTokenBalance } from \"@avail-project/nexus-sdk-v2/utils\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { BadgePercent, ShieldCheck } from \"lucide-react\";\nimport { formatFeeUsd, formatImpactPercent, formatSignedUsd } from \"../utils\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../../ui/tooltip\";\n\ninterface ConfirmationContainerProps {\n widget: DepositWidgetContextValue;\n heading?: string;\n onClose?: () => void;\n}\n\nconst ConfirmationContainer = ({\n widget,\n heading,\n onClose,\n}: ConfirmationContainerProps) => {\n const [showSpendDetails, setShowSpendDetails] = useState(false);\n const [showFeeDetails, setShowFeeDetails] = useState(false);\n const { getFiatValue } = useNexus();\n\n const {\n confirmationDetails,\n feeBreakdown,\n handleConfirmOrder,\n isProcessing,\n txError,\n activeIntent,\n simulationLoading,\n } = widget;\n\n const isLoading = simulationLoading || !activeIntent;\n\n const receiveAmount =\n confirmationDetails?.receiveAmountAfterSwapUsd?.toFixed(2) ?? \"0\";\n const timeLabel = confirmationDetails?.estimatedTime ?? \"~30s\";\n const amountSpent = confirmationDetails?.amountSpent ?? 0;\n // TODO: Ensure unique names are displayed\n const tokenNames = confirmationDetails?.sources\n .filter((s) => s)\n .map((s) => s?.symbol)\n .slice(0, 2)\n .join(\", \");\n const moreCount =\n (confirmationDetails?.sources.filter((s) => s).length ?? 0) - 2;\n const tokenNamesSummary =\n moreCount > 0 ? `${tokenNames} + ${moreCount} more` : tokenNames;\n\n // Combined filter + map into single iteration (js-combine-iterations)\n const sourceDetails = useMemo(() => {\n if (!confirmationDetails?.sources) return [];\n const result: Array<{\n chainName: string;\n chainLogo: string | undefined;\n tokenSymbol: string;\n tokenDecimals: number;\n amount: string;\n amountUsd?: number;\n isDestinationBalance: boolean;\n }> = [];\n for (const source of confirmationDetails.sources) {\n if (!source) continue;\n result.push({\n chainName: source.chainName ?? \"\",\n chainLogo: source.chainLogo,\n tokenSymbol: source.symbol ?? \"\",\n tokenDecimals: source.decimals ?? 6,\n amount: source.balance ?? \"0\",\n amountUsd: parseFloat(source.value ?? \"0\"),\n isDestinationBalance: source.isDestinationBalance ?? false,\n });\n }\n return result;\n }, [confirmationDetails]);\n\n const feeDetailRows = useMemo(\n () =>\n [\n { label: \"Gas sponsorship\", amountUsd: feeBreakdown.gasSponsorshipUsd },\n {\n label: \"Execution Gas fee\",\n amountUsd:\n feeBreakdown.executionGasFeeUsd + feeBreakdown.otherBridgeFeeUsd,\n },\n { label: \"Protocol fee\", amountUsd: feeBreakdown.protocolFeeUsd },\n { label: \"Solver fee\", amountUsd: feeBreakdown.solverFeeUsd },\n ].filter((row) => row.amountUsd > 0),\n [feeBreakdown],\n );\n\n const showFeeBreakdown = !isLoading && feeDetailRows.length > 0;\n const showPriceImpactBreakdown =\n !isLoading &&\n (Math.abs(feeBreakdown.swapImpactUsd) > 0 || feeBreakdown.bufferUsd > 0);\n\n return (\n <>\n \n \n
\n
\n \n
\n }\n title=\"You spend\"\n subtitle={\n isLoading\n ? \"Calculating...\"\n : tokenNamesSummary || \"Selected assets\"\n }\n value={String(amountSpent)}\n showBreakdown={!isLoading && sourceDetails.length > 0}\n loading={isLoading}\n expanded={showSpendDetails}\n onToggleExpand={() => setShowSpendDetails(!showSpendDetails)}\n >\n
\n {sourceDetails.map((source, index) => {\n const amountUsd =\n source.amountUsd ??\n getFiatValue(\n parseFloat(source.amount),\n source.tokenSymbol,\n );\n return (\n \n
\n {source.chainLogo && (\n \n )}\n
\n \n {source.tokenSymbol}\n \n \n {source.chainName}\n \n
\n
\n
\n \n {usdFormatter.format(amountUsd)}\n \n \n {formatTokenBalance(parseFloat(source.amount), {\n decimals: source.tokenDecimals,\n symbol: source.tokenSymbol,\n })}\n \n
\n
\n );\n })}\n
\n \n\n }\n title=\"Total fees\"\n value={formatFeeUsd(feeBreakdown.totalFeeUsd)}\n showBreakdown={showFeeBreakdown}\n loading={isLoading}\n expanded={showFeeDetails}\n onToggleExpand={() => setShowFeeDetails(!showFeeDetails)}\n >\n
\n {feeDetailRows.map((row) => (\n \n \n {row.label}\n \n \n {formatFeeUsd(row.amountUsd)}\n \n
\n ))}\n
\n \n\n {showPriceImpactBreakdown && (\n \n \n \n \n \n

Estimated market impact from multiple swap routes

\n
\n \n }\n title={\n
\n
\n Price impact\n
\n
\n }\n subText={\n
\n
\n
\n \n \n Swap buffer\n \n
\n \n {formatFeeUsd(feeBreakdown.bufferUsd)}\n \n
\n\n

\n Temporary buffer collected to ensure swaps succeed.{\" \"}\n
\n Excess funds are refunded.\n

\n
\n }\n value={`${formatSignedUsd(feeBreakdown.swapImpactUsd)} (${formatImpactPercent(feeBreakdown.swapImpactPercent)})`}\n showBreakdown={false}\n loading={isLoading}\n />\n )}\n
\n \n {txError && widget.status === \"error\" && (\n \n )}\n \n {isProcessing\n ? \"Fetching quote\"\n : isLoading\n ? \"Fetching quote\"\n : \"Confirm and deposit\"}\n \n \n
\n \n );\n};\n\nexport default ConfirmationContainer;\n", "type": "registry:component", "target": "components/deposit/components/confirmation-container.tsx" }, @@ -99,7 +99,7 @@ }, { "path": "registry/nexus-elements/deposit/components/pay-using.tsx", - "content": "import { useMemo, useState, useEffect, useRef } from \"react\";\nimport ButtonCard from \"./button-card\";\nimport { RightChevronIcon, CoinIcon } from \"./icons\";\nimport { Skeleton } from \"../../ui/skeleton\";\nimport {\n LOADING_SKELETON_DELAY_MS,\n MIN_SELECTABLE_SOURCE_BALANCE_USD,\n} from \"../constants/widget\";\nimport type { DestinationConfig, AssetFilterType } from \"../types\";\nimport type { UserAsset } from \"@avail-project/nexus-core\";\nimport { resolveDepositSourceSelection } from \"../utils\";\n\nfunction parseUsdAmount(value?: string): number {\n if (!value) return 0;\n const parsed = Number.parseFloat(value.replace(/,/g, \"\"));\n if (!Number.isFinite(parsed) || parsed <= 0) return 0;\n return parsed;\n}\n\ninterface PayUsingProps {\n onClick?: () => void;\n selectedChainIds: Set;\n filter: AssetFilterType;\n isManualSelection: boolean;\n amount?: string;\n swapBalance: UserAsset[] | null;\n destination: Pick<\n DestinationConfig,\n \"chainId\" | \"tokenAddress\" | \"tokenSymbol\"\n >;\n}\n\nfunction PayUsing({\n onClick,\n selectedChainIds,\n filter,\n isManualSelection,\n amount,\n swapBalance,\n destination,\n}: PayUsingProps) {\n const [isLoading, setIsLoading] = useState(false);\n const previousAmountRef = useRef(undefined);\n const hasAmount = Boolean(amount && amount.trim() !== \"\" && amount !== \"0\");\n\n useEffect(() => {\n const hadAmount = Boolean(\n previousAmountRef.current && previousAmountRef.current.trim() !== \"\",\n );\n\n if (hasAmount && !hadAmount) {\n setIsLoading(true);\n const timer = setTimeout(() => {\n setIsLoading(false);\n }, LOADING_SKELETON_DELAY_MS);\n return () => clearTimeout(timer);\n }\n\n previousAmountRef.current = amount;\n }, [amount, hasAmount]);\n\n const subtitle = useMemo(() => {\n if (!swapBalance) return \"No tokens selected\";\n const symbolBySourceId = new Map();\n\n swapBalance.forEach((asset) => {\n asset.breakdown?.forEach((breakdown) => {\n const chainId = breakdown.chain?.id;\n const tokenAddress = breakdown.contractAddress;\n if (!chainId || !tokenAddress) return;\n const sourceId = `${tokenAddress}-${chainId}`;\n symbolBySourceId.set(sourceId, breakdown.symbol);\n });\n });\n\n const { sourcePoolIds, selectedSourceIds: prioritizedSourceIds } =\n resolveDepositSourceSelection({\n swapBalance,\n destination,\n filter,\n selectedSourceIds: selectedChainIds,\n isManualSelection,\n minimumBalanceUsd: MIN_SELECTABLE_SOURCE_BALANCE_USD,\n targetAmountUsd: parseUsdAmount(amount),\n });\n\n if (sourcePoolIds.length === 0) return \"No tokens selected\";\n\n const orderedSymbols: string[] = [];\n const seenSymbols = new Set();\n prioritizedSourceIds.forEach((sourceId) => {\n const symbol = symbolBySourceId.get(sourceId);\n if (!symbol || seenSymbols.has(symbol)) return;\n seenSymbols.add(symbol);\n orderedSymbols.push(symbol);\n });\n\n const symbols = orderedSymbols;\n const count = prioritizedSourceIds.length;\n\n let text: string;\n if (count === 0) {\n text = \"No tokens selected\";\n } else if (symbols.length <= 2) {\n text = symbols.join(\", \");\n } else {\n text = `${symbols.slice(0, 2).join(\", \")} +${symbols.length - 2} more`;\n }\n\n return text;\n }, [\n selectedChainIds,\n filter,\n isManualSelection,\n swapBalance,\n destination,\n amount,\n ]);\n\n const renderSubtitle = () => {\n if (!hasAmount) {\n return (\n \n Auto-selected based on amount\n \n );\n }\n\n if (isLoading) {\n return ;\n }\n\n return (\n \n {subtitle}\n \n );\n };\n\n const showEditControls = hasAmount && !isLoading;\n\n return (\n }\n rightIcon={\n showEditControls ? (\n
\n \n Edit\n \n \n
\n ) : undefined\n }\n onClick={showEditControls ? onClick : undefined}\n disabled={!showEditControls}\n roundedBottom={false}\n />\n );\n}\n\nexport default PayUsing;\n", + "content": "import { useMemo, useState, useEffect, useRef } from \"react\";\nimport ButtonCard from \"./button-card\";\nimport { RightChevronIcon, CoinIcon } from \"./icons\";\nimport { Skeleton } from \"../../ui/skeleton\";\nimport {\n LOADING_SKELETON_DELAY_MS,\n MIN_SELECTABLE_SOURCE_BALANCE_USD,\n} from \"../constants/widget\";\nimport type { DestinationConfig, AssetFilterType } from \"../types\";\nimport type { TokenBalance as UserAsset } from \"@avail-project/nexus-sdk-v2\";\nimport { resolveDepositSourceSelection } from \"../utils\";\n\nfunction parseUsdAmount(value?: string): number {\n if (!value) return 0;\n const parsed = Number.parseFloat(value.replace(/,/g, \"\"));\n if (!Number.isFinite(parsed) || parsed <= 0) return 0;\n return parsed;\n}\n\ninterface PayUsingProps {\n onClick?: () => void;\n selectedChainIds: Set;\n filter: AssetFilterType;\n isManualSelection: boolean;\n amount?: string;\n swapBalance: UserAsset[] | null;\n destination: Pick<\n DestinationConfig,\n \"chainId\" | \"tokenAddress\" | \"tokenSymbol\"\n >;\n}\n\nfunction PayUsing({\n onClick,\n selectedChainIds,\n filter,\n isManualSelection,\n amount,\n swapBalance,\n destination,\n}: PayUsingProps) {\n const [isLoading, setIsLoading] = useState(false);\n const previousAmountRef = useRef(undefined);\n const hasAmount = Boolean(amount && amount.trim() !== \"\" && amount !== \"0\");\n\n useEffect(() => {\n const hadAmount = Boolean(\n previousAmountRef.current && previousAmountRef.current.trim() !== \"\",\n );\n\n if (hasAmount && !hadAmount) {\n setIsLoading(true);\n const timer = setTimeout(() => {\n setIsLoading(false);\n }, LOADING_SKELETON_DELAY_MS);\n return () => clearTimeout(timer);\n }\n\n previousAmountRef.current = amount;\n }, [amount, hasAmount]);\n\n const subtitle = useMemo(() => {\n if (!swapBalance) return \"No tokens selected\";\n const symbolBySourceId = new Map();\n\n swapBalance.forEach((asset) => {\n asset.chainBalances?.forEach((breakdown) => {\n const chainId = breakdown.chain?.id;\n const tokenAddress = breakdown.contractAddress;\n if (!chainId || !tokenAddress) return;\n const sourceId = `${tokenAddress}-${chainId}`;\n symbolBySourceId.set(sourceId, breakdown.symbol);\n });\n });\n\n const { sourcePoolIds, selectedSourceIds: prioritizedSourceIds } =\n resolveDepositSourceSelection({\n swapBalance,\n destination,\n filter,\n selectedSourceIds: selectedChainIds,\n isManualSelection,\n minimumBalanceUsd: MIN_SELECTABLE_SOURCE_BALANCE_USD,\n targetAmountUsd: parseUsdAmount(amount),\n });\n\n if (sourcePoolIds.length === 0) return \"No tokens selected\";\n\n const orderedSymbols: string[] = [];\n const seenSymbols = new Set();\n prioritizedSourceIds.forEach((sourceId) => {\n const symbol = symbolBySourceId.get(sourceId);\n if (!symbol || seenSymbols.has(symbol)) return;\n seenSymbols.add(symbol);\n orderedSymbols.push(symbol);\n });\n\n const symbols = orderedSymbols;\n const count = prioritizedSourceIds.length;\n\n let text: string;\n if (count === 0) {\n text = \"No tokens selected\";\n } else if (symbols.length <= 2) {\n text = symbols.join(\", \");\n } else {\n text = `${symbols.slice(0, 2).join(\", \")} +${symbols.length - 2} more`;\n }\n\n return text;\n }, [\n selectedChainIds,\n filter,\n isManualSelection,\n swapBalance,\n destination,\n amount,\n ]);\n\n const renderSubtitle = () => {\n if (!hasAmount) {\n return (\n \n Auto-selected based on amount\n \n );\n }\n\n if (isLoading) {\n return ;\n }\n\n return (\n \n {subtitle}\n \n );\n };\n\n const showEditControls = hasAmount && !isLoading;\n\n return (\n }\n rightIcon={\n showEditControls ? (\n
\n \n Edit\n \n \n
\n ) : undefined\n }\n onClick={showEditControls ? onClick : undefined}\n disabled={!showEditControls}\n roundedBottom={false}\n />\n );\n}\n\nexport default PayUsing;\n", "type": "registry:component", "target": "components/deposit/components/pay-using.tsx" }, @@ -129,7 +129,7 @@ }, { "path": "registry/nexus-elements/deposit/components/token-row.tsx", - "content": "\"use client\";\n\nimport { ChevronDownIcon } from \"./icons\";\nimport type { Token } from \"../types\";\nimport { Checkbox } from \"../../ui/checkbox\";\nimport { usdFormatter } from \"../../common\";\nimport { formatTokenBalance } from \"@avail-project/nexus-core\";\nimport { TOKEN_IMAGES } from \"../constants/assets\";\nimport {\n CHAIN_ITEM_HEIGHT_PX,\n VERTICAL_LINE_TOP_OFFSET_PX,\n} from \"../constants/widget\";\n\ninterface TokenRowProps {\n token: Token;\n disabledChainIds: Set;\n selectedChainIds: Set;\n isExpanded: boolean;\n onToggleExpand: () => void;\n onToggleToken: () => void;\n onToggleChain: (chainId: string) => void;\n isFirst?: boolean;\n isLast?: boolean;\n}\n\nexport function TokenRow({\n token,\n disabledChainIds,\n selectedChainIds,\n isExpanded,\n onToggleExpand,\n onToggleToken,\n onToggleChain,\n isFirst = false,\n isLast = false,\n}: TokenRowProps) {\n const hasMultipleChains = token.chains.length > 1;\n const selectableChains = token.chains.filter(\n (chain) => !disabledChainIds.has(chain.id),\n );\n const hasSelectableChains = selectableChains.length > 0;\n const selectedSelectableChains = selectableChains.filter((chain) =>\n selectedChainIds.has(chain.id),\n ).length;\n const tokenCheckState: boolean | \"indeterminate\" =\n selectedSelectableChains === 0\n ? false\n : selectedSelectableChains === selectableChains.length\n ? true\n : \"indeterminate\";\n const tokenDisabled = !hasSelectableChains;\n\n return (\n \n {/* Main token row */}\n \n
\n e.stopPropagation()}\n />\n
\n \n
\n \n {token.symbol}\n \n \n {token.chainsLabel}\n \n
\n
\n
\n
\n
\n \n {token.usdValue}\n \n \n {token.amount}\n \n
\n {hasMultipleChains ? (\n \n ) : (\n
\n )}\n
\n \n\n {/* Expanded chain list */}\n {isExpanded && hasMultipleChains && (\n
\n
\n {/* Vertical line */}\n \n {/* Chain items */}\n
\n {token.chains.map((chain) => (\n \n {/* Horizontal line */}\n
\n {/* Chain content */}\n
\n
\n {\n if (disabledChainIds.has(chain.id)) return;\n onToggleChain(chain.id);\n }}\n />\n \n {chain.name}\n \n
\n
\n \n {usdFormatter.format(chain.usdValue)}\n \n \n {formatTokenBalance(chain.amount, {\n decimals: token.decimals,\n symbol: token.symbol,\n })}\n \n
\n
\n
\n ))}\n
\n
\n
\n )}\n \n );\n}\n\nexport default TokenRow;\n", + "content": "\"use client\";\n\nimport { ChevronDownIcon } from \"./icons\";\nimport type { Token } from \"../types\";\nimport { Checkbox } from \"../../ui/checkbox\";\nimport { usdFormatter } from \"../../common\";\nimport { formatTokenBalance } from \"@avail-project/nexus-sdk-v2/utils\";\nimport { TOKEN_IMAGES } from \"../constants/assets\";\nimport {\n CHAIN_ITEM_HEIGHT_PX,\n VERTICAL_LINE_TOP_OFFSET_PX,\n} from \"../constants/widget\";\n\ninterface TokenRowProps {\n token: Token;\n disabledChainIds: Set;\n selectedChainIds: Set;\n isExpanded: boolean;\n onToggleExpand: () => void;\n onToggleToken: () => void;\n onToggleChain: (chainId: string) => void;\n isFirst?: boolean;\n isLast?: boolean;\n}\n\nexport function TokenRow({\n token,\n disabledChainIds,\n selectedChainIds,\n isExpanded,\n onToggleExpand,\n onToggleToken,\n onToggleChain,\n isFirst = false,\n isLast = false,\n}: TokenRowProps) {\n const hasMultipleChains = token.chains.length > 1;\n const selectableChains = token.chains.filter(\n (chain) => !disabledChainIds.has(chain.id),\n );\n const hasSelectableChains = selectableChains.length > 0;\n const selectedSelectableChains = selectableChains.filter((chain) =>\n selectedChainIds.has(chain.id),\n ).length;\n const tokenCheckState: boolean | \"indeterminate\" =\n selectedSelectableChains === 0\n ? false\n : selectedSelectableChains === selectableChains.length\n ? true\n : \"indeterminate\";\n const tokenDisabled = !hasSelectableChains;\n\n return (\n \n {/* Main token row */}\n \n
\n e.stopPropagation()}\n />\n
\n \n
\n \n {token.symbol}\n \n \n {token.chainsLabel}\n \n
\n
\n
\n
\n
\n \n {token.usdValue}\n \n \n {token.amount}\n \n
\n {hasMultipleChains ? (\n \n ) : (\n
\n )}\n
\n \n\n {/* Expanded chain list */}\n {isExpanded && hasMultipleChains && (\n
\n
\n {/* Vertical line */}\n \n {/* Chain items */}\n
\n {token.chains.map((chain) => (\n \n {/* Horizontal line */}\n
\n {/* Chain content */}\n
\n
\n {\n if (disabledChainIds.has(chain.id)) return;\n onToggleChain(chain.id);\n }}\n />\n \n {chain.name}\n \n
\n
\n \n {usdFormatter.format(chain.usdValue)}\n \n \n {formatTokenBalance(chain.amount, {\n decimals: token.decimals,\n symbol: token.symbol,\n })}\n \n
\n
\n
\n ))}\n
\n
\n
\n )}\n \n );\n}\n\nexport default TokenRow;\n", "type": "registry:component", "target": "components/deposit/components/token-row.tsx" }, @@ -147,7 +147,7 @@ }, { "path": "registry/nexus-elements/deposit/components/transaction-status-container.tsx", - "content": "\"use client\";\n\nimport { CardContent, CardFooter } from \"../../ui/card\";\nimport WidgetHeader from \"./widget-header\";\nimport { AmountDisplay } from \"./amount-display\";\nimport { TransactionSteps, type SimplifiedStep } from \"./transaction-steps\";\nimport type { DepositWidgetContextValue } from \"../types\";\nimport { useMemo } from \"react\";\nimport { usdFormatter } from \"../../common\";\n\ninterface TransactionStatusContainerProps {\n widget: DepositWidgetContextValue;\n heading?: string;\n onClose?: () => void;\n}\n\nfunction TransferIndicator({ isProcessing }: { isProcessing: boolean }) {\n const baseClasses = \"w-2 h-2 transition-all duration-300\";\n\n if (isProcessing) {\n return (\n <>\n \n \n \n \n \n \n );\n }\n\n return (\n <>\n
\n
\n
\n
\n
\n \n );\n}\n\nconst TransactionStatusContainer = ({\n widget,\n heading,\n onClose,\n}: TransactionStatusContainerProps) => {\n const { steps, confirmationDetails, activeIntent, isProcessing } = widget;\n\n const receiveAmount = confirmationDetails?.receiveAmountAfterSwap ?? \"0\";\n const receiveTokenSymbol = confirmationDetails?.receiveTokenSymbol ?? \"USDC\";\n const destinationChainName =\n confirmationDetails?.destinationChainName ??\n activeIntent?.intent?.destination?.chain?.name ??\n \"destination\";\n const sourceCount = widget.skipSwap\n ? 0 // No source assets when using existing balance\n : (activeIntent?.intent?.sources?.length ?? 0);\n const spendAmountUsd = widget?.confirmationDetails?.amountSpent ?? 0;\n\n // Derive simplified steps from actual SDK events\n const simplifiedSteps = useMemo((): SimplifiedStep[] => {\n // When swap is skipped, only show deposit transaction step\n if (widget.skipSwap) {\n return [\n {\n id: \"deposit-transaction\",\n label: \"Deposit transaction\",\n completed: widget.isSuccess,\n },\n ];\n }\n\n const hasRffId = steps.some((s) => s.step.type === \"RFF_ID\" && s.completed);\n // Use SOURCE_SWAP_HASH for \"Collecting on Source\" step\n const hasSourceSwapHash = steps.some(\n (s) => s.step.type === \"DESTINATION_SWAP_HASH\" && s.completed,\n );\n // Deposit transaction only completes when the entire transaction succeeds\n const isTransactionComplete = widget.isSuccess;\n\n return [\n {\n id: \"intent-verification\",\n label: \"Intent Verification\",\n completed: hasRffId,\n },\n {\n id: \"collecting-on-source\",\n label: \"Collecting on Source\",\n completed: hasSourceSwapHash,\n },\n {\n id: \"deposit-transaction\",\n label: \"Deposit transaction\",\n completed: isTransactionComplete,\n },\n ];\n }, [steps, widget.isSuccess, widget.skipSwap]);\n\n // Calculate progress based on completed steps\n const progress = useMemo(() => {\n const completedCount = simplifiedSteps.filter((s) => s.completed).length;\n const totalSteps = simplifiedSteps.length;\n return Math.round((completedCount / totalSteps) * 100);\n }, [simplifiedSteps]);\n\n const getStatusMessage = () => {\n if (widget.isError && widget.txError) {\n return {widget.txError};\n }\n if (widget.isSuccess) return \"Transaction complete\";\n if (widget.isProcessing) return \"Processing transaction...\";\n return \"Verifying intent\";\n };\n\n return (\n <>\n \n \n
\n
\n
\n \n
\n \n
\n \n
\n
\n \n \n
\n
\n {getStatusMessage()}\n
\n \n
\n \n \n \n );\n};\n\nexport default TransactionStatusContainer;\n", + "content": "\"use client\";\n\nimport { CardContent, CardFooter } from \"../../ui/card\";\nimport WidgetHeader from \"./widget-header\";\nimport { AmountDisplay } from \"./amount-display\";\nimport { TransactionSteps, type SimplifiedStep } from \"./transaction-steps\";\nimport type { DepositWidgetContextValue } from \"../types\";\nimport { useMemo } from \"react\";\nimport { usdFormatter } from \"../../common\";\n\ninterface TransactionStatusContainerProps {\n widget: DepositWidgetContextValue;\n heading?: string;\n onClose?: () => void;\n}\n\nfunction TransferIndicator({ isProcessing }: { isProcessing: boolean }) {\n const baseClasses = \"w-2 h-2 transition-all duration-300\";\n\n if (isProcessing) {\n return (\n <>\n \n \n \n \n \n \n );\n }\n\n return (\n <>\n
\n
\n
\n
\n
\n \n );\n}\n\nconst TransactionStatusContainer = ({\n widget,\n heading,\n onClose,\n}: TransactionStatusContainerProps) => {\n const { steps, confirmationDetails, activeIntent, isProcessing } = widget;\n\n const receiveAmount = confirmationDetails?.receiveAmountAfterSwap ?? \"0\";\n const receiveTokenSymbol = confirmationDetails?.receiveTokenSymbol ?? \"USDC\";\n const destinationChainName =\n confirmationDetails?.destinationChainName ??\n // v2: SwapAndExecuteIntent stores readable info under .swap when swapRequired\n (activeIntent?.intent as any)?.swap?.destination?.chain?.name ??\n \"destination\";\n const sourceCount = widget.skipSwap\n ? 0 // No source assets when using existing balance\n : ((activeIntent?.intent as any)?.swap?.sources?.length ?? 0);\n const spendAmountUsd = widget?.confirmationDetails?.amountSpent ?? 0;\n\n // Derive simplified steps from actual SDK events\n const simplifiedSteps = useMemo((): SimplifiedStep[] => {\n // When swap is skipped, only show deposit transaction step\n if (widget.skipSwap) {\n return [\n {\n id: \"deposit-transaction\",\n label: \"Deposit transaction\",\n completed: widget.isSuccess,\n },\n ];\n }\n\n const hasRffId = steps.some((s) => s.step.type === \"RFF_ID\" && s.completed);\n // Use SOURCE_SWAP_HASH for \"Collecting on Source\" step\n const hasSourceSwapHash = steps.some(\n (s) => s.step.type === \"DESTINATION_SWAP_HASH\" && s.completed,\n );\n // Deposit transaction only completes when the entire transaction succeeds\n const isTransactionComplete = widget.isSuccess;\n\n return [\n {\n id: \"intent-verification\",\n label: \"Intent Verification\",\n completed: hasRffId,\n },\n {\n id: \"collecting-on-source\",\n label: \"Collecting on Source\",\n completed: hasSourceSwapHash,\n },\n {\n id: \"deposit-transaction\",\n label: \"Deposit transaction\",\n completed: isTransactionComplete,\n },\n ];\n }, [steps, widget.isSuccess, widget.skipSwap]);\n\n // Calculate progress based on completed steps\n const progress = useMemo(() => {\n const completedCount = simplifiedSteps.filter((s) => s.completed).length;\n const totalSteps = simplifiedSteps.length;\n return Math.round((completedCount / totalSteps) * 100);\n }, [simplifiedSteps]);\n\n const getStatusMessage = () => {\n if (widget.isError && widget.txError) {\n return {widget.txError};\n }\n if (widget.isSuccess) return \"Transaction complete\";\n if (widget.isProcessing) return \"Processing transaction...\";\n return \"Verifying intent\";\n };\n\n return (\n <>\n \n \n
\n
\n
\n \n
\n \n
\n \n
\n
\n \n \n
\n
\n {getStatusMessage()}\n
\n \n
\n \n \n \n );\n};\n\nexport default TransactionStatusContainer;\n", "type": "registry:component", "target": "components/deposit/components/transaction-status-container.tsx" }, @@ -177,25 +177,25 @@ }, { "path": "registry/nexus-elements/deposit/hooks/use-asset-selection.ts", - "content": "\"use client\";\n\nimport { useState, useCallback, useEffect, useRef } from \"react\";\nimport type { AssetSelectionState, DestinationConfig } from \"../types\";\nimport type { UserAsset } from \"@avail-project/nexus-core\";\nimport { MIN_SELECTABLE_SOURCE_BALANCE_USD } from \"../constants/widget\";\nimport { resolveDepositSourceSelection } from \"../utils\";\n\nfunction parseUsdAmount(value?: string): number {\n if (!value) return 0;\n const parsed = Number.parseFloat(value.replace(/,/g, \"\"));\n if (!Number.isFinite(parsed) || parsed <= 0) return 0;\n return parsed;\n}\n\nfunction areSetsEqual(a: Set, b: Set): boolean {\n if (a.size !== b.size) return false;\n for (const item of a) {\n if (!b.has(item)) return false;\n }\n return true;\n}\n\ninterface SetAssetSelectionOptions {\n markUserModified?: boolean;\n}\n\n/**\n * Creates fresh initial asset selection state\n */\nexport const createInitialAssetSelection = (): AssetSelectionState => ({\n selectedChainIds: new Set(),\n filter: \"all\",\n expandedTokens: new Set(),\n});\n\n/**\n * Hook for managing asset selection state in the deposit widget.\n * Handles selection of tokens/chains for cross-chain swaps.\n */\nexport function useAssetSelection(\n swapBalance: UserAsset[] | null,\n destination: Pick<\n DestinationConfig,\n \"chainId\" | \"tokenAddress\" | \"tokenSymbol\"\n >,\n inputAmount?: string,\n) {\n const [assetSelection, setAssetSelectionState] =\n useState(createInitialAssetSelection);\n const hasUserModifiedSelection = useRef(false);\n const [isManualSelection, setIsManualSelection] = useState(false);\n const previousAmountUsd = useRef(parseUsdAmount(inputAmount));\n\n useEffect(() => {\n const nextAmountUsd = parseUsdAmount(inputAmount);\n\n if (\n hasUserModifiedSelection.current &&\n previousAmountUsd.current !== nextAmountUsd\n ) {\n hasUserModifiedSelection.current = false;\n setIsManualSelection(false);\n setAssetSelectionState(createInitialAssetSelection());\n }\n\n previousAmountUsd.current = nextAmountUsd;\n }, [inputAmount]);\n\n // Auto-select token sources by priority until target amount is covered.\n // This keeps adapting to amount changes until the user manually edits selection.\n useEffect(() => {\n if (swapBalance && !hasUserModifiedSelection.current) {\n const targetAmountUsd = parseUsdAmount(inputAmount);\n const { selectedSourceIds: defaultSelectedSourceIds } =\n resolveDepositSourceSelection({\n swapBalance,\n destination,\n filter: assetSelection.filter,\n selectedSourceIds: assetSelection.selectedChainIds,\n isManualSelection: false,\n minimumBalanceUsd: MIN_SELECTABLE_SOURCE_BALANCE_USD,\n targetAmountUsd,\n });\n\n if (defaultSelectedSourceIds.length === 0) return;\n\n const nextSelection = new Set(defaultSelectedSourceIds);\n if (areSetsEqual(assetSelection.selectedChainIds, nextSelection)) return;\n\n setAssetSelectionState((prev) => ({\n ...prev,\n selectedChainIds: nextSelection,\n expandedTokens:\n prev.expandedTokens.size > 0 ? new Set() : prev.expandedTokens,\n }));\n }\n }, [\n swapBalance,\n destination,\n inputAmount,\n assetSelection.filter,\n assetSelection.selectedChainIds,\n ]);\n\n const setAssetSelection = useCallback(\n (\n update: Partial,\n options?: SetAssetSelectionOptions,\n ) => {\n const nextIsManualSelection = options?.markUserModified ?? true;\n hasUserModifiedSelection.current = nextIsManualSelection;\n setIsManualSelection(nextIsManualSelection);\n setAssetSelectionState((prev) => {\n const nextState = { ...prev, ...update };\n\n return nextState;\n });\n },\n [swapBalance],\n );\n\n const resetAssetSelection = useCallback(() => {\n hasUserModifiedSelection.current = false;\n setIsManualSelection(false);\n setAssetSelectionState(createInitialAssetSelection());\n }, [assetSelection.selectedChainIds, swapBalance]);\n\n return {\n assetSelection,\n isManualSelection,\n setAssetSelection,\n resetAssetSelection,\n };\n}\n", + "content": "\"use client\";\n\nimport { useState, useCallback, useEffect, useRef } from \"react\";\nimport type { AssetSelectionState, DestinationConfig } from \"../types\";\nimport type { TokenBalance } from \"@avail-project/nexus-sdk-v2\";\nimport { MIN_SELECTABLE_SOURCE_BALANCE_USD } from \"../constants/widget\";\nimport { resolveDepositSourceSelection } from \"../utils\";\n\nfunction parseUsdAmount(value?: string): number {\n if (!value) return 0;\n const parsed = Number.parseFloat(value.replace(/,/g, \"\"));\n if (!Number.isFinite(parsed) || parsed <= 0) return 0;\n return parsed;\n}\n\nfunction areSetsEqual(a: Set, b: Set): boolean {\n if (a.size !== b.size) return false;\n for (const item of a) {\n if (!b.has(item)) return false;\n }\n return true;\n}\n\ninterface SetAssetSelectionOptions {\n markUserModified?: boolean;\n}\n\n/**\n * Creates fresh initial asset selection state\n */\nexport const createInitialAssetSelection = (): AssetSelectionState => ({\n selectedChainIds: new Set(),\n filter: \"all\",\n expandedTokens: new Set(),\n});\n\n/**\n * Hook for managing asset selection state in the deposit widget.\n * Handles selection of tokens/chains for cross-chain swaps.\n */\nexport function useAssetSelection(\n swapBalance: TokenBalance[] | null,\n destination: Pick<\n DestinationConfig,\n \"chainId\" | \"tokenAddress\" | \"tokenSymbol\"\n >,\n inputAmount?: string,\n) {\n const [assetSelection, setAssetSelectionState] =\n useState(createInitialAssetSelection);\n const hasUserModifiedSelection = useRef(false);\n const [isManualSelection, setIsManualSelection] = useState(false);\n const previousAmountUsd = useRef(parseUsdAmount(inputAmount));\n\n useEffect(() => {\n const nextAmountUsd = parseUsdAmount(inputAmount);\n\n if (\n hasUserModifiedSelection.current &&\n previousAmountUsd.current !== nextAmountUsd\n ) {\n hasUserModifiedSelection.current = false;\n setIsManualSelection(false);\n setAssetSelectionState(createInitialAssetSelection());\n }\n\n previousAmountUsd.current = nextAmountUsd;\n }, [inputAmount]);\n\n // Auto-select token sources by priority until target amount is covered.\n // This keeps adapting to amount changes until the user manually edits selection.\n useEffect(() => {\n if (swapBalance && !hasUserModifiedSelection.current) {\n const targetAmountUsd = parseUsdAmount(inputAmount);\n const { selectedSourceIds: defaultSelectedSourceIds } =\n resolveDepositSourceSelection({\n swapBalance,\n destination,\n filter: assetSelection.filter,\n selectedSourceIds: assetSelection.selectedChainIds,\n isManualSelection: false,\n minimumBalanceUsd: MIN_SELECTABLE_SOURCE_BALANCE_USD,\n targetAmountUsd,\n });\n\n if (defaultSelectedSourceIds.length === 0) return;\n\n const nextSelection = new Set(defaultSelectedSourceIds);\n if (areSetsEqual(assetSelection.selectedChainIds, nextSelection)) return;\n\n setAssetSelectionState((prev) => ({\n ...prev,\n selectedChainIds: nextSelection,\n expandedTokens:\n prev.expandedTokens.size > 0 ? new Set() : prev.expandedTokens,\n }));\n }\n }, [\n swapBalance,\n destination,\n inputAmount,\n assetSelection.filter,\n assetSelection.selectedChainIds,\n ]);\n\n const setAssetSelection = useCallback(\n (\n update: Partial,\n options?: SetAssetSelectionOptions,\n ) => {\n const nextIsManualSelection = options?.markUserModified ?? true;\n hasUserModifiedSelection.current = nextIsManualSelection;\n setIsManualSelection(nextIsManualSelection);\n setAssetSelectionState((prev) => {\n const nextState = { ...prev, ...update };\n\n return nextState;\n });\n },\n [swapBalance],\n );\n\n const resetAssetSelection = useCallback(() => {\n hasUserModifiedSelection.current = false;\n setIsManualSelection(false);\n setAssetSelectionState(createInitialAssetSelection());\n }, [assetSelection.selectedChainIds, swapBalance]);\n\n return {\n assetSelection,\n isManualSelection,\n setAssetSelection,\n resetAssetSelection,\n };\n}\n", "type": "registry:component", "target": "components/deposit/hooks/use-asset-selection.ts" }, { "path": "registry/nexus-elements/deposit/hooks/use-deposit-computed.ts", - "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport type { DestinationConfig, AssetSelectionState } from \"../types\";\nimport type {\n OnSwapIntentHookData,\n NexusSDK,\n UserAsset,\n} from \"@avail-project/nexus-core\";\nimport { CHAIN_METADATA, formatTokenBalance } from \"@avail-project/nexus-core\";\nimport { usdFormatter } from \"../../common\";\nimport type { SwapSkippedData } from \"./use-deposit-state\";\n\nconst NATIVE_TOKEN_PLACEHOLDER_ADDRESS =\n \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\";\nconst ZERO_ADDRESS = \"0x0000000000000000000000000000000000000000\";\n\nfunction normalizeAddress(address?: string | null): string {\n return (address ?? \"\").toLowerCase();\n}\n\nfunction isNativeLikeAddress(address?: string | null): boolean {\n const normalized = normalizeAddress(address);\n return (\n normalized === NATIVE_TOKEN_PLACEHOLDER_ADDRESS ||\n normalized === ZERO_ADDRESS\n );\n}\n\nfunction resolvePricingSymbol(params: {\n chainId: number;\n contractAddress?: string | null;\n fallbackSymbol: string;\n}): string {\n const { chainId, contractAddress, fallbackSymbol } = params;\n if (!isNativeLikeAddress(contractAddress)) {\n return fallbackSymbol;\n }\n\n const nativeSymbol =\n CHAIN_METADATA[chainId as keyof typeof CHAIN_METADATA]?.nativeCurrency\n ?.symbol;\n return nativeSymbol ?? fallbackSymbol;\n}\n\nfunction parseNonNegativeNumber(value: unknown): number {\n const parsed = Number.parseFloat(String(value));\n if (!Number.isFinite(parsed) || parsed < 0) return 0;\n return parsed;\n}\n\nfunction formatFeeKeyLabel(key: string): string {\n const normalized = key.trim();\n if (!normalized) return \"Fee\";\n\n const knownLabels: Record = {\n caGas: \"CA gas\",\n protocol: \"Protocol\",\n solver: \"Solver\",\n collection: \"Collection\",\n fulfilment: \"Fulfilment\",\n gasSupplied: \"Gas supplied\",\n };\n\n if (knownLabels[normalized]) {\n return knownLabels[normalized];\n }\n\n const spaced = normalized\n .replace(/([a-z])([A-Z])/g, \"$1 $2\")\n .replace(/[_-]/g, \" \");\n return spaced.charAt(0).toUpperCase() + spaced.slice(1);\n}\n\ninterface UseDepositComputedProps {\n swapBalance: UserAsset[] | null;\n assetSelection: AssetSelectionState;\n activeIntent: OnSwapIntentHookData | null;\n destination: DestinationConfig;\n inputAmount: string | undefined;\n exchangeRate: Record | null;\n getFiatValue: (amount: number, symbol: string) => number;\n actualGasFeeUsd: number | null;\n swapSkippedData: SwapSkippedData | null;\n skipSwap: boolean;\n nexusSDK: NexusSDK | null;\n}\n\n/**\n * Available asset item from swap balance\n */\nexport interface AvailableAsset {\n chainId: number;\n tokenAddress: `0x${string}`;\n decimals: number;\n symbol: string;\n balance: string;\n balanceInFiat?: number;\n tokenLogo?: string;\n chainLogo?: string;\n chainName?: string;\n}\n\ntype AssetBreakdownWithOptionalIcon = UserAsset[\"breakdown\"][number] & {\n icon?: string;\n};\n\n/**\n * Hook for computing derived values from deposit widget state.\n * Separates computation logic from main hook for better maintainability.\n */\nexport function useDepositComputed(props: UseDepositComputedProps) {\n const {\n swapBalance,\n assetSelection,\n activeIntent,\n destination,\n inputAmount,\n exchangeRate,\n getFiatValue,\n actualGasFeeUsd,\n swapSkippedData,\n skipSwap,\n nexusSDK,\n } = props;\n\n /**\n * Flatten swap balance into a sorted list of available assets\n */\n const availableAssets = useMemo(() => {\n if (!swapBalance) return [];\n const items: AvailableAsset[] = [];\n\n for (const asset of swapBalance) {\n if (!asset?.breakdown?.length) continue;\n for (const breakdown of asset.breakdown) {\n if (!breakdown?.chain?.id || !breakdown.balance) continue;\n const numericBalance = Number.parseFloat(breakdown.balance);\n if (!Number.isFinite(numericBalance) || numericBalance <= 0) continue;\n const breakdownIcon = (breakdown as AssetBreakdownWithOptionalIcon)\n .icon;\n\n items.push({\n chainId: breakdown.chain.id,\n tokenAddress: breakdown.contractAddress as `0x${string}`,\n decimals: breakdown.decimals ?? asset.decimals,\n symbol: breakdown.symbol,\n balance: breakdown.balance,\n balanceInFiat: breakdown.balanceInFiat,\n tokenLogo: breakdownIcon || \"\",\n chainLogo: breakdown.chain.logo,\n chainName: breakdown.chain.name,\n });\n }\n }\n return items.toSorted(\n (a, b) => (b.balanceInFiat ?? 0) - (a.balanceInFiat ?? 0),\n );\n }, [swapBalance]);\n\n /**\n * Total USD value of selected assets\n */\n const totalSelectedBalance = useMemo(\n () =>\n availableAssets.reduce((sum, asset) => {\n const key = `${asset.tokenAddress}-${asset.chainId}`;\n if (assetSelection.selectedChainIds.has(key)) {\n return sum + (asset.balanceInFiat ?? 0);\n }\n return sum;\n }, 0),\n [availableAssets, assetSelection.selectedChainIds],\n );\n\n /**\n * Total balance across all assets\n */\n const totalBalance = useMemo(() => {\n const balance =\n swapBalance?.reduce(\n (acc, balance) => acc + parseFloat(balance.balance),\n 0,\n ) ?? 0;\n const usdBalance =\n swapBalance?.reduce((acc, balance) => acc + balance.balanceInFiat, 0) ??\n 0;\n return { balance, usdBalance };\n }, [swapBalance]);\n\n /**\n * User's existing balance on destination chain\n */\n const destinationBalance = useMemo(() => {\n if (!nexusSDK || !swapBalance || !destination) return undefined;\n return swapBalance\n ?.flatMap((token) => token.breakdown ?? [])\n ?.find(\n (chain) =>\n chain.chain?.id === destination.chainId &&\n normalizeAddress(chain.contractAddress) ===\n normalizeAddress(destination.tokenAddress),\n );\n }, [swapBalance, nexusSDK, destination]);\n\n /**\n * Confirmation screen details computed from intent or skipped swap data\n */\n const confirmationDetails = useMemo(() => {\n // Handle swap skipped case - compute from swapSkippedData\n if (swapSkippedData && skipSwap) {\n const { destination: destData, gas } = swapSkippedData;\n\n // Format the token amount from raw units\n const rawAmount = Number.parseFloat(destData.amount);\n const tokenAmount = rawAmount / Math.pow(10, destData.token.decimals);\n const receiveAmountUsd = getFiatValue(tokenAmount, destData.token.symbol);\n\n // Format for display\n const receiveAmountAfterSwap = `${tokenAmount.toFixed(2)} ${destData.token.symbol}`;\n\n // Gas fee calculation from swapSkippedData\n const estimatedFeeWei = Number.parseFloat(gas.estimatedFee);\n const estimatedFeeEth = estimatedFeeWei / 1e18;\n const gasFeeUsd = getFiatValue(\n estimatedFeeEth,\n destination.gasTokenSymbol ?? \"ETH\",\n );\n\n return {\n sourceLabel: destination.label ?? \"Deposit\",\n sources: [],\n gasTokenSymbol: destination.gasTokenSymbol,\n estimatedTime: destination.estimatedTime ?? \"~30s\",\n amountSpent: receiveAmountUsd,\n totalFeeUsd: gasFeeUsd,\n receiveTokenSymbol: destData.token.symbol,\n receiveAmountAfterSwapUsd: receiveAmountUsd,\n receiveAmountAfterSwap,\n receiveTokenLogo: destination.tokenLogo,\n receiveTokenChain: destData.chain.id,\n destinationChainName: destData.chain.name,\n };\n }\n\n if (!activeIntent || !nexusSDK) return null;\n\n // Use user's requested amount (from input), not SDK's optimized bridge amount\n const receiveAmountUsd = inputAmount\n ? parseFloat(inputAmount.replace(/,/g, \"\"))\n : 0;\n\n // Convert USD amount to token amount for display\n const tokenExchangeRate = exchangeRate?.[destination.tokenSymbol] ?? 1;\n const safeTokenExchangeRate =\n Number.isFinite(tokenExchangeRate) && tokenExchangeRate > 0\n ? tokenExchangeRate\n : 1;\n const receiveTokenAmount = receiveAmountUsd / safeTokenExchangeRate;\n\n const receiveAmountAfterSwap = formatTokenBalance(\n receiveTokenAmount.toString(),\n {\n symbol: destination.tokenSymbol,\n decimals: destination.tokenDecimals,\n },\n );\n\n // Build sources array from intent sources\n const sources: Array<{\n chainId: number;\n tokenAddress: `0x${string}`;\n decimals: number;\n symbol: string;\n balance: string;\n balanceInFiat?: number;\n tokenLogo?: string;\n chainLogo?: string;\n chainName?: string;\n isDestinationBalance?: boolean;\n }> = [];\n\n activeIntent.intent.sources.forEach((source) => {\n const sourcePricingSymbol = resolvePricingSymbol({\n chainId: source.chain.id,\n contractAddress: source.token.contractAddress,\n fallbackSymbol: source.token.symbol,\n });\n const sourceAmountUsd = parseNonNegativeNumber(source.value);\n\n const matchingAsset = availableAssets.find(\n (asset) =>\n asset.chainId === source.chain.id &&\n (normalizeAddress(asset.tokenAddress) ===\n normalizeAddress(source.token.contractAddress) ||\n asset.symbol.toUpperCase() === source.token.symbol.toUpperCase()),\n );\n\n if (matchingAsset) {\n sources.push({\n ...matchingAsset,\n symbol: sourcePricingSymbol,\n balance: source.amount,\n balanceInFiat: sourceAmountUsd,\n isDestinationBalance: false,\n });\n } else {\n sources.push({\n chainId: source.chain.id,\n tokenAddress: source.token.contractAddress as `0x${string}`,\n decimals: source.token.decimals,\n symbol: sourcePricingSymbol,\n balance: source.amount,\n balanceInFiat: sourceAmountUsd,\n chainLogo: source.chain.logo,\n chainName: source.chain.name,\n isDestinationBalance: false,\n });\n }\n });\n\n // Calculate total spent from cross-chain sources\n const totalAmountSpentUsd = activeIntent.intent.sources?.reduce(\n (acc, source) => acc + parseNonNegativeNumber(source.value),\n 0,\n );\n\n // Get the actual amount arriving on destination (AFTER fees)\n const destinationAmountUsd = parseNonNegativeNumber(\n activeIntent.intent.destination?.value,\n );\n\n const intentFeesAndBuffer = activeIntent.intent.feesAndBuffer;\n const bridgeFeeEntries = Object.entries(intentFeesAndBuffer?.bridge ?? {})\n .filter(([key]) => key !== \"total\")\n .map(([key, value]) => ({\n key,\n amountUsd: parseNonNegativeNumber(value),\n }));\n const bridgeFeeComponentsTotal = bridgeFeeEntries.reduce(\n (sum, fee) => sum + fee.amountUsd,\n 0,\n );\n const bridgeFeeExplicitTotal = parseNonNegativeNumber(\n intentFeesAndBuffer?.bridge?.total,\n );\n\n // SDK-provided bridge total is authoritative; component sum is a fallback.\n const bridgeFeeUsd =\n bridgeFeeExplicitTotal > 0\n ? bridgeFeeExplicitTotal\n : bridgeFeeComponentsTotal;\n\n // Fall back to inferred fee only when intent payload has no feesAndBuffer field.\n const inferredFeeUsd = Math.max(\n 0,\n totalAmountSpentUsd - destinationAmountUsd,\n );\n const hasIntentFeeBreakdown = Boolean(intentFeesAndBuffer);\n const totalFeeUsd = hasIntentFeeBreakdown ? bridgeFeeUsd : inferredFeeUsd;\n\n // Calculate destination balance used\n const usedFromDestinationUsd = Math.max(\n 0,\n receiveAmountUsd - destinationAmountUsd,\n );\n\n if (usedFromDestinationUsd > 0) {\n const usedTokenAmount = usedFromDestinationUsd / safeTokenExchangeRate;\n const chainMeta =\n CHAIN_METADATA[destination.chainId as keyof typeof CHAIN_METADATA];\n\n sources.push({\n chainId: destination.chainId,\n tokenAddress: destination.tokenAddress,\n decimals: destination.tokenDecimals,\n symbol: destination.tokenSymbol,\n balance: usedTokenAmount.toString(),\n balanceInFiat: usedFromDestinationUsd,\n tokenLogo: destination.tokenLogo,\n chainLogo: chainMeta?.logo,\n chainName: chainMeta?.name,\n isDestinationBalance: true,\n });\n }\n\n const actualAmountSpent = totalAmountSpentUsd + usedFromDestinationUsd;\n\n return {\n sourceLabel: destination.label ?? \"Deposit\",\n sources,\n gasTokenSymbol: destination.gasTokenSymbol,\n estimatedTime: destination.estimatedTime ?? \"~30s\",\n amountSpent: actualAmountSpent,\n totalFeeUsd,\n receiveTokenSymbol: destination.tokenSymbol,\n receiveAmountAfterSwapUsd: receiveAmountUsd,\n receiveAmountAfterSwap,\n receiveTokenLogo: destination.tokenLogo,\n receiveTokenChain: destination.chainId,\n destinationChainName: activeIntent.intent.destination?.chain?.name,\n };\n }, [\n activeIntent,\n nexusSDK,\n destination,\n availableAssets,\n inputAmount,\n exchangeRate,\n getFiatValue,\n swapSkippedData,\n skipSwap,\n ]);\n\n /**\n * Gas fee breakdown for display\n */\n const feeBreakdown = useMemo(() => {\n let gasUsd = 0;\n\n // Use actual gas fee from receipt if available\n if (actualGasFeeUsd !== null) {\n gasUsd = actualGasFeeUsd;\n } else if (swapSkippedData && skipSwap) {\n // Use gas from swapSkippedData when swap is skipped\n const { gas } = swapSkippedData;\n const estimatedFeeWei = Number.parseFloat(gas.estimatedFee);\n const estimatedFeeEth = estimatedFeeWei / 1e18;\n gasUsd = getFiatValue(\n estimatedFeeEth,\n destination.gasTokenSymbol ?? \"ETH\",\n );\n } else if (activeIntent?.intent?.destination?.gas) {\n // Otherwise use estimated gas from intent\n const gas = activeIntent.intent.destination.gas;\n gasUsd = parseNonNegativeNumber(gas.value);\n }\n\n const bridgeRaw = activeIntent?.intent?.feesAndBuffer?.bridge;\n const caGasUsd = parseNonNegativeNumber(bridgeRaw?.caGas);\n const gasSuppliedUsd = parseNonNegativeNumber(\n (bridgeRaw as Record | undefined)\n ?.gasSupplied,\n );\n const protocolFeeUsd = parseNonNegativeNumber(bridgeRaw?.protocol);\n const solverFeeUsd = parseNonNegativeNumber(bridgeRaw?.solver);\n\n const hasBridgeBreakdown = Boolean(bridgeRaw);\n const executionBridgeUsd = caGasUsd;\n const gasSponsorshipUsd = hasBridgeBreakdown ? gasSuppliedUsd : 0;\n const executionGasFeeUsd = hasBridgeBreakdown ? executionBridgeUsd : gasUsd;\n\n const bridgeComponents = Object.entries(bridgeRaw ?? {})\n .filter(([key]) => key !== \"total\")\n .map(([key, value]) => ({\n key,\n label: formatFeeKeyLabel(key),\n amountUsd: parseNonNegativeNumber(value),\n }))\n .filter((component) => component.amountUsd > 0);\n\n const bridgeComponentsTotal = bridgeComponents.reduce(\n (sum, component) => sum + component.amountUsd,\n 0,\n );\n const bridgeExplicitTotal = parseNonNegativeNumber(bridgeRaw?.total);\n const bridgeUsd =\n bridgeExplicitTotal > 0 ? bridgeExplicitTotal : bridgeComponentsTotal;\n const knownBridgeRowsUsd =\n gasSponsorshipUsd + executionGasFeeUsd + protocolFeeUsd + solverFeeUsd;\n const otherBridgeFeeUsd = Math.max(0, bridgeUsd - knownBridgeRowsUsd);\n\n // Intent buffer can be displayed for transparency but is not added to total fee.\n const bufferUsd = parseNonNegativeNumber(\n activeIntent?.intent?.feesAndBuffer?.buffer,\n );\n\n const totalFeeUsd =\n executionGasFeeUsd +\n gasSponsorshipUsd +\n protocolFeeUsd +\n solverFeeUsd +\n otherBridgeFeeUsd;\n const gasFormatted = usdFormatter.format(gasUsd);\n\n const sourceValueUsd = (activeIntent?.intent?.sources ?? []).reduce(\n (sum, source) => sum + parseNonNegativeNumber(source.value),\n 0,\n );\n\n const destinationValueUsd = parseNonNegativeNumber(\n activeIntent?.intent?.destination?.value,\n );\n\n const totalSomething = destinationValueUsd + totalFeeUsd + bufferUsd;\n const swapImpactUsd = totalSomething - sourceValueUsd;\n const spendBaseUsd = sourceValueUsd - totalFeeUsd - bufferUsd;\n const swapImpactPercent =\n spendBaseUsd > 0 ? (swapImpactUsd / spendBaseUsd) * 100 : 0;\n\n return {\n totalGasFee: gasUsd,\n gasUsd,\n gasFormatted,\n bridgeUsd,\n bufferUsd,\n totalFeeUsd,\n gasSponsorshipUsd,\n executionGasFeeUsd,\n protocolFeeUsd,\n solverFeeUsd,\n otherBridgeFeeUsd,\n swapImpactUsd,\n swapImpactPercent,\n bridgeComponents,\n };\n }, [\n activeIntent,\n getFiatValue,\n actualGasFeeUsd,\n swapSkippedData,\n skipSwap,\n destination.chainId,\n destination.gasTokenSymbol,\n destination.tokenSymbol,\n ]);\n\n return {\n availableAssets,\n totalSelectedBalance,\n totalBalance,\n destinationBalance,\n confirmationDetails,\n feeBreakdown,\n };\n}\n", + "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport type { DestinationConfig, AssetSelectionState } from \"../types\";\nimport type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type {\n SwapAndExecuteOnIntentHookData,\n TokenBalance,\n ChainBalance,\n} from \"@avail-project/nexus-sdk-v2\";\nimport { formatTokenBalance } from \"@avail-project/nexus-sdk-v2/utils\";\nimport { usdFormatter } from \"../../common\";\nimport type { SwapSkippedData } from \"./use-deposit-state\";\n\ntype NexusClient = ReturnType;\n\nconst NATIVE_TOKEN_PLACEHOLDER_ADDRESS =\n \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\";\nconst ZERO_ADDRESS = \"0x0000000000000000000000000000000000000000\";\n\n// v2: CHAIN_METADATA not exported — hardcode well-known native symbols per chain\nconst NATIVE_SYMBOL_BY_CHAIN: Record = {\n 1: \"ETH\", // Ethereum\n 8453: \"ETH\", // Base\n 42161: \"ETH\", // Arbitrum\n 10: \"ETH\", // Optimism\n 137: \"MATIC\", // Polygon\n 43114: \"AVAX\", // Avalanche\n 534352: \"ETH\", // Scroll\n 56: \"BNB\", // BNB\n 8217: \"KAIA\", // Kaia\n 6342: \"ETH\", // MegaETH\n 10143: \"MON\", // Monad\n 999: \"HYPE\", // HyperEVM\n 5115: \"cBTC\", // Citrea\n 11155111: \"ETH\", // Sepolia\n 84532: \"ETH\", // Base Sepolia\n 421614: \"ETH\", // Arbitrum Sepolia\n 11155420: \"ETH\", // Optimism Sepolia\n 80002: \"MATIC\", // Polygon Amoy\n};\n\nfunction normalizeAddress(address?: string | null): string {\n return (address ?? \"\").toLowerCase();\n}\n\nfunction isNativeLikeAddress(address?: string | null): boolean {\n const normalized = normalizeAddress(address);\n return (\n normalized === NATIVE_TOKEN_PLACEHOLDER_ADDRESS ||\n normalized === ZERO_ADDRESS\n );\n}\n\nfunction resolvePricingSymbol(params: {\n chainId: number;\n contractAddress?: string | null;\n fallbackSymbol: string;\n}): string {\n const { chainId, contractAddress, fallbackSymbol } = params;\n if (!isNativeLikeAddress(contractAddress)) {\n return fallbackSymbol;\n }\n\n const nativeSymbol = NATIVE_SYMBOL_BY_CHAIN[chainId];\n return nativeSymbol ?? fallbackSymbol;\n}\n\nfunction parseNonNegativeNumber(value: unknown): number {\n const parsed = Number.parseFloat(String(value));\n if (!Number.isFinite(parsed) || parsed < 0) return 0;\n return parsed;\n}\n\nfunction formatFeeKeyLabel(key: string): string {\n const normalized = key.trim();\n if (!normalized) return \"Fee\";\n\n const knownLabels: Record = {\n caGas: \"CA gas\",\n protocol: \"Protocol\",\n solver: \"Solver\",\n collection: \"Collection\",\n fulfilment: \"Fulfilment\",\n gasSupplied: \"Gas supplied\",\n };\n\n if (knownLabels[normalized]) {\n return knownLabels[normalized];\n }\n\n const spaced = normalized\n .replace(/([a-z])([A-Z])/g, \"$1 $2\")\n .replace(/[_-]/g, \" \");\n return spaced.charAt(0).toUpperCase() + spaced.slice(1);\n}\n\ninterface UseDepositComputedProps {\n swapBalance: TokenBalance[] | null;\n assetSelection: AssetSelectionState;\n activeIntent: SwapAndExecuteOnIntentHookData | null;\n destination: DestinationConfig;\n inputAmount: string | undefined;\n exchangeRate: Record | null;\n getFiatValue: (amount: number, symbol: string) => number;\n actualGasFeeUsd: number | null;\n swapSkippedData: SwapSkippedData | null;\n skipSwap: boolean;\n nexusSDK: NexusClient | null;\n}\n\n/**\n * v2: SwapAndExecuteIntent wraps the inner SwapIntent under .swap when swapRequired=true.\n * This helper extracts compatible properties for display.\n */\ntype SwapIntentLike = {\n sources?: { chain: { id: number; name: string; logo: string }; token: { contractAddress: string; symbol: string; decimals: number }; amount: string; value?: string }[];\n destination?: { chain?: { id?: number; name?: string }; value?: string; gas?: { value?: string } };\n feesAndBuffer?: { bridge?: Record & { total?: string; caGas?: string; protocol?: string; solver?: string }; buffer?: string };\n};\n\nfunction getSwapIntentLike(\n intent: SwapAndExecuteOnIntentHookData[\"intent\"] | undefined | null,\n): SwapIntentLike | null {\n if (!intent) return null;\n if (intent.swapRequired) {\n // v2: inner SwapIntent is at intent.swap\n const inner = (intent as { swapRequired: true; swap: unknown }).swap;\n return inner as SwapIntentLike;\n }\n // swapRequired=false: no swap data available\n return null;\n}\n\n/**\n * Available asset item from swap balance\n */\nexport interface AvailableAsset {\n chainId: number;\n tokenAddress: `0x${string}`;\n decimals: number;\n symbol: string;\n balance: string;\n value?: string; // v2: USD value as string (from ChainBalance.value)\n balanceInFiat?: number; // kept for legacy callers\n tokenLogo?: string;\n chainLogo?: string;\n chainName?: string;\n}\n\ntype AssetBreakdownWithOptionalIcon = ChainBalance & {\n icon?: string;\n};\n\n/**\n * Hook for computing derived values from deposit widget state.\n * Separates computation logic from main hook for better maintainability.\n */\nexport function useDepositComputed(props: UseDepositComputedProps) {\n const {\n swapBalance,\n assetSelection,\n activeIntent,\n destination,\n inputAmount,\n exchangeRate,\n getFiatValue,\n actualGasFeeUsd,\n swapSkippedData,\n skipSwap,\n nexusSDK,\n } = props;\n\n /**\n * Flatten swap balance into a sorted list of available assets\n */\n const availableAssets = useMemo(() => {\n if (!swapBalance) return [];\n const items: AvailableAsset[] = [];\n\n for (const asset of swapBalance) {\n if (!asset?.chainBalances?.length) continue;\n for (const breakdown of asset.chainBalances) {\n if (!breakdown?.chain?.id || !breakdown.balance) continue;\n const numericBalance = Number.parseFloat(breakdown.balance);\n if (!Number.isFinite(numericBalance) || numericBalance <= 0) continue;\n const breakdownIcon = (breakdown as AssetBreakdownWithOptionalIcon)\n .icon;\n\n items.push({\n chainId: breakdown.chain.id,\n tokenAddress: breakdown.contractAddress as `0x${string}`,\n decimals: breakdown.decimals ?? asset.decimals,\n // v2: breakdown has no .symbol — use parent asset.symbol\n symbol: asset.symbol,\n balance: breakdown.balance,\n value: breakdown.value,\n tokenLogo: breakdownIcon || \"\",\n chainLogo: breakdown.chain.logo,\n chainName: breakdown.chain.name,\n });\n }\n }\n return items.toSorted((a: AvailableAsset, b: AvailableAsset) => (parseFloat(b.value ?? \"0\") ?? 0) - (parseFloat(a.value ?? \"0\") ?? 0),\n );\n }, [swapBalance]);\n\n /**\n * Total USD value of selected assets\n */\n const totalSelectedBalance = useMemo(\n () =>\n availableAssets.reduce((sum, asset) => {\n const key = `${asset.tokenAddress}-${asset.chainId}`;\n if (assetSelection.selectedChainIds.has(key)) {\n return sum + (parseFloat(asset.value ?? \"0\") ?? 0);\n }\n return sum;\n }, 0),\n [availableAssets, assetSelection.selectedChainIds],\n );\n\n /**\n * Total balance across all assets\n */\n const totalBalance = useMemo(() => {\n const balance =\n swapBalance?.reduce(\n (acc, balance) => acc + parseFloat(balance.balance),\n 0,\n ) ?? 0;\n const usdBalance =\n swapBalance?.reduce((acc, balance) => acc + parseFloat(balance.value ?? \"0\"), 0) ??\n 0;\n return { balance, usdBalance };\n }, [swapBalance]);\n\n /**\n * User's existing balance on destination chain\n */\n const destinationBalance = useMemo(() => {\n if (!nexusSDK || !swapBalance || !destination) return undefined;\n return swapBalance\n ?.flatMap((token) => token.chainBalances ?? [])\n ?.find(\n (chain) =>\n chain.chain?.id === destination.chainId &&\n normalizeAddress(chain.contractAddress) ===\n normalizeAddress(destination.tokenAddress),\n );\n }, [swapBalance, nexusSDK, destination]);\n\n /**\n * Confirmation screen details computed from intent or skipped swap data\n */\n const confirmationDetails = useMemo(() => {\n // Handle swap skipped case - compute from swapSkippedData\n if (swapSkippedData && skipSwap) {\n const { destination: destData, gas } = swapSkippedData;\n\n // Format the token amount from raw units\n const rawAmount = Number.parseFloat(destData.amount);\n const tokenAmount = rawAmount / Math.pow(10, destData.token.decimals);\n const receiveAmountUsd = getFiatValue(tokenAmount, destData.token.symbol);\n\n // Format for display\n const receiveAmountAfterSwap = `${tokenAmount.toFixed(2)} ${destData.token.symbol}`;\n\n // Gas fee calculation from swapSkippedData\n const estimatedFeeWei = Number.parseFloat(gas.estimatedFee);\n const estimatedFeeEth = estimatedFeeWei / 1e18;\n const gasFeeUsd = getFiatValue(\n estimatedFeeEth,\n destination.gasTokenSymbol ?? \"ETH\",\n );\n\n return {\n sourceLabel: destination.label ?? \"Deposit\",\n sources: [],\n gasTokenSymbol: destination.gasTokenSymbol,\n estimatedTime: destination.estimatedTime ?? \"~30s\",\n amountSpent: receiveAmountUsd,\n totalFeeUsd: gasFeeUsd,\n receiveTokenSymbol: destData.token.symbol,\n receiveAmountAfterSwapUsd: receiveAmountUsd,\n receiveAmountAfterSwap,\n receiveTokenLogo: destination.tokenLogo,\n receiveTokenChain: destData.chain.id,\n destinationChainName: destData.chain.name,\n };\n }\n\n if (!activeIntent || !nexusSDK) return null;\n\n // Use user's requested amount (from input), not SDK's optimized bridge amount\n const receiveAmountUsd = inputAmount\n ? parseFloat(inputAmount.replace(/,/g, \"\"))\n : 0;\n\n // Convert USD amount to token amount for display\n const tokenExchangeRate = exchangeRate?.[destination.tokenSymbol] ?? 1;\n const safeTokenExchangeRate =\n Number.isFinite(tokenExchangeRate) && tokenExchangeRate > 0\n ? tokenExchangeRate\n : 1;\n const receiveTokenAmount = receiveAmountUsd / safeTokenExchangeRate;\n\n const receiveAmountAfterSwap = formatTokenBalance(\n receiveTokenAmount.toString(),\n {\n symbol: destination.tokenSymbol,\n decimals: destination.tokenDecimals,\n },\n );\n\n // Build sources array from intent sources\n const sources: Array<{\n chainId: number;\n tokenAddress: `0x${string}`;\n decimals: number;\n symbol: string;\n balance: string;\n balanceInFiat?: number;\n tokenLogo?: string;\n chainLogo?: string;\n chainName?: string;\n isDestinationBalance?: boolean;\n }> = [];\n\n // v2: extract inner SwapIntent from SwapAndExecuteIntent\n const swapIntent = getSwapIntentLike(activeIntent.intent);\n\n swapIntent?.sources?.forEach((source) => {\n const sourcePricingSymbol = resolvePricingSymbol({\n chainId: source.chain.id,\n contractAddress: source.token.contractAddress,\n fallbackSymbol: source.token.symbol,\n });\n const sourceAmountUsd = parseNonNegativeNumber(source.value);\n\n const matchingAsset = availableAssets.find(\n (asset) =>\n asset.chainId === source.chain.id &&\n (normalizeAddress(asset.tokenAddress) ===\n normalizeAddress(source.token.contractAddress) ||\n asset.symbol.toUpperCase() === source.token.symbol.toUpperCase()),\n );\n\n if (matchingAsset) {\n sources.push({\n ...matchingAsset,\n symbol: sourcePricingSymbol,\n balance: source.amount,\n balanceInFiat: sourceAmountUsd,\n isDestinationBalance: false,\n });\n } else {\n sources.push({\n chainId: source.chain.id,\n tokenAddress: source.token.contractAddress as `0x${string}`,\n decimals: source.token.decimals,\n symbol: sourcePricingSymbol,\n balance: source.amount,\n balanceInFiat: sourceAmountUsd,\n chainLogo: source.chain.logo,\n chainName: source.chain.name,\n isDestinationBalance: false,\n });\n }\n });\n\n // Calculate total spent from cross-chain sources\n const totalAmountSpentUsd = swapIntent?.sources?.reduce(\n (acc: number, source: { value?: string }) => acc + parseNonNegativeNumber(source.value),\n 0,\n ) ?? 0;\n\n // Get the actual amount arriving on destination (AFTER fees)\n const destinationAmountUsd = parseNonNegativeNumber(\n swapIntent?.destination?.value,\n );\n\n const intentFeesAndBuffer = swapIntent?.feesAndBuffer;\n const bridgeFeeEntries = Object.entries(intentFeesAndBuffer?.bridge ?? {})\n .filter(([key]) => key !== \"total\")\n .map(([key, value]) => ({\n key,\n amountUsd: parseNonNegativeNumber(value),\n }));\n const bridgeFeeComponentsTotal = bridgeFeeEntries.reduce(\n (sum, fee) => sum + fee.amountUsd,\n 0,\n );\n const bridgeFeeExplicitTotal = parseNonNegativeNumber(\n intentFeesAndBuffer?.bridge?.total,\n );\n\n // SDK-provided bridge total is authoritative; component sum is a fallback.\n const bridgeFeeUsd =\n bridgeFeeExplicitTotal > 0\n ? bridgeFeeExplicitTotal\n : bridgeFeeComponentsTotal;\n\n // Fall back to inferred fee only when intent payload has no feesAndBuffer field.\n const inferredFeeUsd = Math.max(\n 0,\n totalAmountSpentUsd - destinationAmountUsd,\n );\n const hasIntentFeeBreakdown = Boolean(intentFeesAndBuffer);\n const totalFeeUsd = hasIntentFeeBreakdown ? bridgeFeeUsd : inferredFeeUsd;\n\n // Calculate destination balance used\n const usedFromDestinationUsd = Math.max(\n 0,\n receiveAmountUsd - destinationAmountUsd,\n );\n\n if (usedFromDestinationUsd > 0) {\n const usedTokenAmount = usedFromDestinationUsd / safeTokenExchangeRate;\n // v2: no CHAIN_METADATA — chainLogo and chainName are not available here\n\n sources.push({\n chainId: destination.chainId,\n tokenAddress: destination.tokenAddress,\n decimals: destination.tokenDecimals,\n symbol: destination.tokenSymbol,\n balance: usedTokenAmount.toString(),\n balanceInFiat: usedFromDestinationUsd,\n tokenLogo: destination.tokenLogo,\n chainLogo: undefined,\n chainName: undefined,\n isDestinationBalance: true,\n });\n }\n\n const actualAmountSpent = totalAmountSpentUsd + usedFromDestinationUsd;\n\n return {\n sourceLabel: destination.label ?? \"Deposit\",\n sources,\n gasTokenSymbol: destination.gasTokenSymbol,\n estimatedTime: destination.estimatedTime ?? \"~30s\",\n amountSpent: actualAmountSpent,\n totalFeeUsd,\n receiveTokenSymbol: destination.tokenSymbol,\n receiveAmountAfterSwapUsd: receiveAmountUsd,\n receiveAmountAfterSwap,\n receiveTokenLogo: destination.tokenLogo,\n receiveTokenChain: destination.chainId,\n destinationChainName: swapIntent?.destination?.chain?.name,\n };\n }, [\n activeIntent,\n nexusSDK,\n destination,\n availableAssets,\n inputAmount,\n exchangeRate,\n getFiatValue,\n swapSkippedData,\n skipSwap,\n ]);\n\n /**\n * Gas fee breakdown for display\n */\n const feeBreakdown = useMemo(() => {\n let gasUsd = 0;\n\n // Use actual gas fee from receipt if available\n if (actualGasFeeUsd !== null) {\n gasUsd = actualGasFeeUsd;\n } else if (swapSkippedData && skipSwap) {\n // Use gas from swapSkippedData when swap is skipped\n const { gas } = swapSkippedData;\n const estimatedFeeWei = Number.parseFloat(gas.estimatedFee);\n const estimatedFeeEth = estimatedFeeWei / 1e18;\n gasUsd = getFiatValue(\n estimatedFeeEth,\n destination.gasTokenSymbol ?? \"ETH\",\n );\n } else if (activeIntent?.intent) {\n // v2: extract inner SwapIntent for gas info\n const swapIntentLike = getSwapIntentLike(activeIntent.intent);\n if (swapIntentLike?.destination?.gas) {\n // Otherwise use estimated gas from intent\n const gas = swapIntentLike.destination.gas;\n gasUsd = parseNonNegativeNumber(gas.value);\n }\n }\n\n const bridgeRaw = (() => {\n if (!activeIntent?.intent) return undefined;\n const swapIntentLike = getSwapIntentLike(activeIntent.intent);\n return swapIntentLike?.feesAndBuffer?.bridge;\n })();\n const caGasUsd = parseNonNegativeNumber(bridgeRaw?.caGas);\n const gasSuppliedUsd = parseNonNegativeNumber(\n (bridgeRaw as Record | undefined)\n ?.gasSupplied,\n );\n const protocolFeeUsd = parseNonNegativeNumber(bridgeRaw?.protocol);\n const solverFeeUsd = parseNonNegativeNumber(bridgeRaw?.solver);\n\n const hasBridgeBreakdown = Boolean(bridgeRaw);\n const executionBridgeUsd = caGasUsd;\n const gasSponsorshipUsd = hasBridgeBreakdown ? gasSuppliedUsd : 0;\n const executionGasFeeUsd = hasBridgeBreakdown ? executionBridgeUsd : gasUsd;\n\n const bridgeComponents = Object.entries(bridgeRaw ?? {})\n .filter(([key]) => key !== \"total\")\n .map(([key, value]) => ({\n key,\n label: formatFeeKeyLabel(key),\n amountUsd: parseNonNegativeNumber(value),\n }))\n .filter((component) => component.amountUsd > 0);\n\n const bridgeComponentsTotal = bridgeComponents.reduce(\n (sum, component) => sum + component.amountUsd,\n 0,\n );\n const bridgeExplicitTotal = parseNonNegativeNumber(bridgeRaw?.total);\n const bridgeUsd =\n bridgeExplicitTotal > 0 ? bridgeExplicitTotal : bridgeComponentsTotal;\n const knownBridgeRowsUsd =\n gasSponsorshipUsd + executionGasFeeUsd + protocolFeeUsd + solverFeeUsd;\n const otherBridgeFeeUsd = Math.max(0, bridgeUsd - knownBridgeRowsUsd);\n\n // Intent buffer can be displayed for transparency but is not added to total fee.\n const bufferUsd = parseNonNegativeNumber(\n (() => {\n if (!activeIntent?.intent) return undefined;\n const swapIntentLike2 = getSwapIntentLike(activeIntent.intent);\n return swapIntentLike2?.feesAndBuffer?.buffer;\n })(),\n );\n\n const totalFeeUsd =\n executionGasFeeUsd +\n gasSponsorshipUsd +\n protocolFeeUsd +\n solverFeeUsd +\n otherBridgeFeeUsd;\n const gasFormatted = usdFormatter.format(gasUsd);\n\n const sourceValueUsd = (() => {\n if (!activeIntent?.intent) return 0;\n const swapIntentLike3 = getSwapIntentLike(activeIntent.intent);\n return (swapIntentLike3?.sources ?? []).reduce(\n (sum: number, source: { value?: string }) => sum + parseNonNegativeNumber(source.value),\n 0,\n );\n })();\n\n const destinationValueUsd = (() => {\n if (!activeIntent?.intent) return 0;\n const swapIntentLike4 = getSwapIntentLike(activeIntent.intent);\n return parseNonNegativeNumber(swapIntentLike4?.destination?.value);\n })();\n\n const totalSomething = destinationValueUsd + totalFeeUsd + bufferUsd;\n const swapImpactUsd = totalSomething - sourceValueUsd;\n const spendBaseUsd = sourceValueUsd - totalFeeUsd - bufferUsd;\n const swapImpactPercent =\n spendBaseUsd > 0 ? (swapImpactUsd / spendBaseUsd) * 100 : 0;\n\n return {\n totalGasFee: gasUsd,\n gasUsd,\n gasFormatted,\n bridgeUsd,\n bufferUsd,\n totalFeeUsd,\n gasSponsorshipUsd,\n executionGasFeeUsd,\n protocolFeeUsd,\n solverFeeUsd,\n otherBridgeFeeUsd,\n swapImpactUsd,\n swapImpactPercent,\n bridgeComponents,\n };\n }, [\n activeIntent,\n getFiatValue,\n actualGasFeeUsd,\n swapSkippedData,\n skipSwap,\n destination.chainId,\n destination.gasTokenSymbol,\n destination.tokenSymbol,\n ]);\n\n return {\n availableAssets,\n totalSelectedBalance,\n totalBalance,\n destinationBalance,\n confirmationDetails,\n feeBreakdown,\n };\n}\n", "type": "registry:component", "target": "components/deposit/hooks/use-deposit-computed.ts" }, { "path": "registry/nexus-elements/deposit/hooks/use-deposit-state.ts", - "content": "\"use client\";\n\nimport { useReducer } from \"react\";\nimport type {\n WidgetStep,\n TransactionStatus,\n DepositInputs,\n NavigationDirection,\n} from \"../types\";\nimport type { OnSwapIntentHookData } from \"@avail-project/nexus-core\";\n\n/**\n * Source swap info collected during transaction execution\n */\nexport interface SourceSwapInfo {\n chainId: number;\n chainName: string;\n explorerUrl: string;\n}\n\n/**\n * Data from SDK when swap is skipped (using existing destination balance)\n */\nexport interface SwapSkippedData {\n destination: {\n amount: string;\n chain: { id: number; name: string };\n token: {\n contractAddress: `0x${string}`;\n decimals: number;\n symbol: string;\n };\n };\n input: {\n amount: string;\n token: {\n contractAddress: `0x${string}`;\n decimals: number;\n symbol: string;\n };\n };\n gas: {\n required: string;\n price: string;\n estimatedFee: string;\n };\n}\n\n/**\n * Core deposit widget state\n */\nexport interface DepositState {\n step: WidgetStep;\n inputs: DepositInputs;\n status: TransactionStatus;\n explorerUrls: {\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n };\n sourceSwaps: SourceSwapInfo[];\n nexusIntentUrl: string | null;\n depositTxHash: string | null;\n actualGasFeeUsd: number | null;\n error: string | null;\n lastResult: unknown;\n navigationDirection: NavigationDirection;\n simulation: {\n swapIntent: OnSwapIntentHookData;\n } | null;\n simulationLoading: boolean;\n receiveAmount: string | null;\n skipSwap: boolean;\n intentReady: boolean;\n swapSkippedData: SwapSkippedData | null;\n}\n\n/**\n * Action types for state reducer\n */\nexport type DepositAction =\n | {\n type: \"setStep\";\n payload: { step: WidgetStep; direction: NavigationDirection };\n }\n | { type: \"setInputs\"; payload: Partial }\n | { type: \"setStatus\"; payload: TransactionStatus }\n | {\n type: \"setExplorerUrls\";\n payload: Partial;\n }\n | { type: \"setError\"; payload: string | null }\n | { type: \"setLastResult\"; payload: unknown }\n | {\n type: \"setSimulation\";\n payload: {\n swapIntent: OnSwapIntentHookData;\n };\n }\n | { type: \"setSimulationLoading\"; payload: boolean }\n | { type: \"setReceiveAmount\"; payload: string | null }\n | { type: \"setSkipSwap\"; payload: boolean }\n | { type: \"setIntentReady\"; payload: boolean }\n | { type: \"setSwapSkippedData\"; payload: SwapSkippedData | null }\n | { type: \"addSourceSwap\"; payload: SourceSwapInfo }\n | { type: \"setNexusIntentUrl\"; payload: string | null }\n | { type: \"setDepositTxHash\"; payload: string | null }\n | { type: \"setActualGasFeeUsd\"; payload: number | null }\n | { type: \"reset\" };\n\n/**\n * Step history for back navigation\n */\nexport const STEP_HISTORY: Record = {\n amount: null,\n confirmation: \"amount\",\n \"transaction-status\": null,\n \"transaction-complete\": null,\n \"transaction-failed\": null,\n \"asset-selection\": \"amount\",\n} as const;\n\n/**\n * Creates fresh initial state\n */\nexport const createInitialState = (): DepositState => ({\n step: \"amount\",\n inputs: {\n amount: undefined,\n selectedToken: \"USDC\",\n },\n status: \"idle\",\n explorerUrls: {\n sourceExplorerUrl: null,\n destinationExplorerUrl: null,\n },\n sourceSwaps: [],\n nexusIntentUrl: null,\n depositTxHash: null,\n actualGasFeeUsd: null,\n error: null,\n lastResult: null,\n navigationDirection: null,\n simulation: null,\n simulationLoading: false,\n receiveAmount: null,\n skipSwap: false,\n intentReady: false,\n swapSkippedData: null,\n});\n\n/**\n * State reducer for deposit widget\n */\nfunction depositReducer(state: DepositState, action: DepositAction): DepositState {\n switch (action.type) {\n case \"setStep\":\n return {\n ...state,\n step: action.payload.step,\n navigationDirection: action.payload.direction,\n };\n case \"setInputs\": {\n const newInputs = { ...state.inputs, ...action.payload };\n let newStatus = state.status;\n if (\n state.status === \"idle\" &&\n newInputs.amount &&\n Number.parseFloat(newInputs.amount) > 0\n ) {\n newStatus = \"previewing\";\n }\n if (\n state.status === \"previewing\" &&\n (!newInputs.amount || Number.parseFloat(newInputs.amount) <= 0)\n ) {\n newStatus = \"idle\";\n }\n // Clear error when user changes inputs\n return { ...state, inputs: newInputs, status: newStatus, error: null };\n }\n case \"setStatus\":\n return { ...state, status: action.payload };\n case \"setExplorerUrls\":\n return {\n ...state,\n explorerUrls: { ...state.explorerUrls, ...action.payload },\n };\n case \"setError\":\n return { ...state, error: action.payload };\n case \"setLastResult\":\n return { ...state, lastResult: action.payload };\n case \"setSimulation\":\n return {\n ...state,\n simulation: action.payload,\n };\n case \"setSimulationLoading\":\n return { ...state, simulationLoading: action.payload };\n case \"setReceiveAmount\":\n return { ...state, receiveAmount: action.payload };\n case \"setSkipSwap\":\n return { ...state, skipSwap: action.payload };\n case \"setIntentReady\":\n return { ...state, intentReady: action.payload };\n case \"setSwapSkippedData\":\n return { ...state, swapSkippedData: action.payload };\n case \"addSourceSwap\":\n return { ...state, sourceSwaps: [...state.sourceSwaps, action.payload] };\n case \"setNexusIntentUrl\":\n return { ...state, nexusIntentUrl: action.payload };\n case \"setDepositTxHash\":\n return { ...state, depositTxHash: action.payload };\n case \"setActualGasFeeUsd\":\n return { ...state, actualGasFeeUsd: action.payload };\n case \"reset\":\n return createInitialState();\n default:\n return state;\n }\n}\n\n/**\n * Hook for managing deposit widget state via reducer\n */\nexport function useDepositState() {\n const [state, dispatch] = useReducer(\n depositReducer,\n undefined,\n createInitialState\n );\n\n return { state, dispatch };\n}\n", + "content": "\"use client\";\n\nimport { useReducer } from \"react\";\nimport type {\n WidgetStep,\n TransactionStatus,\n DepositInputs,\n NavigationDirection,\n} from \"../types\";\nimport type { SwapAndExecuteOnIntentHookData } from \"@avail-project/nexus-sdk-v2\";\n\n/**\n * Source swap info collected during transaction execution\n */\nexport interface SourceSwapInfo {\n chainId: number;\n chainName: string;\n explorerUrl: string;\n}\n\n/**\n * Data from SDK when swap is skipped (using existing destination balance)\n */\nexport interface SwapSkippedData {\n destination: {\n amount: string;\n chain: { id: number; name: string };\n token: {\n contractAddress: `0x${string}`;\n decimals: number;\n symbol: string;\n };\n };\n input: {\n amount: string;\n token: {\n contractAddress: `0x${string}`;\n decimals: number;\n symbol: string;\n };\n };\n gas: {\n required: string;\n price: string;\n estimatedFee: string;\n };\n}\n\n/**\n * Core deposit widget state\n */\nexport interface DepositState {\n step: WidgetStep;\n inputs: DepositInputs;\n status: TransactionStatus;\n explorerUrls: {\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n };\n sourceSwaps: SourceSwapInfo[];\n nexusIntentUrl: string | null;\n depositTxHash: string | null;\n actualGasFeeUsd: number | null;\n error: string | null;\n lastResult: unknown;\n navigationDirection: NavigationDirection;\n simulation: {\n swapIntent: SwapAndExecuteOnIntentHookData;\n } | null;\n simulationLoading: boolean;\n receiveAmount: string | null;\n skipSwap: boolean;\n intentReady: boolean;\n swapSkippedData: SwapSkippedData | null;\n}\n\n/**\n * Action types for state reducer\n */\nexport type DepositAction =\n | {\n type: \"setStep\";\n payload: { step: WidgetStep; direction: NavigationDirection };\n }\n | { type: \"setInputs\"; payload: Partial }\n | { type: \"setStatus\"; payload: TransactionStatus }\n | {\n type: \"setExplorerUrls\";\n payload: Partial;\n }\n | { type: \"setError\"; payload: string | null }\n | { type: \"setLastResult\"; payload: unknown }\n | {\n type: \"setSimulation\";\n payload: {\n swapIntent: SwapAndExecuteOnIntentHookData;\n };\n }\n | { type: \"setSimulationLoading\"; payload: boolean }\n | { type: \"setReceiveAmount\"; payload: string | null }\n | { type: \"setSkipSwap\"; payload: boolean }\n | { type: \"setIntentReady\"; payload: boolean }\n | { type: \"setSwapSkippedData\"; payload: SwapSkippedData | null }\n | { type: \"addSourceSwap\"; payload: SourceSwapInfo }\n | { type: \"setNexusIntentUrl\"; payload: string | null }\n | { type: \"setDepositTxHash\"; payload: string | null }\n | { type: \"setActualGasFeeUsd\"; payload: number | null }\n | { type: \"reset\" };\n\n/**\n * Step history for back navigation\n */\nexport const STEP_HISTORY: Record = {\n amount: null,\n confirmation: \"amount\",\n \"transaction-status\": null,\n \"transaction-complete\": null,\n \"transaction-failed\": null,\n \"asset-selection\": \"amount\",\n} as const;\n\n/**\n * Creates fresh initial state\n */\nexport const createInitialState = (): DepositState => ({\n step: \"amount\",\n inputs: {\n amount: undefined,\n selectedToken: \"USDC\",\n },\n status: \"idle\",\n explorerUrls: {\n sourceExplorerUrl: null,\n destinationExplorerUrl: null,\n },\n sourceSwaps: [],\n nexusIntentUrl: null,\n depositTxHash: null,\n actualGasFeeUsd: null,\n error: null,\n lastResult: null,\n navigationDirection: null,\n simulation: null,\n simulationLoading: false,\n receiveAmount: null,\n skipSwap: false,\n intentReady: false,\n swapSkippedData: null,\n});\n\n/**\n * State reducer for deposit widget\n */\nfunction depositReducer(state: DepositState, action: DepositAction): DepositState {\n switch (action.type) {\n case \"setStep\":\n return {\n ...state,\n step: action.payload.step,\n navigationDirection: action.payload.direction,\n };\n case \"setInputs\": {\n const newInputs = { ...state.inputs, ...action.payload };\n let newStatus = state.status;\n if (\n state.status === \"idle\" &&\n newInputs.amount &&\n Number.parseFloat(newInputs.amount) > 0\n ) {\n newStatus = \"previewing\";\n }\n if (\n state.status === \"previewing\" &&\n (!newInputs.amount || Number.parseFloat(newInputs.amount) <= 0)\n ) {\n newStatus = \"idle\";\n }\n // Clear error when user changes inputs\n return { ...state, inputs: newInputs, status: newStatus, error: null };\n }\n case \"setStatus\":\n return { ...state, status: action.payload };\n case \"setExplorerUrls\":\n return {\n ...state,\n explorerUrls: { ...state.explorerUrls, ...action.payload },\n };\n case \"setError\":\n return { ...state, error: action.payload };\n case \"setLastResult\":\n return { ...state, lastResult: action.payload };\n case \"setSimulation\":\n return {\n ...state,\n simulation: action.payload,\n };\n case \"setSimulationLoading\":\n return { ...state, simulationLoading: action.payload };\n case \"setReceiveAmount\":\n return { ...state, receiveAmount: action.payload };\n case \"setSkipSwap\":\n return { ...state, skipSwap: action.payload };\n case \"setIntentReady\":\n return { ...state, intentReady: action.payload };\n case \"setSwapSkippedData\":\n return { ...state, swapSkippedData: action.payload };\n case \"addSourceSwap\":\n return { ...state, sourceSwaps: [...state.sourceSwaps, action.payload] };\n case \"setNexusIntentUrl\":\n return { ...state, nexusIntentUrl: action.payload };\n case \"setDepositTxHash\":\n return { ...state, depositTxHash: action.payload };\n case \"setActualGasFeeUsd\":\n return { ...state, actualGasFeeUsd: action.payload };\n case \"reset\":\n return createInitialState();\n default:\n return state;\n }\n}\n\n/**\n * Hook for managing deposit widget state via reducer\n */\nexport function useDepositState() {\n const [state, dispatch] = useReducer(\n depositReducer,\n undefined,\n createInitialState\n );\n\n return { state, dispatch };\n}\n", "type": "registry:component", "target": "components/deposit/hooks/use-deposit-state.ts" }, { "path": "registry/nexus-elements/deposit/hooks/use-deposit-widget.ts", - "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type {\n WidgetStep,\n DepositWidgetContextValue,\n DepositInputs,\n DestinationConfig,\n} from \"../types\";\nimport {\n ERROR_CODES,\n NEXUS_EVENTS,\n CHAIN_METADATA,\n type SwapStepType,\n type ExecuteParams,\n type OnSwapIntentHookData,\n type SwapAndExecuteParams,\n type SwapAndExecuteResult,\n parseUnits,\n} from \"@avail-project/nexus-core\";\nimport {\n SWAP_EXPECTED_STEPS,\n useNexusError,\n usePolling,\n useStopwatch,\n useTransactionSteps,\n} from \"../../common\";\nimport { type Address, type Hex, formatEther } from \"viem\";\nimport { useAccount } from \"wagmi\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport {\n MIN_SELECTABLE_SOURCE_BALANCE_USD,\n SIMULATION_POLL_INTERVAL_MS,\n} from \"../constants/widget\";\n\n// Import extracted hooks\nimport {\n useDepositState,\n STEP_HISTORY,\n type SwapSkippedData,\n} from \"./use-deposit-state\";\nimport { useAssetSelection } from \"./use-asset-selection\";\nimport { useDepositComputed } from \"./use-deposit-computed\";\nimport { resolveDepositSourceSelection } from \"../utils\";\n\ninterface UseDepositProps {\n executeDeposit: (\n tokenSymbol: string,\n tokenAddress: `0x${string}`,\n amount: bigint,\n chainId: number,\n user: Address,\n ) => Omit;\n destination: DestinationConfig;\n onSuccess?: () => void;\n onError?: (error: string) => void;\n}\n\nfunction parseUsdAmount(value?: string): number {\n if (!value) return 0;\n const parsed = Number.parseFloat(value.replace(/,/g, \"\"));\n if (!Number.isFinite(parsed) || parsed <= 0) return 0;\n return parsed;\n}\n\nfunction summarizeIntentSources(\n intentSources: OnSwapIntentHookData[\"intent\"][\"sources\"] | undefined,\n) {\n return (intentSources ?? []).map((source) => ({\n chainId: source.chain.id,\n chainName: source.chain.name,\n tokenAddress: source.token.contractAddress,\n tokenSymbol: source.token.symbol,\n amount: source.amount,\n }));\n}\n\n/**\n * Main deposit widget hook that orchestrates state, SDK integration,\n * and computed values via smaller focused hooks.\n */\nexport function useDepositWidget(\n props: UseDepositProps,\n): DepositWidgetContextValue {\n const { executeDeposit, destination, onSuccess, onError } = props;\n\n // External dependencies\n const {\n nexusSDK,\n swapIntent,\n swapBalance,\n fetchSwapBalance,\n getFiatValue,\n exchangeRate,\n resolveTokenUsdRate,\n } = useNexus();\n const { address } = useAccount();\n const handleNexusError = useNexusError();\n\n // Core state management\n const { state, dispatch } = useDepositState();\n const [pollingEnabled, setPollingEnabled] = useState(false);\n\n // Asset selection state\n const {\n assetSelection,\n isManualSelection,\n setAssetSelection,\n resetAssetSelection,\n } = useAssetSelection(swapBalance, destination, state.inputs.amount);\n\n // Refs for tracking\n const hasAutoSelected = useRef(false);\n const initialSimulationDone = useRef(false);\n const determiningSwapComplete = useRef(false);\n const lastSimulationTime = useRef(0);\n const suppressNextWidgetPreviewCancelError = useRef(false);\n\n const denyActiveSwapIntent = useCallback(\n (options?: { suppressUiError?: boolean }) => {\n const activeSwapIntent =\n swapIntent.current ?? state.simulation?.swapIntent;\n\n if (options?.suppressUiError && activeSwapIntent) {\n suppressNextWidgetPreviewCancelError.current = true;\n }\n\n if (!activeSwapIntent) {\n return;\n }\n\n try {\n activeSwapIntent.deny();\n } catch (error) {\n suppressNextWidgetPreviewCancelError.current = false;\n console.error(\"Failed to deny active swap intent\", error);\n } finally {\n swapIntent.current = null;\n }\n },\n [swapIntent, state.simulation],\n );\n\n // Transaction steps tracking\n const {\n seed,\n onStepComplete,\n reset: resetSteps,\n steps,\n } = useTransactionSteps();\n\n // Stopwatch for timing\n const stopwatch = useStopwatch({\n running:\n state.status === \"executing\" ||\n (state.status === \"previewing\" && determiningSwapComplete.current),\n intervalMs: 100,\n });\n\n // Derived state\n const isProcessing = state.status === \"executing\";\n const isSuccess = state.status === \"success\";\n const isError = state.status === \"error\";\n const activeIntent = state.simulation?.swapIntent ?? swapIntent.current;\n\n // Computed values\n const {\n availableAssets,\n totalSelectedBalance,\n totalBalance,\n confirmationDetails,\n feeBreakdown,\n } = useDepositComputed({\n swapBalance,\n assetSelection,\n activeIntent,\n destination,\n inputAmount: state.inputs.amount,\n exchangeRate,\n getFiatValue,\n actualGasFeeUsd: state.actualGasFeeUsd,\n swapSkippedData: state.swapSkippedData,\n skipSwap: state.skipSwap,\n nexusSDK,\n });\n\n // Action callbacks\n const setInputs = useCallback(\n (next: Partial) => {\n dispatch({ type: \"setInputs\", payload: next });\n },\n [dispatch],\n );\n\n const setTxError = useCallback(\n (error: string | null) => {\n dispatch({ type: \"setError\", payload: error });\n },\n [dispatch],\n );\n\n /**\n * Start the swap and execute flow with the SDK\n */\n const start = useCallback(\n (inputs: SwapAndExecuteParams, targetAmountUsd?: number) => {\n if (!nexusSDK || !inputs || isProcessing) return;\n\n seed(SWAP_EXPECTED_STEPS);\n const requiredAmountUsd =\n targetAmountUsd ?? parseUsdAmount(state.inputs.amount);\n const { sourcePoolIds, selectedSourceIds, fromSources } =\n resolveDepositSourceSelection({\n swapBalance,\n destination,\n filter: assetSelection.filter,\n selectedSourceIds: assetSelection.selectedChainIds,\n isManualSelection,\n minimumBalanceUsd: MIN_SELECTABLE_SOURCE_BALANCE_USD,\n targetAmountUsd: requiredAmountUsd,\n });\n\n if (fromSources.length === 0) {\n const message =\n \"No eligible source balances found. A minimum source balance of $1.00 is required.\";\n dispatch({ type: \"setError\", payload: message });\n dispatch({ type: \"setStatus\", payload: \"error\" });\n onError?.(message);\n return;\n }\n\n const inputsWithSources = {\n ...inputs,\n fromSources,\n };\n nexusSDK\n .swapAndExecute(inputsWithSources, {\n onEvent: (event) => {\n if (event.name === NEXUS_EVENTS.SWAP_STEP_COMPLETE) {\n const step = event.args as SwapStepType & {\n completed?: boolean;\n data?: SwapSkippedData;\n };\n\n // Handle SWAP_SKIPPED - go directly to transaction-status\n if (step?.type === \"SWAP_SKIPPED\") {\n dispatch({ type: \"setSkipSwap\", payload: true });\n dispatch({\n type: \"setSwapSkippedData\",\n payload: step.data ?? null,\n });\n dispatch({ type: \"setStatus\", payload: \"executing\" });\n dispatch({\n type: \"setStep\",\n payload: { step: \"transaction-status\", direction: \"forward\" },\n });\n stopwatch.start();\n }\n\n if (step?.type === \"DETERMINING_SWAP\" && step?.completed) {\n determiningSwapComplete.current = true;\n stopwatch.start();\n dispatch({ type: \"setIntentReady\", payload: true });\n }\n onStepComplete(step);\n }\n },\n })\n .then((data: SwapAndExecuteResult) => {\n suppressNextWidgetPreviewCancelError.current = false;\n\n // Extract source swaps from the result\n const sourceSwapsFromResult = data.swapResult?.sourceSwaps ?? [];\n sourceSwapsFromResult.forEach((sourceSwap) => {\n const chainMeta =\n CHAIN_METADATA[sourceSwap.chainId as keyof typeof CHAIN_METADATA];\n const baseUrl = chainMeta?.blockExplorerUrls?.[0] ?? \"\";\n const explorerUrl = baseUrl\n ? `${baseUrl}/tx/${sourceSwap.txHash}`\n : \"\";\n dispatch({\n type: \"addSourceSwap\",\n payload: {\n chainId: sourceSwap.chainId,\n chainName: chainMeta?.name ?? `Chain ${sourceSwap.chainId}`,\n explorerUrl,\n },\n });\n });\n\n // Set explorer URLs from the result\n if (sourceSwapsFromResult.length > 0) {\n const firstSourceSwap = sourceSwapsFromResult[0];\n const chainMeta =\n CHAIN_METADATA[\n firstSourceSwap.chainId as keyof typeof CHAIN_METADATA\n ];\n const baseUrl = chainMeta?.blockExplorerUrls?.[0] ?? \"\";\n const sourceExplorerUrl = baseUrl\n ? `${baseUrl}/tx/${firstSourceSwap.txHash}`\n : \"\";\n dispatch({\n type: \"setExplorerUrls\",\n payload: { sourceExplorerUrl },\n });\n }\n\n // Destination explorer URL\n const destChainMeta =\n CHAIN_METADATA[destination.chainId as keyof typeof CHAIN_METADATA];\n const destBaseUrl = destChainMeta?.blockExplorerUrls?.[0] ?? \"\";\n const destinationExplorerUrl =\n data.swapResult?.explorerURL ??\n (data.executeResponse?.txHash && destBaseUrl\n ? `${destBaseUrl}/tx/${data.executeResponse.txHash}`\n : null);\n\n if (destinationExplorerUrl) {\n dispatch({\n type: \"setExplorerUrls\",\n payload: { destinationExplorerUrl },\n });\n }\n\n // Store Nexus intent URL and deposit tx hash\n dispatch({\n type: \"setNexusIntentUrl\",\n payload: data.swapResult?.explorerURL ?? null,\n });\n dispatch({\n type: \"setDepositTxHash\",\n payload: data.executeResponse?.txHash ?? null,\n });\n\n // Calculate actual gas fee from receipt\n const receipt = data.executeResponse?.receipt;\n if (receipt?.gasUsed && receipt?.effectiveGasPrice) {\n const gasUsed = BigInt(receipt.gasUsed);\n const effectiveGasPrice = BigInt(receipt.effectiveGasPrice);\n const gasCostWei = gasUsed * effectiveGasPrice;\n const gasCostNative = parseFloat(formatEther(gasCostWei));\n const gasTokenSymbol = destination.gasTokenSymbol ?? \"ETH\";\n const gasCostUsd = getFiatValue(gasCostNative, gasTokenSymbol);\n dispatch({\n type: \"setActualGasFeeUsd\",\n payload: gasCostUsd,\n });\n }\n\n dispatch({\n type: \"setReceiveAmount\",\n payload: swapIntent.current?.intent?.destination?.amount ?? \"\",\n });\n onSuccess?.();\n dispatch({ type: \"setStatus\", payload: \"success\" });\n dispatch({\n type: \"setStep\",\n payload: { step: \"transaction-complete\", direction: \"forward\" },\n });\n })\n .catch((error) => {\n const { code, message } = handleNexusError(error);\n const isUserRejectedError =\n code === ERROR_CODES.USER_DENIED_INTENT ||\n code === ERROR_CODES.USER_DENIED_INTENT_SIGNATURE ||\n code === ERROR_CODES.USER_DENIED_ALLOWANCE ||\n code === ERROR_CODES.USER_DENIED_SIWE_SIGNATURE;\n const shouldSuppressWidgetError =\n suppressNextWidgetPreviewCancelError.current && isUserRejectedError;\n\n suppressNextWidgetPreviewCancelError.current = false;\n\n if (shouldSuppressWidgetError) {\n onError?.(message);\n return;\n }\n\n dispatch({ type: \"setError\", payload: message });\n dispatch({ type: \"setStatus\", payload: \"error\" });\n\n if (initialSimulationDone.current) {\n dispatch({\n type: \"setStep\",\n payload: { step: \"transaction-failed\", direction: \"forward\" },\n });\n } else {\n dispatch({\n type: \"setStep\",\n payload: { step: \"amount\", direction: \"backward\" },\n });\n }\n onError?.(message);\n })\n .finally(async () => {\n await fetchSwapBalance();\n });\n },\n [\n nexusSDK,\n isProcessing,\n seed,\n onStepComplete,\n swapIntent,\n onSuccess,\n onError,\n handleNexusError,\n assetSelection.selectedChainIds,\n assetSelection.filter,\n isManualSelection,\n swapBalance,\n destination,\n getFiatValue,\n fetchSwapBalance,\n dispatch,\n stopwatch,\n state.inputs.amount,\n ],\n );\n\n /**\n * Handle amount input continue - starts simulation\n */\n const beginAmountSimulation = useCallback(\n async (totalAmountUsd: number) => {\n if (!nexusSDK) {\n dispatch({\n type: \"setError\",\n payload: \"Nexus SDK is not initialized.\",\n });\n dispatch({ type: \"setStatus\", payload: \"error\" });\n return false;\n }\n if (!address) {\n dispatch({\n type: \"setError\",\n payload: \"Connect your wallet to continue.\",\n });\n dispatch({ type: \"setStatus\", payload: \"error\" });\n return false;\n }\n const destinationRate = await resolveTokenUsdRate(\n destination.tokenSymbol,\n );\n if (\n !destinationRate ||\n !Number.isFinite(destinationRate) ||\n destinationRate <= 0\n ) {\n dispatch({\n type: \"setError\",\n payload: `Unable to fetch pricing for ${destination.tokenSymbol}. Please try again.`,\n });\n dispatch({ type: \"setStatus\", payload: \"error\" });\n return false;\n }\n\n // Reset state and refs for a fresh simulation\n dispatch({ type: \"setError\", payload: null });\n dispatch({ type: \"setIntentReady\", payload: false });\n initialSimulationDone.current = false;\n determiningSwapComplete.current = false;\n denyActiveSwapIntent();\n\n const tokenAmount = totalAmountUsd / destinationRate;\n const tokenAmountStr = tokenAmount.toFixed(destination.tokenDecimals);\n const parsed = parseUnits(tokenAmountStr, destination.tokenDecimals);\n\n const executeParams = executeDeposit(\n destination.tokenSymbol,\n destination.tokenAddress,\n parsed,\n destination.chainId,\n address,\n );\n\n const newInputs: SwapAndExecuteParams = {\n toChainId: destination.chainId,\n toTokenAddress: destination.tokenAddress,\n toAmount: parsed,\n execute: {\n to: executeParams.to,\n value: executeParams.value,\n data: executeParams.data,\n gasPrice: executeParams.gasPrice,\n tokenApproval: executeParams.tokenApproval as {\n token: `0x${string}`;\n amount: bigint;\n spender: Hex;\n },\n gas: BigInt(400_000),\n },\n };\n\n dispatch({\n type: \"setInputs\",\n payload: { amount: totalAmountUsd.toString() },\n });\n dispatch({ type: \"setStatus\", payload: \"simulation-loading\" });\n dispatch({ type: \"setSimulationLoading\", payload: true });\n start(newInputs, totalAmountUsd);\n return true;\n },\n [\n nexusSDK,\n address,\n resolveTokenUsdRate,\n destination,\n executeDeposit,\n start,\n denyActiveSwapIntent,\n dispatch,\n ],\n );\n\n const handleAmountContinue = useCallback(\n (totalAmountUsd: number) => {\n void beginAmountSimulation(totalAmountUsd);\n },\n [beginAmountSimulation],\n );\n\n /**\n * Handle order confirmation - allow intent to execute\n */\n const handleConfirmOrder = useCallback(() => {\n if (!activeIntent) return;\n dispatch({ type: \"setStatus\", payload: \"executing\" });\n dispatch({\n type: \"setStep\",\n payload: { step: \"transaction-status\", direction: \"forward\" },\n });\n activeIntent.allow();\n }, [activeIntent, dispatch]);\n\n /**\n * Navigate to a specific step\n */\n const goToStep = useCallback(\n (newStep: WidgetStep) => {\n if (state.step === \"amount\" && newStep === \"confirmation\") {\n const amount = state.inputs.amount;\n if (amount) {\n const totalAmountUsd = parseFloat(amount.replace(/,/g, \"\"));\n if (totalAmountUsd > 0) {\n void (async () => {\n const started = await beginAmountSimulation(totalAmountUsd);\n if (!started) return;\n dispatch({\n type: \"setStep\",\n payload: { step: newStep, direction: \"forward\" },\n });\n })();\n return;\n }\n }\n }\n dispatch({\n type: \"setStep\",\n payload: { step: newStep, direction: \"forward\" },\n });\n },\n [state.step, state.inputs.amount, beginAmountSimulation, dispatch],\n );\n\n /**\n * Navigate back to previous step\n */\n const goBack = useCallback(async () => {\n const previousStep = STEP_HISTORY[state.step];\n if (previousStep) {\n const suppressUiError = state.step === \"confirmation\" && !isProcessing;\n dispatch({ type: \"setError\", payload: null });\n dispatch({\n type: \"setStep\",\n payload: { step: previousStep, direction: \"backward\" },\n });\n denyActiveSwapIntent({ suppressUiError });\n initialSimulationDone.current = false;\n lastSimulationTime.current = 0;\n setPollingEnabled(false);\n stopwatch.stop();\n stopwatch.reset();\n await fetchSwapBalance();\n }\n }, [\n state.step,\n isProcessing,\n stopwatch,\n dispatch,\n denyActiveSwapIntent,\n fetchSwapBalance,\n ]);\n\n /**\n * Reset widget to initial state\n */\n const reset = useCallback(async () => {\n const suppressUiError = state.step === \"confirmation\" && !isProcessing;\n dispatch({ type: \"reset\" });\n resetAssetSelection();\n resetSteps();\n denyActiveSwapIntent({ suppressUiError });\n initialSimulationDone.current = false;\n lastSimulationTime.current = 0;\n setPollingEnabled(false);\n stopwatch.stop();\n stopwatch.reset();\n await fetchSwapBalance();\n }, [\n resetSteps,\n stopwatch,\n dispatch,\n resetAssetSelection,\n denyActiveSwapIntent,\n fetchSwapBalance,\n state.step,\n isProcessing,\n ]);\n\n /**\n * Refresh simulation data\n */\n const refreshSimulation = useCallback(async () => {\n const timeSinceLastSimulation = Date.now() - lastSimulationTime.current;\n if (timeSinceLastSimulation < 5000) {\n return;\n }\n\n try {\n dispatch({ type: \"setSimulationLoading\", payload: true });\n const updated = await swapIntent.current?.refresh();\n if (updated) {\n swapIntent.current!.intent = updated;\n\n dispatch({\n type: \"setSimulation\",\n payload: {\n swapIntent: swapIntent.current!,\n },\n });\n }\n } catch (e) {\n console.error(\"Unable to refresh intent\", e);\n } finally {\n dispatch({ type: \"setSimulationLoading\", payload: false });\n stopwatch.reset();\n lastSimulationTime.current = Date.now();\n }\n }, [stopwatch, swapIntent, dispatch]);\n\n const startTransaction = useCallback(() => {\n if (isProcessing) return;\n dispatch({ type: \"setError\", payload: null });\n }, [isProcessing, dispatch]);\n\n // Effect: Handle swap intent when it arrives\n useEffect(() => {\n if (!state.intentReady || initialSimulationDone.current) {\n return;\n }\n\n if (!swapIntent.current) {\n return;\n }\n\n initialSimulationDone.current = true;\n dispatch({\n type: \"setSimulation\",\n payload: { swapIntent: swapIntent.current! },\n });\n dispatch({ type: \"setSimulationLoading\", payload: false });\n dispatch({ type: \"setStatus\", payload: \"previewing\" });\n lastSimulationTime.current = Date.now();\n setPollingEnabled(true);\n }, [state.intentReady, swapIntent, dispatch]);\n\n // Effect: Fetch swap balance on mount\n useEffect(() => {\n if (!nexusSDK) return;\n\n if (!swapBalance) {\n void fetchSwapBalance();\n return;\n }\n\n if (!hasAutoSelected.current && availableAssets.length > 0) {\n hasAutoSelected.current = true;\n }\n }, [nexusSDK, swapBalance, availableAssets, fetchSwapBalance]);\n\n // Polling for simulation refresh\n usePolling(\n pollingEnabled &&\n state.status === \"previewing\" &&\n Boolean(swapIntent.current) &&\n !state.simulationLoading,\n async () => {\n await refreshSimulation();\n },\n SIMULATION_POLL_INTERVAL_MS,\n );\n\n // Return the full context value\n return {\n step: state.step,\n inputs: state.inputs,\n setInputs,\n status: state.status,\n explorerUrls: state.explorerUrls,\n sourceSwaps: state.sourceSwaps,\n nexusIntentUrl: state.nexusIntentUrl,\n depositTxHash: state.depositTxHash,\n destination,\n isProcessing,\n isSuccess,\n isError,\n txError: state.error,\n setTxError,\n goToStep,\n goBack,\n reset,\n navigationDirection: state.navigationDirection,\n startTransaction,\n lastResult: state.lastResult,\n assetSelection,\n isManualSelection,\n setAssetSelection,\n swapBalance,\n activeIntent,\n confirmationDetails,\n feeBreakdown,\n steps,\n timer: stopwatch.seconds,\n handleConfirmOrder,\n handleAmountContinue,\n totalSelectedBalance,\n skipSwap: state.skipSwap,\n simulationLoading: state.simulationLoading,\n totalBalance,\n };\n}\n", + "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type {\n WidgetStep,\n DepositWidgetContextValue,\n DepositInputs,\n DestinationConfig,\n} from \"../types\";\nimport {\n ERROR_CODES,\n type ExecuteParams,\n type OnSwapIntentHookData,\n type SwapAndExecuteOnIntentHookData,\n type SwapAndExecuteParams,\n type SwapAndExecuteResult,\n} from \"@avail-project/nexus-sdk-v2\";\nimport { parseUnits } from \"viem\";\n\n// v2: SwapStepType removed — use a local step shape\ntype SwapStepType = {\n typeID?: string;\n type?: string;\n [key: string]: unknown;\n};\nimport {\n SWAP_EXPECTED_STEPS,\n useNexusError,\n usePolling,\n useStopwatch,\n useTransactionSteps,\n} from \"../../common\";\nimport { type Address, type Hex, formatEther } from \"viem\";\nimport { useAccount } from \"wagmi\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport {\n MIN_SELECTABLE_SOURCE_BALANCE_USD,\n SIMULATION_POLL_INTERVAL_MS,\n} from \"../constants/widget\";\n\n// Import extracted hooks\nimport {\n useDepositState,\n STEP_HISTORY,\n type SwapSkippedData,\n} from \"./use-deposit-state\";\nimport { useAssetSelection } from \"./use-asset-selection\";\nimport { useDepositComputed } from \"./use-deposit-computed\";\nimport { resolveDepositSourceSelection } from \"../utils\";\n\ninterface UseDepositProps {\n executeDeposit: (\n tokenSymbol: string,\n tokenAddress: `0x${string}`,\n amount: bigint,\n chainId: number,\n user: Address,\n ) => Omit;\n destination: DestinationConfig;\n onSuccess?: () => void;\n onError?: (error: string) => void;\n}\n\nfunction parseUsdAmount(value?: string): number {\n if (!value) return 0;\n const parsed = Number.parseFloat(value.replace(/,/g, \"\"));\n if (!Number.isFinite(parsed) || parsed <= 0) return 0;\n return parsed;\n}\n\nfunction summarizeIntentSources(\n intent: SwapAndExecuteOnIntentHookData[\"intent\"] | undefined,\n) {\n // v2: SwapAndExecuteIntent has swap.sources only when swapRequired=true\n if (!intent) return [];\n const sources = intent.swapRequired\n ? ((intent as { swapRequired: true; swap: { sources?: unknown[] } }).swap?.sources ?? [])\n : [];\n return sources.map((source) => ({\n chainId: (source as { chain?: { id?: number } }).chain?.id,\n chainName: (source as { chain?: { name?: string } }).chain?.name,\n tokenAddress: (source as { token?: { contractAddress?: string } }).token?.contractAddress,\n tokenSymbol: (source as { token?: { symbol?: string } }).token?.symbol,\n amount: (source as { amount?: string }).amount,\n }));\n}\n\n/**\n * Main deposit widget hook that orchestrates state, SDK integration,\n * and computed values via smaller focused hooks.\n */\nexport function useDepositWidget(\n props: UseDepositProps,\n): DepositWidgetContextValue {\n const { executeDeposit, destination, onSuccess, onError } = props;\n\n // External dependencies\n const {\n nexusSDK,\n swapIntent,\n swapBalance,\n fetchSwapBalance,\n getFiatValue,\n exchangeRate,\n resolveTokenUsdRate,\n } = useNexus();\n const { address } = useAccount();\n const handleNexusError = useNexusError();\n\n // Core state management\n const { state, dispatch } = useDepositState();\n const [pollingEnabled, setPollingEnabled] = useState(false);\n\n // Asset selection state\n const {\n assetSelection,\n isManualSelection,\n setAssetSelection,\n resetAssetSelection,\n } = useAssetSelection(swapBalance, destination, state.inputs.amount);\n\n // Refs for tracking\n const hasAutoSelected = useRef(false);\n const initialSimulationDone = useRef(false);\n const determiningSwapComplete = useRef(false);\n const lastSimulationTime = useRef(0);\n const suppressNextWidgetPreviewCancelError = useRef(false);\n\n const denyActiveSwapIntent = useCallback(\n (options?: { suppressUiError?: boolean }) => {\n const activeSwapIntent =\n swapIntent.current ?? state.simulation?.swapIntent;\n\n if (options?.suppressUiError && activeSwapIntent) {\n suppressNextWidgetPreviewCancelError.current = true;\n }\n\n if (!activeSwapIntent) {\n return;\n }\n\n try {\n activeSwapIntent.deny();\n } catch (error) {\n suppressNextWidgetPreviewCancelError.current = false;\n console.error(\"Failed to deny active swap intent\", error);\n } finally {\n swapIntent.current = null;\n }\n },\n [swapIntent, state.simulation],\n );\n\n // Transaction steps tracking\n const {\n seed,\n onStepComplete,\n reset: resetSteps,\n steps,\n } = useTransactionSteps();\n\n // Stopwatch for timing\n const stopwatch = useStopwatch({\n running:\n state.status === \"executing\" ||\n (state.status === \"previewing\" && determiningSwapComplete.current),\n intervalMs: 100,\n });\n\n // Derived state\n const isProcessing = state.status === \"executing\";\n const isSuccess = state.status === \"success\";\n const isError = state.status === \"error\";\n const activeIntent = state.simulation?.swapIntent ?? swapIntent.current;\n\n // Computed values\n const {\n availableAssets,\n totalSelectedBalance,\n totalBalance,\n confirmationDetails,\n feeBreakdown,\n } = useDepositComputed({\n swapBalance,\n assetSelection,\n activeIntent,\n destination,\n inputAmount: state.inputs.amount,\n exchangeRate,\n getFiatValue,\n actualGasFeeUsd: state.actualGasFeeUsd,\n swapSkippedData: state.swapSkippedData,\n skipSwap: state.skipSwap,\n nexusSDK,\n });\n\n // Action callbacks\n const setInputs = useCallback(\n (next: Partial) => {\n dispatch({ type: \"setInputs\", payload: next });\n },\n [dispatch],\n );\n\n const setTxError = useCallback(\n (error: string | null) => {\n dispatch({ type: \"setError\", payload: error });\n },\n [dispatch],\n );\n\n /**\n * Start the swap and execute flow with the SDK\n */\n const start = useCallback(\n (inputs: SwapAndExecuteParams, targetAmountUsd?: number) => {\n if (!nexusSDK || !inputs || isProcessing) return;\n\n seed(SWAP_EXPECTED_STEPS);\n const requiredAmountUsd =\n targetAmountUsd ?? parseUsdAmount(state.inputs.amount);\n const { sourcePoolIds, selectedSourceIds, fromSources } =\n resolveDepositSourceSelection({\n swapBalance,\n destination,\n filter: assetSelection.filter,\n selectedSourceIds: assetSelection.selectedChainIds,\n isManualSelection,\n minimumBalanceUsd: MIN_SELECTABLE_SOURCE_BALANCE_USD,\n targetAmountUsd: requiredAmountUsd,\n });\n\n if (fromSources.length === 0) {\n const message =\n \"No eligible source balances found. A minimum source balance of $1.00 is required.\";\n dispatch({ type: \"setError\", payload: message });\n dispatch({ type: \"setStatus\", payload: \"error\" });\n onError?.(message);\n return;\n }\n\n const inputsWithSources = {\n ...inputs,\n sources: fromSources, // v2: fromSources renamed to sources\n };\n nexusSDK\n .swapAndExecute(inputsWithSources, {\n onEvent: (event) => {\n // v2: typed discriminated union — plan_preview seeds steps, plan_progress updates them\n if (event.type === \"plan_preview\") {\n const planSteps = (event as { type: string; plan?: { steps?: unknown[] } }).plan?.steps ?? [];\n seed(planSteps.map((s, i) => ({\n typeID: `step-${i}`,\n ...(s as Record),\n })) as SwapStepType[]);\n return;\n }\n\n if (event.type === \"plan_progress\") {\n const progressEvent = event as {\n type: string;\n stepType: string;\n state: string;\n step: Record;\n explorerUrl?: string;\n };\n\n // DETERMINING_SWAP equivalent: intent_hook / route_ready state\n if (\n progressEvent.stepType === \"bridge_intent_submission\" &&\n progressEvent.state === \"completed\"\n ) {\n determiningSwapComplete.current = true;\n stopwatch.start();\n dispatch({ type: \"setIntentReady\", payload: true });\n }\n\n const step: SwapStepType = {\n typeID: progressEvent.stepType,\n type: progressEvent.stepType,\n ...progressEvent.step,\n explorerURL: progressEvent.explorerUrl,\n };\n onStepComplete(step);\n }\n },\n onIntent: (intentData) => {\n // v2: onIntent is a top-level SwapAndExecuteOptions hook\n swapIntent.current = intentData;\n determiningSwapComplete.current = true;\n stopwatch.start();\n dispatch({ type: \"setIntentReady\", payload: true });\n },\n })\n .then((data: SwapAndExecuteResult) => {\n suppressNextWidgetPreviewCancelError.current = false;\n\n // Extract source swaps from the result (v2: SuccessfulSwapResult.sourceSwaps are ChainSwap[])\n const sourceSwapsFromResult = data.swapResult?.sourceSwaps ?? [];\n sourceSwapsFromResult.forEach((sourceSwap) => {\n // v2: no CHAIN_METADATA — use txHash for explorer URL via intentExplorerUrl\n dispatch({\n type: \"addSourceSwap\",\n payload: {\n chainId: sourceSwap.chainId,\n chainName: `Chain ${sourceSwap.chainId}`,\n explorerUrl: data.swapResult?.intentExplorerUrl ?? \"\",\n },\n });\n });\n\n // Set explorer URLs from the result\n if (sourceSwapsFromResult.length > 0) {\n dispatch({\n type: \"setExplorerUrls\",\n payload: { sourceExplorerUrl: data.swapResult?.intentExplorerUrl ?? null },\n });\n }\n\n // Destination explorer URL (v2: intentExplorerUrl replaces swapResult.explorerURL)\n const destinationExplorerUrl =\n data.swapResult?.intentExplorerUrl ??\n (data.executeResponse?.txHash\n ? `https://explorer.avail.so/tx/${data.executeResponse.txHash}`\n : null);\n\n if (destinationExplorerUrl) {\n dispatch({\n type: \"setExplorerUrls\",\n payload: { destinationExplorerUrl },\n });\n }\n\n // Store Nexus intent URL and deposit tx hash\n dispatch({\n type: \"setNexusIntentUrl\",\n payload: data.swapResult?.intentExplorerUrl ?? null,\n });\n dispatch({\n type: \"setDepositTxHash\",\n payload: data.executeResponse?.txHash ?? null,\n });\n\n // Calculate actual gas fee from receipt\n const receipt = data.executeResponse?.receipt;\n if (receipt?.gasUsed && receipt?.effectiveGasPrice) {\n const gasUsed = BigInt(receipt.gasUsed);\n const effectiveGasPrice = BigInt(receipt.effectiveGasPrice);\n const gasCostWei = gasUsed * effectiveGasPrice;\n const gasCostNative = parseFloat(formatEther(gasCostWei));\n const gasTokenSymbol = destination.gasTokenSymbol ?? \"ETH\";\n const gasCostUsd = getFiatValue(gasCostNative, gasTokenSymbol);\n dispatch({\n type: \"setActualGasFeeUsd\",\n payload: gasCostUsd,\n });\n }\n\n dispatch({\n type: \"setReceiveAmount\",\n // v2: SwapAndExecuteIntent doesn't have destination.amount — use swapResult if available\n payload: data.swapResult?.intentExplorerUrl\n ? (swapIntent.current as unknown as { intent?: { destination?: { amount?: string } } })?.intent?.destination?.amount ?? \"\"\n : \"\",\n });\n onSuccess?.();\n dispatch({ type: \"setStatus\", payload: \"success\" });\n dispatch({\n type: \"setStep\",\n payload: { step: \"transaction-complete\", direction: \"forward\" },\n });\n })\n .catch((error) => {\n const { code, message } = handleNexusError(error);\n const isUserRejectedError =\n code === ERROR_CODES.USER_DENIED_INTENT ||\n code === ERROR_CODES.USER_DENIED_INTENT_SIGNATURE ||\n code === ERROR_CODES.USER_DENIED_ALLOWANCE;\n // v2: USER_DENIED_SIWE_SIGNATURE removed\n const shouldSuppressWidgetError =\n suppressNextWidgetPreviewCancelError.current && isUserRejectedError;\n\n suppressNextWidgetPreviewCancelError.current = false;\n\n if (shouldSuppressWidgetError) {\n onError?.(message);\n return;\n }\n\n dispatch({ type: \"setError\", payload: message });\n dispatch({ type: \"setStatus\", payload: \"error\" });\n\n if (initialSimulationDone.current) {\n dispatch({\n type: \"setStep\",\n payload: { step: \"transaction-failed\", direction: \"forward\" },\n });\n } else {\n dispatch({\n type: \"setStep\",\n payload: { step: \"amount\", direction: \"backward\" },\n });\n }\n onError?.(message);\n })\n .finally(async () => {\n await fetchSwapBalance();\n });\n },\n [\n nexusSDK,\n isProcessing,\n seed,\n onStepComplete,\n swapIntent,\n onSuccess,\n onError,\n handleNexusError,\n assetSelection.selectedChainIds,\n assetSelection.filter,\n isManualSelection,\n swapBalance,\n destination,\n getFiatValue,\n fetchSwapBalance,\n dispatch,\n stopwatch,\n state.inputs.amount,\n ],\n );\n\n /**\n * Handle amount input continue - starts simulation\n */\n const beginAmountSimulation = useCallback(\n async (totalAmountUsd: number) => {\n if (!nexusSDK) {\n dispatch({\n type: \"setError\",\n payload: \"Nexus SDK is not initialized.\",\n });\n dispatch({ type: \"setStatus\", payload: \"error\" });\n return false;\n }\n if (!address) {\n dispatch({\n type: \"setError\",\n payload: \"Connect your wallet to continue.\",\n });\n dispatch({ type: \"setStatus\", payload: \"error\" });\n return false;\n }\n const destinationRate = await resolveTokenUsdRate(\n destination.tokenSymbol,\n );\n if (\n !destinationRate ||\n !Number.isFinite(destinationRate) ||\n destinationRate <= 0\n ) {\n dispatch({\n type: \"setError\",\n payload: `Unable to fetch pricing for ${destination.tokenSymbol}. Please try again.`,\n });\n dispatch({ type: \"setStatus\", payload: \"error\" });\n return false;\n }\n\n // Reset state and refs for a fresh simulation\n dispatch({ type: \"setError\", payload: null });\n dispatch({ type: \"setIntentReady\", payload: false });\n initialSimulationDone.current = false;\n determiningSwapComplete.current = false;\n denyActiveSwapIntent();\n\n const tokenAmount = totalAmountUsd / destinationRate;\n const tokenAmountStr = tokenAmount.toFixed(destination.tokenDecimals);\n const parsed = parseUnits(tokenAmountStr, destination.tokenDecimals);\n\n const executeParams = executeDeposit(\n destination.tokenSymbol,\n destination.tokenAddress,\n parsed,\n destination.chainId,\n address,\n );\n\n const newInputs: SwapAndExecuteParams = {\n toChainId: destination.chainId,\n toTokenAddress: destination.tokenAddress,\n toAmountRaw: parsed, // v2: toAmount renamed to toAmountRaw\n execute: {\n to: executeParams.to,\n value: executeParams.value,\n data: executeParams.data,\n gasPrice: executeParams.gasPrice,\n tokenApproval: executeParams.tokenApproval\n ? ({\n toTokenAddress: (executeParams.tokenApproval as unknown as { token?: string; toTokenAddress?: string }).toTokenAddress\n ?? (executeParams.tokenApproval as unknown as { token?: string }).token,\n amount: (executeParams.tokenApproval as { amount: bigint }).amount,\n spender: (executeParams.tokenApproval as { spender: `0x${string}` }).spender,\n } as { toTokenAddress: `0x${string}`; amount: bigint; spender: `0x${string}` })\n : undefined,\n gas: BigInt(400_000),\n },\n };\n\n dispatch({\n type: \"setInputs\",\n payload: { amount: totalAmountUsd.toString() },\n });\n dispatch({ type: \"setStatus\", payload: \"simulation-loading\" });\n dispatch({ type: \"setSimulationLoading\", payload: true });\n start(newInputs, totalAmountUsd);\n return true;\n },\n [\n nexusSDK,\n address,\n resolveTokenUsdRate,\n destination,\n executeDeposit,\n start,\n denyActiveSwapIntent,\n dispatch,\n ],\n );\n\n const handleAmountContinue = useCallback(\n (totalAmountUsd: number) => {\n void beginAmountSimulation(totalAmountUsd);\n },\n [beginAmountSimulation],\n );\n\n /**\n * Handle order confirmation - allow intent to execute\n */\n const handleConfirmOrder = useCallback(() => {\n if (!activeIntent) return;\n dispatch({ type: \"setStatus\", payload: \"executing\" });\n dispatch({\n type: \"setStep\",\n payload: { step: \"transaction-status\", direction: \"forward\" },\n });\n activeIntent.allow();\n }, [activeIntent, dispatch]);\n\n /**\n * Navigate to a specific step\n */\n const goToStep = useCallback(\n (newStep: WidgetStep) => {\n if (state.step === \"amount\" && newStep === \"confirmation\") {\n const amount = state.inputs.amount;\n if (amount) {\n const totalAmountUsd = parseFloat(amount.replace(/,/g, \"\"));\n if (totalAmountUsd > 0) {\n void (async () => {\n const started = await beginAmountSimulation(totalAmountUsd);\n if (!started) return;\n dispatch({\n type: \"setStep\",\n payload: { step: newStep, direction: \"forward\" },\n });\n })();\n return;\n }\n }\n }\n dispatch({\n type: \"setStep\",\n payload: { step: newStep, direction: \"forward\" },\n });\n },\n [state.step, state.inputs.amount, beginAmountSimulation, dispatch],\n );\n\n /**\n * Navigate back to previous step\n */\n const goBack = useCallback(async () => {\n const previousStep = STEP_HISTORY[state.step];\n if (previousStep) {\n const suppressUiError = state.step === \"confirmation\" && !isProcessing;\n dispatch({ type: \"setError\", payload: null });\n dispatch({\n type: \"setStep\",\n payload: { step: previousStep, direction: \"backward\" },\n });\n denyActiveSwapIntent({ suppressUiError });\n initialSimulationDone.current = false;\n lastSimulationTime.current = 0;\n setPollingEnabled(false);\n stopwatch.stop();\n stopwatch.reset();\n await fetchSwapBalance();\n }\n }, [\n state.step,\n isProcessing,\n stopwatch,\n dispatch,\n denyActiveSwapIntent,\n fetchSwapBalance,\n ]);\n\n /**\n * Reset widget to initial state\n */\n const reset = useCallback(async () => {\n const suppressUiError = state.step === \"confirmation\" && !isProcessing;\n dispatch({ type: \"reset\" });\n resetAssetSelection();\n resetSteps();\n denyActiveSwapIntent({ suppressUiError });\n initialSimulationDone.current = false;\n lastSimulationTime.current = 0;\n setPollingEnabled(false);\n stopwatch.stop();\n stopwatch.reset();\n await fetchSwapBalance();\n }, [\n resetSteps,\n stopwatch,\n dispatch,\n resetAssetSelection,\n denyActiveSwapIntent,\n fetchSwapBalance,\n state.step,\n isProcessing,\n ]);\n\n /**\n * Refresh simulation data\n */\n const refreshSimulation = useCallback(async () => {\n const timeSinceLastSimulation = Date.now() - lastSimulationTime.current;\n if (timeSinceLastSimulation < 5000) {\n return;\n }\n\n try {\n dispatch({ type: \"setSimulationLoading\", payload: true });\n const updated = await swapIntent.current?.refresh();\n if (updated) {\n swapIntent.current!.intent = updated;\n\n dispatch({\n type: \"setSimulation\",\n payload: {\n swapIntent: swapIntent.current!,\n },\n });\n }\n } catch (e) {\n console.error(\"Unable to refresh intent\", e);\n } finally {\n dispatch({ type: \"setSimulationLoading\", payload: false });\n stopwatch.reset();\n lastSimulationTime.current = Date.now();\n }\n }, [stopwatch, swapIntent, dispatch]);\n\n const startTransaction = useCallback(() => {\n if (isProcessing) return;\n dispatch({ type: \"setError\", payload: null });\n }, [isProcessing, dispatch]);\n\n // Effect: Handle swap intent when it arrives\n useEffect(() => {\n if (!state.intentReady || initialSimulationDone.current) {\n return;\n }\n\n if (!swapIntent.current) {\n return;\n }\n\n initialSimulationDone.current = true;\n dispatch({\n type: \"setSimulation\",\n payload: { swapIntent: swapIntent.current! },\n });\n dispatch({ type: \"setSimulationLoading\", payload: false });\n dispatch({ type: \"setStatus\", payload: \"previewing\" });\n lastSimulationTime.current = Date.now();\n setPollingEnabled(true);\n }, [state.intentReady, swapIntent, dispatch]);\n\n // Effect: Fetch swap balance on mount\n useEffect(() => {\n if (!nexusSDK) return;\n\n if (!swapBalance) {\n void fetchSwapBalance();\n return;\n }\n\n if (!hasAutoSelected.current && availableAssets.length > 0) {\n hasAutoSelected.current = true;\n }\n }, [nexusSDK, swapBalance, availableAssets, fetchSwapBalance]);\n\n // Polling for simulation refresh\n usePolling(\n pollingEnabled &&\n state.status === \"previewing\" &&\n Boolean(swapIntent.current) &&\n !state.simulationLoading,\n async () => {\n await refreshSimulation();\n },\n SIMULATION_POLL_INTERVAL_MS,\n );\n\n // Return the full context value\n return {\n step: state.step,\n inputs: state.inputs,\n setInputs,\n status: state.status,\n explorerUrls: state.explorerUrls,\n sourceSwaps: state.sourceSwaps,\n nexusIntentUrl: state.nexusIntentUrl,\n depositTxHash: state.depositTxHash,\n destination,\n isProcessing,\n isSuccess,\n isError,\n txError: state.error,\n setTxError,\n goToStep,\n goBack,\n reset,\n navigationDirection: state.navigationDirection,\n startTransaction,\n lastResult: state.lastResult,\n assetSelection,\n isManualSelection,\n setAssetSelection,\n swapBalance,\n activeIntent,\n confirmationDetails,\n feeBreakdown,\n steps,\n timer: stopwatch.seconds,\n handleConfirmOrder,\n handleAmountContinue,\n totalSelectedBalance,\n skipSwap: state.skipSwap,\n simulationLoading: state.simulationLoading,\n totalBalance,\n };\n}\n", "type": "registry:component", "target": "components/deposit/hooks/use-deposit-widget.ts" }, @@ -207,13 +207,13 @@ }, { "path": "registry/nexus-elements/deposit/types.ts", - "content": "import type {\n SUPPORTED_CHAINS_IDS,\n ExecuteParams,\n OnSwapIntentHookData,\n SwapStepType,\n UserAsset,\n} from \"@avail-project/nexus-core\";\nimport type { Address } from \"viem\";\n\nexport type WidgetStep =\n | \"amount\"\n | \"confirmation\"\n | \"transaction-status\"\n | \"transaction-complete\"\n | \"transaction-failed\"\n | \"asset-selection\";\n\nexport type TransactionStatus =\n | \"idle\"\n | \"previewing\"\n | \"simulation-loading\"\n | \"executing\"\n | \"success\"\n | \"error\";\n\nexport type NavigationDirection = \"forward\" | \"backward\" | null;\n\nexport type AssetFilterType = \"all\" | \"stablecoins\" | \"native\" | \"custom\";\n\nexport type TokenCategory = \"stablecoin\" | \"native\" | \"memecoin\";\n\nexport interface ChainItem {\n id: string;\n tokenAddress: `0x${string}`;\n chainId: number;\n name: string;\n usdValue: number;\n amount: number;\n}\n\nexport interface Token {\n id: string;\n symbol: string;\n decimals: number;\n chainsLabel: string;\n usdValue: string;\n amount: string;\n logo: string;\n category: TokenCategory;\n chains: ChainItem[];\n}\n\nexport interface DepositInputs {\n amount?: string;\n selectedToken: string;\n toChainId?: number;\n toTokenAddress?: `0x${string}`;\n toAmount?: bigint;\n}\n\nexport interface AssetSelectionState {\n selectedChainIds: Set;\n filter: AssetFilterType;\n expandedTokens: Set;\n}\n\nexport interface SetAssetSelectionOptions {\n markUserModified?: boolean;\n}\n\nexport interface DestinationConfig {\n chainId: SUPPORTED_CHAINS_IDS;\n depositTargetLogo?: string;\n tokenAddress: `0x${string}`;\n tokenSymbol: string;\n tokenDecimals: number;\n tokenLogo?: string;\n label?: string;\n estimatedTime?: string;\n gasTokenSymbol?: string;\n explorerUrl?: string;\n}\n\nexport interface ExecuteDepositParams {\n tokenSymbol: string;\n tokenAddress: string;\n amount: bigint;\n chainId: number;\n user: Address;\n}\n\nexport type ExecuteDepositResult = Omit;\n\nexport interface UseDepositWidgetProps {\n executeDeposit: (\n tokenSymbol: string,\n tokenAddress: `0x${string}`,\n amount: bigint,\n chainId: number,\n user: Address,\n ) => Omit;\n destination: DestinationConfig;\n onSuccess?: () => void;\n onError?: (error: string) => void;\n}\n\nexport interface DepositWidgetContextValue {\n // Core state\n step: WidgetStep;\n inputs: DepositInputs;\n status: TransactionStatus;\n\n // Input management\n setInputs: (inputs: Partial) => void;\n\n // Explorer URLs\n explorerUrls: {\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n };\n\n // Source swap transactions (from BRIDGE_DEPOSIT events)\n sourceSwaps: Array<{\n chainId: number;\n chainName: string;\n explorerUrl: string;\n }>;\n\n // Transaction result data\n nexusIntentUrl: string | null;\n depositTxHash: string | null;\n\n // Destination config (for building explorer URLs)\n destination: DestinationConfig;\n\n // Derived state\n isProcessing: boolean;\n isSuccess: boolean;\n isError: boolean;\n\n // Error handling\n txError: string | null;\n setTxError: (error: string | null) => void;\n\n // Navigation\n goToStep: (step: WidgetStep) => void;\n goBack: () => void;\n reset: () => void;\n navigationDirection: NavigationDirection;\n\n // Transaction actions\n startTransaction: () => void;\n\n // Results\n lastResult: unknown;\n\n // Asset selection\n assetSelection: AssetSelectionState;\n isManualSelection: boolean;\n setAssetSelection: (\n selection: Partial,\n options?: SetAssetSelectionOptions,\n ) => void;\n\n // SDK integration\n swapBalance: UserAsset[] | null;\n activeIntent: OnSwapIntentHookData | null;\n confirmationDetails: {\n sourceLabel: string;\n sources: Array<\n | {\n chainId: number;\n tokenAddress: `0x${string}`;\n decimals: number;\n symbol: string;\n balance: string;\n balanceInFiat?: number;\n tokenLogo?: string;\n chainLogo?: string;\n chainName?: string;\n isDestinationBalance?: boolean;\n }\n | undefined\n >;\n gasTokenSymbol?: string;\n estimatedTime?: string;\n amountSpent: number;\n totalFeeUsd: number;\n receiveTokenSymbol: string;\n receiveAmountAfterSwap: string;\n receiveAmountAfterSwapUsd: number;\n receiveTokenLogo?: string;\n receiveTokenChain: number;\n destinationChainName?: string;\n } | null;\n feeBreakdown: {\n totalGasFee: number;\n gasUsd: number;\n gasFormatted: string;\n bridgeUsd: number;\n bufferUsd: number;\n totalFeeUsd: number;\n gasSponsorshipUsd: number;\n executionGasFeeUsd: number;\n protocolFeeUsd: number;\n solverFeeUsd: number;\n otherBridgeFeeUsd: number;\n swapImpactUsd: number;\n swapImpactPercent: number;\n bridgeComponents: Array<{\n key: string;\n label: string;\n amountUsd: number;\n }>;\n };\n steps: Array<{\n id: number;\n completed: boolean;\n step: SwapStepType;\n }>;\n timer: number;\n handleConfirmOrder: () => void;\n handleAmountContinue: (totalAmountUsd: number) => void;\n totalSelectedBalance: number;\n skipSwap: boolean;\n simulationLoading: boolean;\n totalBalance:\n | {\n balance: number;\n usdBalance: number;\n }\n | undefined;\n}\n\nexport interface BaseDepositWidgetProps {\n onSuccess?: () => void;\n onError?: (error: string) => void;\n}\n\nexport interface DepositWidgetProps\n extends UseDepositWidgetProps,\n BaseDepositWidgetProps {\n heading?: string;\n embed?: boolean;\n className?: string;\n onClose?: () => void;\n /** Control the dialog open state (non-embed mode only) */\n open?: boolean;\n /** Callback when dialog open state changes (non-embed mode only) */\n onOpenChange?: (open: boolean) => void;\n /** Default open state for uncontrolled usage (non-embed mode only) */\n defaultOpen?: boolean;\n}\n", + "content": "import type {\n ExecuteParams,\n OnSwapIntentHookData,\n SwapAndExecuteOnIntentHookData,\n TokenBalance,\n} from \"@avail-project/nexus-sdk-v2\";\nimport type { Address } from \"viem\";\n\n// v2: SwapStepType removed — use a local generic step shape\nexport type SwapStepType = {\n typeID?: string;\n type?: string;\n [key: string]: unknown;\n};\n\n// Re-export for convenience\nexport type { OnSwapIntentHookData };\n\nexport type WidgetStep =\n | \"amount\"\n | \"confirmation\"\n | \"transaction-status\"\n | \"transaction-complete\"\n | \"transaction-failed\"\n | \"asset-selection\";\n\nexport type TransactionStatus =\n | \"idle\"\n | \"previewing\"\n | \"simulation-loading\"\n | \"executing\"\n | \"success\"\n | \"error\";\n\nexport type NavigationDirection = \"forward\" | \"backward\" | null;\n\nexport type AssetFilterType = \"all\" | \"stablecoins\" | \"native\" | \"custom\";\n\nexport type TokenCategory = \"stablecoin\" | \"native\" | \"memecoin\";\n\nexport interface ChainItem {\n id: string;\n tokenAddress: `0x${string}`;\n chainId: number;\n name: string;\n usdValue: number;\n amount: number;\n}\n\nexport interface Token {\n id: string;\n symbol: string;\n decimals: number;\n chainsLabel: string;\n usdValue: string;\n amount: string;\n logo: string;\n category: TokenCategory;\n chains: ChainItem[];\n}\n\nexport interface DepositInputs {\n amount?: string;\n selectedToken: string;\n toChainId?: number;\n toTokenAddress?: `0x${string}`;\n toAmount?: bigint;\n}\n\nexport interface AssetSelectionState {\n selectedChainIds: Set;\n filter: AssetFilterType;\n expandedTokens: Set;\n}\n\nexport interface SetAssetSelectionOptions {\n markUserModified?: boolean;\n}\n\nexport interface DestinationConfig {\n chainId: number; // v2: was SUPPORTED_CHAINS_IDS\n depositTargetLogo?: string;\n tokenAddress: `0x${string}`;\n tokenSymbol: string;\n tokenDecimals: number;\n tokenLogo?: string;\n label?: string;\n estimatedTime?: string;\n gasTokenSymbol?: string;\n explorerUrl?: string;\n}\n\nexport interface ExecuteDepositParams {\n tokenSymbol: string;\n tokenAddress: string;\n amount: bigint;\n chainId: number;\n user: Address;\n}\n\nexport type ExecuteDepositResult = Omit;\n\nexport interface UseDepositWidgetProps {\n executeDeposit: (\n tokenSymbol: string,\n tokenAddress: `0x${string}`,\n amount: bigint,\n chainId: number,\n user: Address,\n ) => Omit;\n destination: DestinationConfig;\n onSuccess?: () => void;\n onError?: (error: string) => void;\n}\n\nexport interface DepositWidgetContextValue {\n // Core state\n step: WidgetStep;\n inputs: DepositInputs;\n status: TransactionStatus;\n\n // Input management\n setInputs: (inputs: Partial) => void;\n\n // Explorer URLs\n explorerUrls: {\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n };\n\n // Source swap transactions (from BRIDGE_DEPOSIT events)\n sourceSwaps: Array<{\n chainId: number;\n chainName: string;\n explorerUrl: string;\n }>;\n\n // Transaction result data\n nexusIntentUrl: string | null;\n depositTxHash: string | null;\n\n // Destination config (for building explorer URLs)\n destination: DestinationConfig;\n\n // Derived state\n isProcessing: boolean;\n isSuccess: boolean;\n isError: boolean;\n\n // Error handling\n txError: string | null;\n setTxError: (error: string | null) => void;\n\n // Navigation\n goToStep: (step: WidgetStep) => void;\n goBack: () => void;\n reset: () => void;\n navigationDirection: NavigationDirection;\n\n // Transaction actions\n startTransaction: () => void;\n\n // Results\n lastResult: unknown;\n\n // Asset selection\n assetSelection: AssetSelectionState;\n isManualSelection: boolean;\n setAssetSelection: (\n selection: Partial,\n options?: SetAssetSelectionOptions,\n ) => void;\n\n // SDK integration\n swapBalance: TokenBalance[] | null;\n activeIntent: SwapAndExecuteOnIntentHookData | null;\n confirmationDetails: {\n sourceLabel: string;\n sources: Array<\n | {\n chainId: number;\n tokenAddress: `0x${string}`;\n decimals: number;\n symbol: string;\n balance: string;\n value?: string; // v2: string USD value (replaces balanceInFiat: number)\n balanceInFiat?: number; // kept for any legacy callers\n tokenLogo?: string;\n chainLogo?: string;\n chainName?: string;\n isDestinationBalance?: boolean;\n }\n | undefined\n >;\n gasTokenSymbol?: string;\n estimatedTime?: string;\n amountSpent: number;\n totalFeeUsd: number;\n receiveTokenSymbol: string;\n receiveAmountAfterSwap: string;\n receiveAmountAfterSwapUsd: number;\n receiveTokenLogo?: string;\n receiveTokenChain: number;\n destinationChainName?: string;\n } | null;\n feeBreakdown: {\n totalGasFee: number;\n gasUsd: number;\n gasFormatted: string;\n bridgeUsd: number;\n bufferUsd: number;\n totalFeeUsd: number;\n gasSponsorshipUsd: number;\n executionGasFeeUsd: number;\n protocolFeeUsd: number;\n solverFeeUsd: number;\n otherBridgeFeeUsd: number;\n swapImpactUsd: number;\n swapImpactPercent: number;\n bridgeComponents: Array<{\n key: string;\n label: string;\n amountUsd: number;\n }>;\n };\n steps: Array<{\n id: number;\n completed: boolean;\n step: SwapStepType;\n }>;\n timer: number;\n handleConfirmOrder: () => void;\n handleAmountContinue: (totalAmountUsd: number) => void;\n totalSelectedBalance: number;\n skipSwap: boolean;\n simulationLoading: boolean;\n totalBalance:\n | {\n balance: number;\n usdBalance: number;\n }\n | undefined;\n}\n\nexport interface BaseDepositWidgetProps {\n onSuccess?: () => void;\n onError?: (error: string) => void;\n}\n\nexport interface DepositWidgetProps\n extends UseDepositWidgetProps,\n BaseDepositWidgetProps {\n heading?: string;\n embed?: boolean;\n className?: string;\n onClose?: () => void;\n /** Control the dialog open state (non-embed mode only) */\n open?: boolean;\n /** Callback when dialog open state changes (non-embed mode only) */\n onOpenChange?: (open: boolean) => void;\n /** Default open state for uncontrolled usage (non-embed mode only) */\n defaultOpen?: boolean;\n}\n", "type": "registry:component", "target": "components/deposit/types.ts" }, { "path": "registry/nexus-elements/deposit/utils.ts", - "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\nimport { STABLECOIN_SYMBOLS } from \"./constants/widget\";\nimport {\n CHAIN_METADATA,\n sortSourcesByPriority,\n UserAsset,\n} from \"@avail-project/nexus-core\";\nimport { AssetFilterType, DestinationConfig, Token } from \"./types\";\nimport { Hex, padHex } from \"viem\";\nimport { formatUsdForDisplay } from \"../common\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\nexport function parseNonNegativeNumber(value: unknown): number {\n const parsed = Number.parseFloat(String(value));\n if (!Number.isFinite(parsed) || parsed < 0) return 0;\n return parsed;\n}\n\n/**\n * Parse currency input by removing $ and commas, keeping only numbers and decimal\n */\nexport function parseCurrencyInput(input: string): string {\n return input.replace(/[^0-9.]/g, \"\");\n}\n\nexport function isStablecoin(symbol: string): boolean {\n return STABLECOIN_SYMBOLS.includes(\n symbol as (typeof STABLECOIN_SYMBOLS)[number],\n );\n}\n\nexport function isNative(symbol: string): boolean {\n return Object.values(CHAIN_METADATA).some(\n (chain) => chain.nativeCurrency.symbol === symbol,\n );\n}\n\n/**\n * Get checkbox state for a token based on selected chains\n */\nexport function getTokenCheckState(\n token: Token,\n selectedChainIds: Set,\n): boolean | \"indeterminate\" {\n const selectedChainCount = token.chains.filter((c) =>\n selectedChainIds.has(c.id),\n ).length;\n\n if (selectedChainCount === 0) return false;\n if (selectedChainCount === token.chains.length) return true;\n return \"indeterminate\";\n}\n\n/**\n * Check if current selection matches a preset filter\n * Returns the matching filter type or \"custom\"\n */\nexport function checkIfMatchesPreset(\n tokens: Token[],\n selectedChainIds: Set,\n): AssetFilterType {\n if (selectedChainIds.size === 0) return \"custom\";\n\n const allIds = new Set();\n const stableIds = new Set();\n const nativeIds = new Set();\n\n tokens.forEach((token) => {\n token.chains.forEach((chain) => {\n allIds.add(chain.id);\n if (isStablecoin(token.symbol)) {\n stableIds.add(chain.id);\n }\n if (isNative(token.symbol)) {\n nativeIds.add(chain.id);\n }\n });\n });\n\n const setsEqual = (a: Set, b: Set) =>\n a.size === b.size && [...a].every((id) => b.has(id));\n\n if (setsEqual(selectedChainIds, allIds)) return \"all\";\n if (setsEqual(selectedChainIds, stableIds)) return \"stablecoins\";\n if (setsEqual(selectedChainIds, nativeIds)) return \"native\";\n return \"custom\";\n}\n\n/**\n * Get chain IDs for a preset filter\n */\nexport function getChainIdsForFilter(\n tokens: Token[],\n filter: \"all\" | \"stablecoins\" | \"native\",\n): Set {\n const ids = new Set();\n tokens.forEach((token) => {\n const shouldInclude =\n filter === \"all\" ||\n (filter === \"stablecoins\" && isStablecoin(token.symbol)) ||\n (filter === \"native\" && isNative(token.symbol));\n\n if (shouldInclude) {\n token.chains.forEach((chain) => ids.add(chain.id));\n }\n });\n return ids;\n}\n\n/**\n * Calculate total USD value for selected chain IDs\n */\nexport function calculateSelectedAmount(\n tokens: Token[],\n selectedChainIds: Set,\n): number {\n let total = 0;\n tokens.forEach((token) => {\n token.chains.forEach((chain) => {\n if (selectedChainIds.has(chain.id)) {\n total += chain.usdValue;\n }\n });\n });\n return total;\n}\n\nconst ZERO_ADDRESS = \"0x0000000000000000000000000000000000000000\";\nconst EVM_NATIVE_PLACEHOLDER = \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\";\nconst MAX_PRIORITY_RANK = Number.MAX_SAFE_INTEGER;\n\nfunction normalizeAddress(address: string): string {\n return address.toLowerCase();\n}\n\nfunction toComparableSdkAddress(address: string): string {\n const normalized = normalizeAddress(address);\n const effectiveAddress =\n normalized === ZERO_ADDRESS ? EVM_NATIVE_PLACEHOLDER : normalized;\n\n try {\n return padHex(effectiveAddress as Hex, { size: 32 }).toLowerCase();\n } catch {\n return effectiveAddress;\n }\n}\n\nexport function getFiatLookupKey(\n tokenAddress: string,\n chainId: number,\n): string {\n return `${normalizeAddress(tokenAddress)}-${chainId}`;\n}\n\nexport function getPriorityLookupKey(\n tokenAddress: string,\n chainId: number,\n): string {\n return `${toComparableSdkAddress(tokenAddress)}-${chainId}`;\n}\n\ninterface SourceCandidate {\n sourceId: string;\n balanceInFiat: number;\n priorityRank: number;\n}\n\nexport function parseSourceId(sourceId: string): {\n tokenAddress: Hex;\n chainId: number;\n} | null {\n const separatorIndex = sourceId.lastIndexOf(\"-\");\n if (separatorIndex <= 0) return null;\n\n const tokenAddress = sourceId.slice(0, separatorIndex) as Hex;\n const chainId = Number.parseInt(sourceId.slice(separatorIndex + 1), 10);\n if (!Number.isInteger(chainId) || chainId <= 0) return null;\n\n return { tokenAddress, chainId };\n}\n\nfunction buildSourceFiatByKeyMap(\n swapBalance: UserAsset[] | null,\n): Map {\n const map = new Map();\n if (!swapBalance) return map;\n\n for (const asset of swapBalance) {\n for (const breakdown of asset.breakdown ?? []) {\n const chainId = breakdown.chain?.id;\n const tokenAddress = breakdown.contractAddress;\n if (!chainId || !tokenAddress) continue;\n\n const balanceInFiat = parseNonNegativeNumber(breakdown.balanceInFiat);\n\n map.set(getFiatLookupKey(tokenAddress, chainId), balanceInFiat);\n }\n }\n\n return map;\n}\n\nfunction buildPriorityRankMap(\n swapBalance: UserAsset[] | null,\n destination: Pick<\n DestinationConfig,\n \"chainId\" | \"tokenAddress\" | \"tokenSymbol\"\n >,\n): Map {\n const map = new Map();\n if (!swapBalance?.length) return map;\n\n const sortedSources = sortSourcesByPriority(swapBalance, {\n chainID: destination.chainId,\n tokenAddress: destination.tokenAddress,\n symbol: destination.tokenSymbol,\n });\n\n sortedSources.forEach((source, index) => {\n map.set(getPriorityLookupKey(source.tokenAddress, source.chainID), index);\n });\n\n return map;\n}\n\nfunction sortSourceIdsByPriority(params: {\n sourceIds: Iterable;\n swapBalance: UserAsset[] | null;\n destination: Pick<\n DestinationConfig,\n \"chainId\" | \"tokenAddress\" | \"tokenSymbol\"\n >;\n minimumBalanceUsd?: number;\n}): string[] {\n return buildSortedSourceCandidates(params).map((item) => item.sourceId);\n}\n\nfunction buildSortedSourceCandidates(params: {\n sourceIds: Iterable;\n swapBalance: UserAsset[] | null;\n destination: Pick<\n DestinationConfig,\n \"chainId\" | \"tokenAddress\" | \"tokenSymbol\"\n >;\n minimumBalanceUsd?: number;\n}): SourceCandidate[] {\n const { sourceIds, swapBalance, destination, minimumBalanceUsd } = params;\n const uniqueIds = [...new Set(sourceIds)];\n if (uniqueIds.length === 0) return [];\n\n const sourceFiatByKeyMap = buildSourceFiatByKeyMap(swapBalance);\n const priorityRankMap = buildPriorityRankMap(swapBalance, destination);\n\n return uniqueIds\n .map((sourceId) => {\n const parsed = parseSourceId(sourceId);\n if (!parsed) return null;\n\n const fiatKey = getFiatLookupKey(parsed.tokenAddress, parsed.chainId);\n const priorityKey = getPriorityLookupKey(\n parsed.tokenAddress,\n parsed.chainId,\n );\n const balanceInFiat = sourceFiatByKeyMap.get(fiatKey) ?? 0;\n const priorityRank =\n priorityRankMap.get(priorityKey) ?? MAX_PRIORITY_RANK;\n\n return {\n sourceId,\n balanceInFiat,\n priorityRank,\n };\n })\n .filter((item): item is NonNullable => {\n if (!item) return false;\n if (minimumBalanceUsd == null) return true;\n return item.balanceInFiat >= minimumBalanceUsd;\n })\n .sort((a, b) => {\n if (a.priorityRank !== b.priorityRank) {\n return a.priorityRank - b.priorityRank;\n }\n if (a.balanceInFiat !== b.balanceInFiat) {\n return b.balanceInFiat - a.balanceInFiat;\n }\n return a.sourceId.localeCompare(b.sourceId);\n });\n}\n\nexport function buildDepositSourcePoolIds(params: {\n swapBalance: UserAsset[] | null;\n filter: AssetFilterType;\n selectedSourceIds: Iterable;\n isManualSelection: boolean;\n}): string[] {\n const { swapBalance, filter, selectedSourceIds, isManualSelection } = params;\n const selectedSourceIdSet = new Set(selectedSourceIds);\n\n if (isManualSelection) {\n return [...selectedSourceIdSet];\n }\n\n const sourceIds = new Set();\n\n swapBalance?.forEach((asset) => {\n asset.breakdown?.forEach((breakdown) => {\n const chainId = breakdown.chain?.id;\n const tokenAddress = breakdown.contractAddress;\n if (!chainId || !tokenAddress) return;\n\n const stable = isStablecoin(breakdown.symbol);\n const native = isNative(breakdown.symbol);\n const sourceId = `${tokenAddress}-${chainId}`;\n const include =\n filter === \"all\" ||\n (filter === \"stablecoins\" && stable) ||\n (filter === \"native\" && native) ||\n (filter === \"custom\" && selectedSourceIdSet.has(sourceId));\n\n if (include) {\n sourceIds.add(sourceId);\n }\n });\n });\n\n return [...sourceIds];\n}\n\nexport interface ResolvedDepositSourceSelection {\n sourcePoolIds: string[];\n selectedSourceIds: string[];\n fromSources: Array<{ tokenAddress: Hex; chainId: number }>;\n}\n\nexport function resolveDepositSourceSelection(params: {\n swapBalance: UserAsset[] | null;\n destination: Pick<\n DestinationConfig,\n \"chainId\" | \"tokenAddress\" | \"tokenSymbol\"\n >;\n filter: AssetFilterType;\n selectedSourceIds: Iterable;\n isManualSelection: boolean;\n minimumBalanceUsd: number;\n targetAmountUsd?: number;\n}): ResolvedDepositSourceSelection {\n const {\n swapBalance,\n destination,\n filter,\n selectedSourceIds,\n isManualSelection,\n minimumBalanceUsd,\n targetAmountUsd,\n } = params;\n\n const sourcePoolIds = buildDepositSourcePoolIds({\n swapBalance,\n filter,\n selectedSourceIds,\n isManualSelection,\n });\n\n const resolvedSelectedSourceIds = isManualSelection\n ? sortSourceIdsByPriority({\n sourceIds: sourcePoolIds,\n swapBalance,\n destination,\n minimumBalanceUsd,\n })\n : buildPrioritySelectedSourceIds({\n swapBalance,\n destination,\n minimumBalanceUsd,\n targetAmountUsd,\n sourceIds: sourcePoolIds,\n });\n\n const fromSources = buildSortedFromSources({\n sourceIds: resolvedSelectedSourceIds,\n swapBalance,\n destination,\n minimumBalanceUsd,\n });\n\n return {\n sourcePoolIds,\n selectedSourceIds: resolvedSelectedSourceIds,\n fromSources,\n };\n}\n\nexport function buildSelectableSourceIds(params: {\n swapBalance: UserAsset[] | null;\n destination: Pick<\n DestinationConfig,\n \"chainId\" | \"tokenAddress\" | \"tokenSymbol\"\n >;\n minimumBalanceUsd: number;\n}): string[] {\n const { swapBalance, destination, minimumBalanceUsd } = params;\n const sourceIds = new Set();\n\n if (!swapBalance) return [];\n\n for (const asset of swapBalance) {\n for (const breakdown of asset.breakdown ?? []) {\n const chainId = breakdown.chain?.id;\n const tokenAddress = breakdown.contractAddress;\n if (!chainId || !tokenAddress) continue;\n\n sourceIds.add(`${tokenAddress}-${chainId}`);\n }\n }\n\n return sortSourceIdsByPriority({\n sourceIds,\n swapBalance,\n destination,\n minimumBalanceUsd,\n });\n}\n\nexport function buildPrioritySelectedSourceIds(params: {\n swapBalance: UserAsset[] | null;\n destination: Pick<\n DestinationConfig,\n \"chainId\" | \"tokenAddress\" | \"tokenSymbol\"\n >;\n minimumBalanceUsd: number;\n targetAmountUsd?: number;\n sourceIds?: Iterable;\n}): string[] {\n const {\n swapBalance,\n destination,\n minimumBalanceUsd,\n targetAmountUsd,\n sourceIds,\n } = params;\n\n const requestedSourceIds = sourceIds ? [...new Set(sourceIds)] : undefined;\n const orderedCandidateSourceIds = requestedSourceIds\n ? sortSourceIdsByPriority({\n sourceIds: requestedSourceIds,\n swapBalance,\n destination,\n minimumBalanceUsd,\n })\n : buildSelectableSourceIds({\n swapBalance,\n destination,\n minimumBalanceUsd,\n });\n\n if (orderedCandidateSourceIds.length === 0) return [];\n\n const normalizedTargetAmountUsd = parseNonNegativeNumber(targetAmountUsd);\n if (normalizedTargetAmountUsd <= 0) {\n const defaultSourceIds = [orderedCandidateSourceIds[0]];\n\n return defaultSourceIds;\n }\n\n const sourceFiatByKeyMap = buildSourceFiatByKeyMap(swapBalance);\n const priorityRankMap = buildPriorityRankMap(swapBalance, destination);\n const selectedSourceIds: string[] = [];\n let runningTotalUsd = 0;\n\n for (const sourceId of orderedCandidateSourceIds) {\n const parsed = parseSourceId(sourceId);\n if (!parsed) continue;\n\n selectedSourceIds.push(sourceId);\n runningTotalUsd +=\n sourceFiatByKeyMap.get(\n getFiatLookupKey(parsed.tokenAddress, parsed.chainId),\n ) ?? 0;\n\n if (runningTotalUsd >= normalizedTargetAmountUsd) {\n break;\n }\n }\n\n return selectedSourceIds;\n}\n\nexport function buildSortedFromSources(params: {\n sourceIds: Iterable;\n swapBalance: UserAsset[] | null;\n destination: Pick<\n DestinationConfig,\n \"chainId\" | \"tokenAddress\" | \"tokenSymbol\"\n >;\n minimumBalanceUsd?: number;\n}): Array<{ tokenAddress: Hex; chainId: number }> {\n const requestedSourceIds = [...new Set(params.sourceIds)];\n const orderedIds = sortSourceIdsByPriority({\n ...params,\n sourceIds: requestedSourceIds,\n });\n const orderedSources = orderedIds\n .map((sourceId) => parseSourceId(sourceId))\n .filter((item): item is NonNullable => Boolean(item));\n\n return orderedSources;\n}\n\nexport function formatFeeUsd(amountUsd: number): string {\n if (amountUsd > 0 && amountUsd < 0.001) {\n return \"< $0.001\";\n }\n return formatUsdForDisplay(amountUsd);\n}\n\nexport function formatSignedUsd(value: number): string {\n if (!Number.isFinite(value) || value === 0) return \"$0.00\";\n const sign = value < 0 ? \"-\" : \"+\";\n const absolute = Math.abs(value);\n const absoluteLabel =\n absolute < 0.001 ? \"< $0.001\" : formatUsdForDisplay(absolute);\n return `${sign}${absoluteLabel}`;\n}\n\nexport function formatImpactPercent(value: number): string {\n if (!Number.isFinite(value) || value === 0) return \"0%\";\n const absolute = Math.abs(value);\n if (absolute < 0.01) {\n return \"< 0.01%\";\n }\n const sign = value < 0 ? \"-\" : \"+\";\n const fixed = absolute.toFixed(2);\n return `${sign}${fixed.replace(/\\.?0+$/, \"\")}%`;\n}\n", + "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\nimport { STABLECOIN_SYMBOLS } from \"./constants/widget\";\nimport type { TokenBalance } from \"@avail-project/nexus-sdk-v2\";\nimport { AssetFilterType, DestinationConfig, Token } from \"./types\";\nimport { Hex, padHex } from \"viem\";\nimport { formatUsdForDisplay } from \"../common\";\n\n// v2: CHAIN_METADATA not exported — use a stable hardcoded set of well-known native symbols\nconst WELL_KNOWN_NATIVE_SYMBOLS = new Set([\n \"ETH\", \"MATIC\", \"AVAX\", \"BNB\", \"OP\", \"ARB\", \"KAIA\", \"CELO\", \"FTM\", \"MON\",\n \"HYPE\",\n]);\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\nexport function parseNonNegativeNumber(value: unknown): number {\n const parsed = Number.parseFloat(String(value));\n if (!Number.isFinite(parsed) || parsed < 0) return 0;\n return parsed;\n}\n\n/**\n * Parse currency input by removing $ and commas, keeping only numbers and decimal\n */\nexport function parseCurrencyInput(input: string): string {\n return input.replace(/[^0-9.]/g, \"\");\n}\n\nexport function isStablecoin(symbol: string): boolean {\n return STABLECOIN_SYMBOLS.includes(\n symbol as (typeof STABLECOIN_SYMBOLS)[number],\n );\n}\n\nexport function isNative(symbol: string): boolean {\n return WELL_KNOWN_NATIVE_SYMBOLS.has(symbol?.toUpperCase());\n}\n\n/**\n * Get checkbox state for a token based on selected chains\n */\nexport function getTokenCheckState(\n token: Token,\n selectedChainIds: Set,\n): boolean | \"indeterminate\" {\n const selectedChainCount = token.chains.filter((c) =>\n selectedChainIds.has(c.id),\n ).length;\n\n if (selectedChainCount === 0) return false;\n if (selectedChainCount === token.chains.length) return true;\n return \"indeterminate\";\n}\n\n/**\n * Check if current selection matches a preset filter\n * Returns the matching filter type or \"custom\"\n */\nexport function checkIfMatchesPreset(\n tokens: Token[],\n selectedChainIds: Set,\n): AssetFilterType {\n if (selectedChainIds.size === 0) return \"custom\";\n\n const allIds = new Set();\n const stableIds = new Set();\n const nativeIds = new Set();\n\n tokens.forEach((token) => {\n token.chains.forEach((chain) => {\n allIds.add(chain.id);\n if (isStablecoin(token.symbol)) {\n stableIds.add(chain.id);\n }\n console.log(\"token\", token)\n if (isNative(token.symbol)) {\n nativeIds.add(chain.id);\n }\n });\n });\n\n const setsEqual = (a: Set, b: Set) =>\n a.size === b.size && [...a].every((id) => b.has(id));\n\n if (setsEqual(selectedChainIds, allIds)) return \"all\";\n if (setsEqual(selectedChainIds, stableIds)) return \"stablecoins\";\n if (setsEqual(selectedChainIds, nativeIds)) return \"native\";\n return \"custom\";\n}\n\n/**\n * Get chain IDs for a preset filter\n */\nexport function getChainIdsForFilter(\n tokens: Token[],\n filter: \"all\" | \"stablecoins\" | \"native\",\n): Set {\n const ids = new Set();\n tokens.forEach((token) => {\n const shouldInclude =\n filter === \"all\" ||\n (filter === \"stablecoins\" && isStablecoin(token.symbol)) ||\n (filter === \"native\" && isNative(token.symbol));\n\n if (shouldInclude) {\n token.chains.forEach((chain) => ids.add(chain.id));\n }\n });\n return ids;\n}\n\n/**\n * Calculate total USD value for selected chain IDs\n */\nexport function calculateSelectedAmount(\n tokens: Token[],\n selectedChainIds: Set,\n): number {\n let total = 0;\n tokens.forEach((token) => {\n token.chains.forEach((chain) => {\n if (selectedChainIds.has(chain.id)) {\n total += chain.usdValue;\n }\n });\n });\n return total;\n}\n\nconst ZERO_ADDRESS = \"0x0000000000000000000000000000000000000000\";\nconst EVM_NATIVE_PLACEHOLDER = \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\";\nconst MAX_PRIORITY_RANK = Number.MAX_SAFE_INTEGER;\n\nfunction normalizeAddress(address: string): string {\n return address.toLowerCase();\n}\n\nfunction toComparableSdkAddress(address: string): string {\n const normalized = normalizeAddress(address);\n const effectiveAddress =\n normalized === ZERO_ADDRESS ? EVM_NATIVE_PLACEHOLDER : normalized;\n\n try {\n return padHex(effectiveAddress as Hex, { size: 32 }).toLowerCase();\n } catch {\n return effectiveAddress;\n }\n}\n\nexport function getFiatLookupKey(\n tokenAddress: string,\n chainId: number,\n): string {\n return `${normalizeAddress(tokenAddress)}-${chainId}`;\n}\n\nexport function getPriorityLookupKey(\n tokenAddress: string,\n chainId: number,\n): string {\n return `${toComparableSdkAddress(tokenAddress)}-${chainId}`;\n}\n\ninterface SourceCandidate {\n sourceId: string;\n balanceInFiat: number;\n priorityRank: number;\n}\n\nexport function parseSourceId(sourceId: string): {\n tokenAddress: Hex;\n chainId: number;\n} | null {\n const separatorIndex = sourceId.lastIndexOf(\"-\");\n if (separatorIndex <= 0) return null;\n\n const tokenAddress = sourceId.slice(0, separatorIndex) as Hex;\n const chainId = Number.parseInt(sourceId.slice(separatorIndex + 1), 10);\n if (!Number.isInteger(chainId) || chainId <= 0) return null;\n\n return { tokenAddress, chainId };\n}\n\nfunction buildSourceFiatByKeyMap(\n swapBalance: TokenBalance[] | null,\n): Map {\n const map = new Map();\n if (!swapBalance) return map;\n\n for (const asset of swapBalance) {\n for (const breakdown of asset.chainBalances ?? []) {\n const chainId = breakdown.chain?.id;\n const tokenAddress = breakdown.contractAddress;\n if (!chainId || !tokenAddress) continue;\n\n const balanceInFiat = parseNonNegativeNumber(breakdown.value);\n\n map.set(getFiatLookupKey(tokenAddress, chainId), balanceInFiat);\n }\n }\n\n return map;\n}\n\n// v2: sortSourcesByPriority removed — build priority rank map by fiat balance descending\nfunction buildPriorityRankMap(\n swapBalance: TokenBalance[] | null,\n destination: Pick<\n DestinationConfig,\n \"chainId\" | \"tokenAddress\" | \"tokenSymbol\"\n >,\n): Map {\n const map = new Map();\n if (!swapBalance?.length) return map;\n\n // Collect all sources with their fiat values and sort by fiat descending\n const candidates: { key: string; balanceInFiat: number }[] = [];\n for (const asset of swapBalance) {\n for (const breakdown of asset.chainBalances ?? []) {\n const chainId = breakdown.chain?.id;\n const tokenAddress = breakdown.contractAddress;\n if (!chainId || !tokenAddress) continue;\n // Exclude the destination chain\n if (chainId === destination.chainId) continue;\n candidates.push({\n key: getPriorityLookupKey(tokenAddress, chainId),\n balanceInFiat: parseNonNegativeNumber(breakdown.value),\n });\n }\n }\n candidates.sort((a, b) => b.balanceInFiat - a.balanceInFiat);\n candidates.forEach((c, i) => map.set(c.key, i));\n return map;\n}\n\nfunction sortSourceIdsByPriority(params: {\n sourceIds: Iterable;\n swapBalance: TokenBalance[] | null;\n destination: Pick<\n DestinationConfig,\n \"chainId\" | \"tokenAddress\" | \"tokenSymbol\"\n >;\n minimumBalanceUsd?: number;\n}): string[] {\n return buildSortedSourceCandidates(params).map((item) => item.sourceId);\n}\n\nfunction buildSortedSourceCandidates(params: {\n sourceIds: Iterable;\n swapBalance: TokenBalance[] | null;\n destination: Pick<\n DestinationConfig,\n \"chainId\" | \"tokenAddress\" | \"tokenSymbol\"\n >;\n minimumBalanceUsd?: number;\n}): SourceCandidate[] {\n const { sourceIds, swapBalance, destination, minimumBalanceUsd } = params;\n const uniqueIds = [...new Set(sourceIds)];\n if (uniqueIds.length === 0) return [];\n\n const sourceFiatByKeyMap = buildSourceFiatByKeyMap(swapBalance);\n const priorityRankMap = buildPriorityRankMap(swapBalance, destination);\n\n return uniqueIds\n .map((sourceId) => {\n const parsed = parseSourceId(sourceId);\n if (!parsed) return null;\n\n const fiatKey = getFiatLookupKey(parsed.tokenAddress, parsed.chainId);\n const priorityKey = getPriorityLookupKey(\n parsed.tokenAddress,\n parsed.chainId,\n );\n const balanceInFiat = sourceFiatByKeyMap.get(fiatKey) ?? 0;\n const priorityRank =\n priorityRankMap.get(priorityKey) ?? MAX_PRIORITY_RANK;\n\n return {\n sourceId,\n balanceInFiat,\n priorityRank,\n };\n })\n .filter((item): item is NonNullable => {\n if (!item) return false;\n if (minimumBalanceUsd == null) return true;\n return item.balanceInFiat >= minimumBalanceUsd;\n })\n .sort((a, b) => {\n if (a.priorityRank !== b.priorityRank) {\n return a.priorityRank - b.priorityRank;\n }\n if (a.balanceInFiat !== b.balanceInFiat) {\n return b.balanceInFiat - a.balanceInFiat;\n }\n return a.sourceId.localeCompare(b.sourceId);\n });\n}\n\nexport function buildDepositSourcePoolIds(params: {\n swapBalance: TokenBalance[] | null;\n filter: AssetFilterType;\n selectedSourceIds: Iterable;\n isManualSelection: boolean;\n}): string[] {\n const { swapBalance, filter, selectedSourceIds, isManualSelection } = params;\n const selectedSourceIdSet = new Set(selectedSourceIds);\n\n if (isManualSelection) {\n return [...selectedSourceIdSet];\n }\n\n const sourceIds = new Set();\n\n swapBalance?.forEach((asset) => {\n asset.chainBalances?.forEach((breakdown) => {\n const chainId = breakdown.chain?.id;\n const tokenAddress = breakdown.contractAddress;\n if (!chainId || !tokenAddress) return;\n\n // v2: breakdown has no .symbol — use parent asset.symbol\n const stable = isStablecoin(asset.symbol);\n const native = isNative(asset.symbol);\n const sourceId = `${tokenAddress}-${chainId}`;\n const include =\n filter === \"all\" ||\n (filter === \"stablecoins\" && stable) ||\n (filter === \"native\" && native) ||\n (filter === \"custom\" && selectedSourceIdSet.has(sourceId));\n\n if (include) {\n sourceIds.add(sourceId);\n }\n });\n });\n\n return [...sourceIds];\n}\n\nexport interface ResolvedDepositSourceSelection {\n sourcePoolIds: string[];\n selectedSourceIds: string[];\n fromSources: Array<{ tokenAddress: Hex; chainId: number }>;\n}\n\nexport function resolveDepositSourceSelection(params: {\n swapBalance: TokenBalance[] | null;\n destination: Pick<\n DestinationConfig,\n \"chainId\" | \"tokenAddress\" | \"tokenSymbol\"\n >;\n filter: AssetFilterType;\n selectedSourceIds: Iterable;\n isManualSelection: boolean;\n minimumBalanceUsd: number;\n targetAmountUsd?: number;\n}): ResolvedDepositSourceSelection {\n const {\n swapBalance,\n destination,\n filter,\n selectedSourceIds,\n isManualSelection,\n minimumBalanceUsd,\n targetAmountUsd,\n } = params;\n\n const sourcePoolIds = buildDepositSourcePoolIds({\n swapBalance,\n filter,\n selectedSourceIds,\n isManualSelection,\n });\n\n const resolvedSelectedSourceIds = isManualSelection\n ? sortSourceIdsByPriority({\n sourceIds: sourcePoolIds,\n swapBalance,\n destination,\n minimumBalanceUsd,\n })\n : buildPrioritySelectedSourceIds({\n swapBalance,\n destination,\n minimumBalanceUsd,\n targetAmountUsd,\n sourceIds: sourcePoolIds,\n });\n\n const fromSources = buildSortedFromSources({\n sourceIds: resolvedSelectedSourceIds,\n swapBalance,\n destination,\n minimumBalanceUsd,\n });\n\n return {\n sourcePoolIds,\n selectedSourceIds: resolvedSelectedSourceIds,\n fromSources,\n };\n}\n\nexport function buildSelectableSourceIds(params: {\n swapBalance: TokenBalance[] | null;\n destination: Pick<\n DestinationConfig,\n \"chainId\" | \"tokenAddress\" | \"tokenSymbol\"\n >;\n minimumBalanceUsd: number;\n}): string[] {\n const { swapBalance, destination, minimumBalanceUsd } = params;\n const sourceIds = new Set();\n\n if (!swapBalance) return [];\n\n for (const asset of swapBalance) {\n for (const breakdown of asset.chainBalances ?? []) {\n const chainId = breakdown.chain?.id;\n const tokenAddress = breakdown.contractAddress;\n if (!chainId || !tokenAddress) continue;\n\n sourceIds.add(`${tokenAddress}-${chainId}`);\n }\n }\n\n return sortSourceIdsByPriority({\n sourceIds,\n swapBalance,\n destination,\n minimumBalanceUsd,\n });\n}\n\nexport function buildPrioritySelectedSourceIds(params: {\n swapBalance: TokenBalance[] | null;\n destination: Pick<\n DestinationConfig,\n \"chainId\" | \"tokenAddress\" | \"tokenSymbol\"\n >;\n minimumBalanceUsd: number;\n targetAmountUsd?: number;\n sourceIds?: Iterable;\n}): string[] {\n const {\n swapBalance,\n destination,\n minimumBalanceUsd,\n targetAmountUsd,\n sourceIds,\n } = params;\n\n const requestedSourceIds = sourceIds ? [...new Set(sourceIds)] : undefined;\n const orderedCandidateSourceIds = requestedSourceIds\n ? sortSourceIdsByPriority({\n sourceIds: requestedSourceIds,\n swapBalance,\n destination,\n minimumBalanceUsd,\n })\n : buildSelectableSourceIds({\n swapBalance,\n destination,\n minimumBalanceUsd,\n });\n\n if (orderedCandidateSourceIds.length === 0) return [];\n\n const normalizedTargetAmountUsd = parseNonNegativeNumber(targetAmountUsd);\n if (normalizedTargetAmountUsd <= 0) {\n const defaultSourceIds = [orderedCandidateSourceIds[0]];\n\n return defaultSourceIds;\n }\n\n const sourceFiatByKeyMap = buildSourceFiatByKeyMap(swapBalance);\n const priorityRankMap = buildPriorityRankMap(swapBalance, destination);\n const selectedSourceIds: string[] = [];\n let runningTotalUsd = 0;\n\n for (const sourceId of orderedCandidateSourceIds) {\n const parsed = parseSourceId(sourceId);\n if (!parsed) continue;\n\n selectedSourceIds.push(sourceId);\n runningTotalUsd +=\n sourceFiatByKeyMap.get(\n getFiatLookupKey(parsed.tokenAddress, parsed.chainId),\n ) ?? 0;\n\n if (runningTotalUsd >= normalizedTargetAmountUsd) {\n break;\n }\n }\n\n return selectedSourceIds;\n}\n\nexport function buildSortedFromSources(params: {\n sourceIds: Iterable;\n swapBalance: TokenBalance[] | null;\n destination: Pick<\n DestinationConfig,\n \"chainId\" | \"tokenAddress\" | \"tokenSymbol\"\n >;\n minimumBalanceUsd?: number;\n}): Array<{ tokenAddress: Hex; chainId: number }> {\n const requestedSourceIds = [...new Set(params.sourceIds)];\n const orderedIds = sortSourceIdsByPriority({\n ...params,\n sourceIds: requestedSourceIds,\n });\n const orderedSources = orderedIds\n .map((sourceId) => parseSourceId(sourceId))\n .filter((item): item is NonNullable => Boolean(item));\n\n return orderedSources;\n}\n\nexport function formatFeeUsd(amountUsd: number): string {\n if (amountUsd > 0 && amountUsd < 0.001) {\n return \"< $0.001\";\n }\n return formatUsdForDisplay(amountUsd);\n}\n\nexport function formatSignedUsd(value: number): string {\n if (!Number.isFinite(value) || value === 0) return \"$0.00\";\n const sign = value < 0 ? \"-\" : \"+\";\n const absolute = Math.abs(value);\n const absoluteLabel =\n absolute < 0.001 ? \"< $0.001\" : formatUsdForDisplay(absolute);\n return `${sign}${absoluteLabel}`;\n}\n\nexport function formatImpactPercent(value: number): string {\n if (!Number.isFinite(value) || value === 0) return \"0%\";\n const absolute = Math.abs(value);\n if (absolute < 0.01) {\n return \"< 0.01%\";\n }\n const sign = value < 0 ? \"-\" : \"+\";\n const fixed = absolute.toFixed(2);\n return `${sign}${fixed.replace(/\\.?0+$/, \"\")}%`;\n}\n", "type": "registry:component", "target": "components/deposit/utils.ts" }, @@ -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-sdk-v2\";\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.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.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.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 // v2: RFF_FEE_EXPIRED was removed; use string key for forward compat\n [\"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 );\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 as unknown as { data?: { context?: unknown } })?.data?.context,\n details: (err as unknown as { data?: { details?: unknown } })?.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" }, @@ -273,13 +273,13 @@ }, { "path": "registry/nexus-elements/common/hooks/useTransactionExecution.ts", - "content": "import {\n type BridgeStepType,\n NEXUS_EVENTS,\n type NexusSDK,\n type OnAllowanceHookData,\n type OnIntentHookData,\n} from \"@avail-project/nexus-core\";\nimport {\n type Dispatch,\n type RefObject,\n type SetStateAction,\n useCallback,\n useRef,\n} from \"react\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport {\n type SourceSelectionValidation,\n type TransactionFlowEvent,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n} from \"../types/transaction-flow\";\n\ninterface NexusErrorInfo {\n code: string;\n message: string;\n context?: unknown;\n details?: unknown;\n}\n\ntype NexusErrorHandler = (error: unknown) => NexusErrorInfo;\n\ninterface UseTransactionExecutionProps {\n operationName: \"bridge\" | \"transfer\";\n nexusSDK: NexusSDK | null;\n intent: RefObject;\n allowance: RefObject;\n inputs: TransactionFlowInputs;\n configuredMaxAmount?: string;\n allAvailableSourceChainIds: number[];\n sourceChainsForSdk?: number[];\n sourceSelectionKey: string;\n sourceSelection: SourceSelectionValidation;\n loading: boolean;\n txError: string | null;\n areInputsValid: boolean;\n executeTransaction: TransactionFlowExecutor;\n getMaxForCurrentSelection: () => Promise;\n onStepsList: (steps: BridgeStepType[]) => void;\n onStepComplete: (step: BridgeStepType) => void;\n resetSteps: () => void;\n setStatus: (status: TransactionStatus) => void;\n resetInputs: () => void;\n setRefreshing: Dispatch>;\n setIsDialogOpen: Dispatch>;\n setTxError: Dispatch>;\n setLastExplorerUrl: Dispatch>;\n setSelectedSourceChains: Dispatch>;\n setAppliedSourceSelectionKey: Dispatch>;\n stopwatch: {\n start: () => void;\n stop: () => void;\n reset: () => void;\n };\n handleNexusError: NexusErrorHandler;\n onStart?: () => void;\n onComplete?: (explorerUrl?: string) => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n notifyHistoryRefresh?: () => void;\n}\n\nexport function useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n}: UseTransactionExecutionProps) {\n const commitLockRef = useRef(false);\n const runIdRef = useRef(0);\n\n const refreshIntent = async (options?: { reportError?: boolean }) => {\n if (!intent.current) return false;\n const activeRunId = runIdRef.current;\n setRefreshing(true);\n try {\n const updated = await intent.current.refresh(sourceChainsForSdk);\n if (activeRunId !== runIdRef.current) return false;\n if (updated) {\n intent.current.intent = updated;\n }\n setAppliedSourceSelectionKey(sourceSelectionKey);\n return true;\n } catch (error) {\n if (activeRunId !== runIdRef.current) return false;\n console.error(\"Transaction failed:\", error);\n if (options?.reportError) {\n const message = \"Unable to refresh source selection. Please try again.\";\n setTxError(message);\n onError?.(message);\n }\n return false;\n } finally {\n if (activeRunId === runIdRef.current) {\n setRefreshing(false);\n }\n }\n };\n\n const onSuccess = async (explorerUrl?: string) => {\n stopwatch.stop();\n setStatus(\"success\");\n onComplete?.(explorerUrl);\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n await fetchBalance();\n notifyHistoryRefresh?.();\n };\n\n const handleTransaction = async () => {\n if (commitLockRef.current) return;\n commitLockRef.current = true;\n const currentRunId = ++runIdRef.current;\n let didEnterExecutingState = false;\n const cleanupSupersededExecution = () => {\n if (!didEnterExecutingState) return;\n setRefreshing(false);\n setIsDialogOpen(false);\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n setStatus(\"idle\");\n };\n\n try {\n if (\n !inputs?.amount ||\n !inputs?.recipient ||\n !inputs?.chain ||\n !inputs?.token\n ) {\n console.error(\"Missing required inputs\");\n return;\n }\n if (!nexusSDK) {\n const message = \"Nexus SDK not initialized\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n const parsedAmount = Number(inputs.amount);\n if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) {\n const message = \"Enter a valid amount greater than 0.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n const amountBigInt = nexusSDK.convertTokenReadableAmountToBigInt(\n inputs.amount,\n inputs.token,\n inputs.chain,\n );\n\n if (configuredMaxAmount) {\n const configuredMaxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n configuredMaxAmount,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > configuredMaxRaw) {\n const message = `Amount exceeds maximum limit of ${configuredMaxAmount} ${inputs.token}.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n }\n\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (currentRunId !== runIdRef.current) return;\n if (!maxForCurrentSelection) {\n const message = `Unable to determine max ${operationName} amount for selected sources. Please try again.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n const maxForSelectionRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxForCurrentSelection,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > maxForSelectionRaw) {\n const message = `Selected sources can provide up to ${maxForCurrentSelection} ${inputs.token}. Reduce amount or enable more sources.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n setStatus(\"executing\");\n didEnterExecutingState = true;\n setTxError(null);\n onStart?.();\n setLastExplorerUrl(\"\");\n setAppliedSourceSelectionKey(sourceSelectionKey);\n\n const onEvent = (event: TransactionFlowEvent) => {\n if (currentRunId !== runIdRef.current) return;\n if (event.name === NEXUS_EVENTS.STEPS_LIST) {\n const list = Array.isArray(event.args) ? event.args : [];\n onStepsList(list as BridgeStepType[]);\n }\n if (event.name === NEXUS_EVENTS.STEP_COMPLETE) {\n if (\n !Array.isArray(event.args) &&\n \"type\" in event.args &&\n event.args.type === \"INTENT_HASH_SIGNED\"\n ) {\n stopwatch.start();\n }\n if (!Array.isArray(event.args)) {\n onStepComplete(event.args as BridgeStepType);\n }\n }\n };\n\n const transactionResult = await executeTransaction({\n token: inputs.token,\n amount: amountBigInt,\n toChainId: inputs.chain,\n recipient: inputs.recipient,\n sourceChains: sourceChainsForSdk,\n onEvent,\n });\n\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution();\n return;\n }\n if (!transactionResult) {\n throw new Error(\"Transaction rejected by user\");\n }\n setLastExplorerUrl(transactionResult.explorerUrl);\n await onSuccess(transactionResult.explorerUrl);\n } catch (error) {\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution();\n return;\n }\n const { message, code, context, details } = handleNexusError(error);\n console.error(`Fast ${operationName} transaction failed:`, {\n code,\n message,\n context,\n details,\n });\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n setTxError(message);\n onError?.(message);\n setIsDialogOpen(false);\n setSelectedSourceChains(null);\n setRefreshing(false);\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n void fetchBalance();\n setStatus(\"error\");\n } finally {\n commitLockRef.current = false;\n }\n };\n\n const reset = () => {\n runIdRef.current += 1;\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setStatus(\"idle\");\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n };\n\n const startTransaction = () => {\n if (!intent.current) return;\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (sourceSelection.isBelowRequired && inputs?.token) {\n const message = `Selected sources are not enough. Add ${sourceSelection.missingToProceed} ${inputs.token} more to make this transaction.`;\n setTxError(message);\n onError?.(message);\n return;\n }\n void (async () => {\n const refreshed = await refreshIntent({ reportError: true });\n if (!refreshed || !intent.current) return;\n intent.current.allow();\n setIsDialogOpen(true);\n setTxError(null);\n })();\n };\n\n const commitAmount = async () => {\n if (intent.current || loading || txError || !areInputsValid) return;\n await handleTransaction();\n };\n\n const invalidatePendingExecution = useCallback(() => {\n runIdRef.current += 1;\n if (intent.current) {\n intent.current.deny();\n intent.current = null;\n }\n setRefreshing(false);\n setAppliedSourceSelectionKey(\"ALL\");\n }, [intent, setAppliedSourceSelectionKey, setRefreshing]);\n\n return {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n };\n}\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type { OnAllowanceHookData, OnIntentHookData } from \"@avail-project/nexus-sdk-v2\";\nimport {\n type Dispatch,\n type RefObject,\n type SetStateAction,\n useCallback,\n useRef,\n} from \"react\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport {\n type SourceSelectionValidation,\n type TransactionFlowEvent,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n} from \"../types/transaction-flow\";\n\ntype NexusClient = ReturnType;\n\ninterface NexusErrorInfo {\n code: string;\n message: string;\n context?: unknown;\n details?: unknown;\n}\n\ntype NexusErrorHandler = (error: unknown) => NexusErrorInfo;\n\n// v2 plan_progress step types for bridge\nconst BRIDGE_STEP_INTENT_SIGNED = \"request_signing\";\n\ninterface UseTransactionExecutionProps {\n operationName: \"bridge\" | \"transfer\";\n nexusSDK: NexusClient | null;\n intent: RefObject;\n allowance: RefObject;\n inputs: TransactionFlowInputs;\n configuredMaxAmount?: string;\n allAvailableSourceChainIds: number[];\n sourceChainsForSdk?: number[];\n sourceSelectionKey: string;\n sourceSelection: SourceSelectionValidation;\n loading: boolean;\n txError: string | null;\n areInputsValid: boolean;\n executeTransaction: TransactionFlowExecutor;\n getMaxForCurrentSelection: () => Promise;\n onStepsList: (steps: { typeID?: string; type?: string; [key: string]: unknown }[]) => void;\n onStepComplete: (step: { typeID?: string; type?: string; [key: string]: unknown }) => void;\n resetSteps: () => void;\n setStatus: (status: TransactionStatus) => void;\n resetInputs: () => void;\n setRefreshing: Dispatch>;\n setIsDialogOpen: Dispatch>;\n setTxError: Dispatch>;\n setLastExplorerUrl: Dispatch>;\n setSelectedSourceChains: Dispatch>;\n setAppliedSourceSelectionKey: Dispatch>;\n stopwatch: {\n start: () => void;\n stop: () => void;\n reset: () => void;\n };\n handleNexusError: NexusErrorHandler;\n onStart?: () => void;\n onComplete?: (explorerUrl?: string) => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n notifyHistoryRefresh?: () => void;\n}\n\nexport function useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n}: UseTransactionExecutionProps) {\n const commitLockRef = useRef(false);\n const runIdRef = useRef(0);\n\n const refreshIntent = async (options?: { reportError?: boolean }) => {\n if (!intent.current) return false;\n const activeRunId = runIdRef.current;\n setRefreshing(true);\n try {\n const updated = await intent.current.refresh(sourceChainsForSdk);\n if (activeRunId !== runIdRef.current) return false;\n if (updated) {\n intent.current.intent = updated;\n }\n setAppliedSourceSelectionKey(sourceSelectionKey);\n return true;\n } catch (error) {\n if (activeRunId !== runIdRef.current) return false;\n console.error(\"Transaction failed:\", error);\n if (options?.reportError) {\n const message = \"Unable to refresh source selection. Please try again.\";\n setTxError(message);\n onError?.(message);\n }\n return false;\n } finally {\n if (activeRunId === runIdRef.current) {\n setRefreshing(false);\n }\n }\n };\n\n const onSuccess = async (explorerUrl?: string) => {\n stopwatch.stop();\n setStatus(\"success\");\n onComplete?.(explorerUrl);\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n await fetchBalance();\n notifyHistoryRefresh?.();\n };\n\n const handleTransaction = async () => {\n if (commitLockRef.current) return;\n commitLockRef.current = true;\n const currentRunId = ++runIdRef.current;\n let didEnterExecutingState = false;\n // Declared here (outside try/catch) so both the event handler and the catch block\n // can read/write it — prevents the catch from clobbering event-driven completions\n let completedFromEvent = false;\n const cleanupSupersededExecution = () => {\n if (!didEnterExecutingState) return;\n // Don't tear down the dialog if an event already handled success/failure —\n // resetInputs() inside onSuccess triggers invalidatePendingExecution which\n // increments runIdRef, making this branch fire spuriously.\n if (completedFromEvent) return;\n setRefreshing(false);\n setIsDialogOpen(false);\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n setStatus(\"idle\");\n };\n\n\n try {\n if (\n !inputs?.amount ||\n !inputs?.recipient ||\n !inputs?.chain ||\n !inputs?.token\n ) {\n console.error(\"Missing required inputs\");\n return;\n }\n if (!nexusSDK) {\n const message = \"Nexus SDK not initialized\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n const parsedAmount = Number(inputs.amount);\n if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) {\n const message = \"Enter a valid amount greater than 0.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n // v2: convertTokenReadableAmountToBigInt(amount, tokenSymbol, chainId)\n const amountBigInt = nexusSDK.convertTokenReadableAmountToBigInt(\n inputs.amount,\n inputs.token,\n inputs.chain,\n );\n\n if (configuredMaxAmount) {\n const configuredMaxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n configuredMaxAmount,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > configuredMaxRaw) {\n const message = `Amount exceeds maximum limit of ${configuredMaxAmount} ${inputs.token}.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n }\n\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (currentRunId !== runIdRef.current) return;\n if (!maxForCurrentSelection) {\n const message = `Unable to determine max ${operationName} amount for selected sources. Please try again.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n const maxForSelectionRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxForCurrentSelection,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > maxForSelectionRaw) {\n const message = `Selected sources can provide up to ${maxForCurrentSelection} ${inputs.token}. Reduce amount or enable more sources.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n setStatus(\"executing\");\n didEnterExecutingState = true;\n setTxError(null);\n onStart?.();\n setLastExplorerUrl(\"\");\n setAppliedSourceSelectionKey(sourceSelectionKey);\n\n // Terminal step types — when state:\"completed\" fires on these, the operation is done\n const TERMINAL_STEP_TYPES = new Set([\n \"bridge_fill\", // bridge & transfer final fill\n \"destination_swap\", // swap final step\n ]);\n\n // v2 onEvent uses typed discriminated union: { type, ... }\n const onEvent = (event: TransactionFlowEvent) => {\n if (currentRunId !== runIdRef.current) return;\n\n if (event.type === \"plan_preview\") {\n // Seed UI with the step list from the plan\n type StepShape = { typeID?: string; type?: string; [key: string]: unknown };\n const steps = ((event as { type: string; plan: { steps: StepShape[] } }).plan?.steps ?? []) as StepShape[];\n onStepsList(steps);\n }\n\n if (event.type === \"plan_progress\") {\n const progressEvent = event as {\n type: string;\n stepType: string;\n state: string;\n step: { typeID?: string; type?: string; [key: string]: unknown };\n error?: string;\n };\n\n // Always mark step as complete/updated in UI\n onStepComplete(progressEvent.step);\n\n const isTerminal = TERMINAL_STEP_TYPES.has(progressEvent.stepType);\n\n if (progressEvent.state === \"failed\") {\n // Any step failure → abort\n if (!completedFromEvent) {\n completedFromEvent = true;\n const errorMessage = progressEvent.error ?? \"Transaction failed\";\n stopwatch.stop();\n setTxError(errorMessage);\n onError?.(errorMessage);\n setStatus(\"error\");\n }\n return;\n }\n\n if (isTerminal && progressEvent.state === \"completed\") {\n // Terminal step completed → success\n if (!completedFromEvent) {\n completedFromEvent = true;\n stopwatch.stop();\n // explorerUrl is on the event itself, not the step object\n const explorerUrl = (event as { explorerUrl?: string }).explorerUrl;\n if (explorerUrl) setLastExplorerUrl(explorerUrl);\n void onSuccess(explorerUrl);\n }\n }\n }\n\n if (event.type === \"status\") {\n const statusEvent = event as { type: string; status: string };\n if (statusEvent.status === \"completed\" && !completedFromEvent) {\n completedFromEvent = true;\n stopwatch.stop();\n void onSuccess(undefined);\n }\n }\n };\n\n const transactionResult = await executeTransaction({\n token: inputs.token,\n amount: amountBigInt,\n toChainId: inputs.chain,\n recipient: inputs.recipient,\n sources: sourceChainsForSdk,\n onEvent,\n });\n\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution(); // no-op when completedFromEvent=true\n if (!completedFromEvent) return; // only bail if not already completed\n // else fall through — still want to capture explorerUrl from the result\n }\n if (!transactionResult) {\n if (!completedFromEvent) {\n throw new Error(\"Transaction rejected by user\");\n }\n // Already handled via events\n return;\n }\n\n // SDK promise resolved — use result for explorerUrl if event-driven success didn't set it\n if (!completedFromEvent) {\n // Fallback: SDK resolved but we never got a terminal event (e.g. single-step flows)\n setLastExplorerUrl(transactionResult.explorerUrl ?? \"\");\n await onSuccess(transactionResult.explorerUrl);\n } else {\n // Event-driven success already ran — capture the explorerUrl from the resolved result\n if (transactionResult.explorerUrl) {\n setLastExplorerUrl(transactionResult.explorerUrl);\n }\n }\n\n } catch (error) {\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution();\n return;\n }\n // If event-driven success/failure already handled this transaction, ignore SDK-level errors\n // (the SDK may throw or return oddly after a successful fill event)\n if (completedFromEvent) return;\n const { message, code, context, details } = handleNexusError(error);\n console.error(`Fast ${operationName} transaction failed:`, {\n code,\n message,\n context,\n details,\n });\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n setTxError(message);\n onError?.(message);\n setIsDialogOpen(false);\n setSelectedSourceChains(null);\n setRefreshing(false);\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n void fetchBalance();\n setStatus(\"error\");\n } finally {\n commitLockRef.current = false;\n }\n };\n\n const reset = () => {\n runIdRef.current += 1;\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setStatus(\"idle\");\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n };\n\n const startTransaction = () => {\n if (!intent.current) return;\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (sourceSelection.isBelowRequired && inputs?.token) {\n const message = `Selected sources are not enough. Add ${sourceSelection.missingToProceed} ${inputs.token} more to make this transaction.`;\n setTxError(message);\n onError?.(message);\n return;\n }\n void (async () => {\n const refreshed = await refreshIntent({ reportError: true });\n if (!refreshed || !intent.current) return;\n intent.current.allow();\n setIsDialogOpen(true);\n // Start the stopwatch AFTER the dialog opens so the isDialogOpen effect\n // does not immediately reset it (the effect only resets when dialog is closed)\n stopwatch.reset();\n stopwatch.start();\n setTxError(null);\n })();\n };\n\n const commitAmount = async () => {\n if (intent.current || loading || txError || !areInputsValid) return;\n await handleTransaction();\n };\n\n const invalidatePendingExecution = useCallback(() => {\n runIdRef.current += 1;\n if (intent.current) {\n intent.current.deny();\n intent.current = null;\n }\n setRefreshing(false);\n setAppliedSourceSelectionKey(\"ALL\");\n }, [intent, setAppliedSourceSelectionKey, setRefreshing]);\n\n return {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n };\n}\n", "type": "registry:component", "target": "components/common/hooks/useTransactionExecution.ts" }, { "path": "registry/nexus-elements/common/hooks/useTransactionFlow.ts", - "content": "import {\n type BridgeStepType,\n type NexusNetwork,\n NexusSDK,\n type OnAllowanceHookData,\n type OnIntentHookData,\n parseUnits,\n type UserAsset,\n} from \"@avail-project/nexus-core\";\nimport {\n useEffect,\n useMemo,\n useCallback,\n useRef,\n useState,\n useReducer,\n type RefObject,\n} from \"react\";\nimport { type Address, isAddress } from \"viem\";\nimport { useNexusError } from \"./useNexusError\";\nimport { useTransactionExecution } from \"./useTransactionExecution\";\nimport { usePolling } from \"./usePolling\";\nimport { useStopwatch } from \"./useStopwatch\";\nimport { useDebouncedCallback } from \"./useDebouncedCallback\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport { useTransactionSteps } from \"../tx/useTransactionSteps\";\nimport {\n type SourceCoverageState,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n type TransactionFlowPrefill,\n type TransactionFlowType,\n} from \"../types/transaction-flow\";\nimport {\n MAX_AMOUNT_DEBOUNCE_MS,\n buildInitialInputs,\n clampAmountToMax,\n formatAmountForDisplay,\n getCoverageDecimals,\n normalizeMaxAmount,\n} from \"../utils/transaction-flow\";\n\ninterface BaseTransactionFlowProps {\n type: TransactionFlowType;\n network: NexusNetwork;\n nexusSDK: NexusSDK | null;\n intent: RefObject;\n allowance: RefObject;\n bridgableBalance: UserAsset[] | null;\n prefill?: TransactionFlowPrefill;\n onComplete?: (explorerUrl?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n maxAmount?: string | number;\n isSourceMenuOpen?: boolean;\n notifyHistoryRefresh?: () => void;\n executeTransaction: TransactionFlowExecutor;\n}\n\nexport interface UseTransactionFlowProps extends BaseTransactionFlowProps {\n connectedAddress?: Address;\n}\n\ntype State = {\n inputs: TransactionFlowInputs;\n status: TransactionStatus;\n};\n\ntype Action =\n | { type: \"setInputs\"; payload: Partial }\n | { type: \"resetInputs\" }\n | { type: \"setStatus\"; payload: TransactionStatus };\n\nexport function useTransactionFlow(props: UseTransactionFlowProps) {\n const {\n type,\n network,\n nexusSDK,\n intent,\n bridgableBalance,\n prefill,\n onComplete,\n onStart,\n onError,\n fetchBalance,\n allowance,\n maxAmount,\n isSourceMenuOpen = false,\n notifyHistoryRefresh,\n executeTransaction,\n } = props;\n\n const connectedAddress = props.connectedAddress;\n const operationName = type === \"bridge\" ? \"bridge\" : \"transfer\";\n const handleNexusError = useNexusError();\n const initialState: State = {\n inputs: buildInitialInputs({ type, network, connectedAddress, prefill }),\n status: \"idle\",\n };\n\n function reducer(state: State, action: Action): State {\n switch (action.type) {\n case \"setInputs\":\n return { ...state, inputs: { ...state.inputs, ...action.payload } };\n case \"resetInputs\":\n return {\n ...state,\n inputs: buildInitialInputs({\n type,\n network,\n connectedAddress,\n prefill,\n }),\n };\n case \"setStatus\":\n return { ...state, status: action.payload };\n default:\n return state;\n }\n }\n\n const [state, dispatch] = useReducer(reducer, initialState);\n const inputs = state.inputs;\n const setInputs = (\n next: TransactionFlowInputs | Partial,\n ) => {\n dispatch({\n type: \"setInputs\",\n payload: next as Partial,\n });\n };\n\n const loading = state.status === \"executing\";\n const [refreshing, setRefreshing] = useState(false);\n const [isDialogOpen, setIsDialogOpen] = useState(false);\n const [txError, setTxError] = useState(null);\n const [lastExplorerUrl, setLastExplorerUrl] = useState(\"\");\n const previousConnectedAddressRef = useRef
(\n connectedAddress,\n );\n const maxAmountRequestIdRef = useRef(0);\n const [selectedSourceChains, setSelectedSourceChains] = useState<\n number[] | null\n >(null);\n const [selectedSourcesMaxAmount, setSelectedSourcesMaxAmount] = useState<\n string | null\n >(null);\n const [appliedSourceSelectionKey, setAppliedSourceSelectionKey] =\n useState(\"ALL\");\n const {\n steps,\n onStepsList,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n const configuredMaxAmount = useMemo(\n () => normalizeMaxAmount(maxAmount),\n [maxAmount],\n );\n\n const areInputsValid = useMemo(() => {\n const hasToken = inputs?.token !== undefined && inputs?.token !== null;\n const hasChain = inputs?.chain !== undefined && inputs?.chain !== null;\n const hasAmount = Boolean(inputs?.amount) && Number(inputs?.amount) > 0;\n const hasValidRecipient =\n Boolean(inputs?.recipient) && isAddress(inputs.recipient as string);\n return hasToken && hasChain && hasAmount && hasValidRecipient;\n }, [inputs]);\n\n const filteredBridgableBalance = useMemo(() => {\n return bridgableBalance?.find((bal) =>\n inputs?.token === \"USDM\"\n ? bal?.symbol === \"USDC\"\n : bal?.symbol === inputs?.token,\n );\n }, [bridgableBalance, inputs?.token]);\n\n const availableSources = useMemo(() => {\n const breakdown = filteredBridgableBalance?.breakdown ?? [];\n const destinationChainId = inputs?.chain;\n const nonZero = breakdown.filter((source) => {\n if (Number.parseFloat(source.balance ?? \"0\") <= 0) return false;\n if (typeof destinationChainId === \"number\") {\n return source.chain.id !== destinationChainId;\n }\n return true;\n });\n const decimals = filteredBridgableBalance?.decimals;\n if (!nexusSDK || typeof decimals !== \"number\") {\n return nonZero.sort(\n (a, b) => Number.parseFloat(b.balance) - Number.parseFloat(a.balance),\n );\n }\n return nonZero.sort((a, b) => {\n try {\n const aRaw = parseUnits(a.balance ?? \"0\", decimals);\n const bRaw = parseUnits(b.balance ?? \"0\", decimals);\n if (aRaw === bRaw) return 0;\n return aRaw > bRaw ? -1 : 1;\n } catch {\n return Number.parseFloat(b.balance) - Number.parseFloat(a.balance);\n }\n });\n }, [\n inputs?.chain,\n filteredBridgableBalance?.breakdown,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n ]);\n\n const allAvailableSourceChainIds = useMemo(\n () => availableSources.map((source) => source.chain.id),\n [availableSources],\n );\n\n const effectiveSelectedSourceChains = useMemo(() => {\n if (selectedSourceChains && selectedSourceChains.length > 0) {\n const availableSet = new Set(allAvailableSourceChainIds);\n const filteredSelection = selectedSourceChains.filter((id) =>\n availableSet.has(id),\n );\n if (filteredSelection.length > 0) {\n return filteredSelection;\n }\n }\n return allAvailableSourceChainIds;\n }, [selectedSourceChains, allAvailableSourceChainIds]);\n\n const sourceChainsForSdk =\n effectiveSelectedSourceChains.length > 0\n ? effectiveSelectedSourceChains\n : undefined;\n\n const sourceSelectionKey = useMemo(() => {\n if (allAvailableSourceChainIds.length === 0) return \"NONE\";\n if (!selectedSourceChains || selectedSourceChains.length === 0) {\n return \"ALL\";\n }\n return [...effectiveSelectedSourceChains].sort((a, b) => a - b).join(\"|\");\n }, [\n allAvailableSourceChainIds.length,\n effectiveSelectedSourceChains,\n selectedSourceChains,\n ]);\n const hasPendingSourceSelectionChanges =\n sourceSelectionKey !== appliedSourceSelectionKey;\n const intentSourceSpendAmount = intent.current?.intent?.sourcesTotal;\n\n const getMaxForCurrentSelection = useCallback(async () => {\n if (!nexusSDK || !inputs?.token || !inputs?.chain) return undefined;\n const maxBalAvailable = await nexusSDK.calculateMaxForBridge({\n token: inputs.token,\n toChainId: inputs.chain,\n recipient: inputs.recipient,\n sourceChains: sourceChainsForSdk,\n });\n if (!maxBalAvailable?.amount) return \"0\";\n return clampAmountToMax({\n amount: maxBalAvailable.amount,\n maxAmount: configuredMaxAmount,\n nexusSDK,\n token: inputs.token,\n chainId: inputs.chain,\n });\n }, [\n configuredMaxAmount,\n inputs?.chain,\n inputs?.recipient,\n inputs?.token,\n nexusSDK,\n sourceChainsForSdk,\n ]);\n\n const toggleSourceChain = useCallback(\n (chainId: number) => {\n setSelectedSourceChains((prev) => {\n if (allAvailableSourceChainIds.length === 0) return prev;\n const current =\n prev && prev.length > 0 ? prev : allAvailableSourceChainIds;\n const next = current.includes(chainId)\n ? current.filter((id) => id !== chainId)\n : [...current, chainId];\n if (next.length === 0) {\n return current;\n }\n const isAllSelected =\n next.length === allAvailableSourceChainIds.length &&\n allAvailableSourceChainIds.every((id) => next.includes(id));\n return isAllSelected ? null : next;\n });\n },\n [allAvailableSourceChainIds],\n );\n\n const sourceSelection = useMemo(() => {\n const amount =\n intentSourceSpendAmount?.trim() ?? inputs?.amount?.trim() ?? \"\";\n const decimals = getCoverageDecimals({\n type,\n token: inputs?.token,\n chainId: inputs?.chain,\n fallback: filteredBridgableBalance?.decimals,\n });\n const selectedChainSet = new Set(effectiveSelectedSourceChains);\n const selectedTotalRaw =\n !nexusSDK || typeof decimals !== \"number\"\n ? BigInt(0)\n : availableSources.reduce((sum, source) => {\n if (!selectedChainSet.has(source.chain.id)) return sum;\n try {\n return sum + parseUnits(source.balance ?? \"0\", decimals);\n } catch {\n return sum;\n }\n }, BigInt(0));\n const selectedTotal =\n !nexusSDK || typeof decimals !== \"number\"\n ? \"0\"\n : formatAmountForDisplay(selectedTotalRaw, decimals, nexusSDK);\n const baseSelection = {\n selectedTotal,\n requiredTotal: amount || \"0\",\n requiredSafetyTotal: amount || \"0\",\n missingToProceed: \"0\",\n missingToSafety: \"0\",\n coverageState: \"healthy\" as SourceCoverageState,\n coverageToSafetyPercent: 100,\n isBelowRequired: false,\n isBelowSafetyBuffer: false,\n };\n\n if (!nexusSDK || !inputs?.token || !inputs?.chain || !amount) {\n return baseSelection;\n }\n\n try {\n const requiredRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n inputs.token,\n inputs.chain,\n );\n if (requiredRaw <= BigInt(0)) {\n return baseSelection;\n }\n\n const missingToProceedRaw =\n selectedTotalRaw >= requiredRaw\n ? BigInt(0)\n : requiredRaw - selectedTotalRaw;\n const missingToSafetyRaw = missingToProceedRaw;\n\n const coverageState: SourceCoverageState =\n selectedTotalRaw < requiredRaw ? \"error\" : \"healthy\";\n\n const coverageBasisPoints =\n requiredRaw === BigInt(0)\n ? 10_000\n : selectedTotalRaw >= requiredRaw\n ? 10_000\n : Number((selectedTotalRaw * BigInt(10_000)) / requiredRaw);\n\n return {\n selectedTotal,\n requiredTotal: amount,\n requiredSafetyTotal: amount,\n missingToProceed: formatAmountForDisplay(\n missingToProceedRaw,\n decimals,\n nexusSDK,\n ),\n missingToSafety: formatAmountForDisplay(\n missingToSafetyRaw,\n decimals,\n nexusSDK,\n ),\n coverageState,\n coverageToSafetyPercent: coverageBasisPoints / 100,\n isBelowRequired: coverageState === \"error\",\n isBelowSafetyBuffer: coverageState === \"error\",\n };\n } catch {\n return baseSelection;\n }\n }, [\n type,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n inputs?.chain,\n inputs?.amount,\n inputs?.token,\n intentSourceSpendAmount,\n availableSources,\n effectiveSelectedSourceChains,\n ]);\n\n const stopwatch = useStopwatch({ intervalMs: 100 });\n const setStatus = useCallback(\n (status: TransactionStatus) =>\n dispatch({ type: \"setStatus\", payload: status }),\n [],\n );\n\n const resetInputs = useCallback(() => {\n dispatch({ type: \"resetInputs\" });\n }, []);\n\n const {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n } = useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n });\n\n usePolling(\n Boolean(intent.current) &&\n !isDialogOpen &&\n !isSourceMenuOpen &&\n !hasPendingSourceSelectionChanges,\n async () => {\n await refreshIntent();\n },\n 15000,\n );\n\n const debouncedRefreshMaxForSelection = useDebouncedCallback(\n async (requestId: number) => {\n try {\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (requestId !== maxAmountRequestIdRef.current) return;\n setSelectedSourcesMaxAmount(maxForCurrentSelection ?? \"0\");\n } catch (error) {\n if (requestId !== maxAmountRequestIdRef.current) return;\n console.error(\"Unable to calculate max for selected sources:\", error);\n setSelectedSourcesMaxAmount(\"0\");\n }\n },\n MAX_AMOUNT_DEBOUNCE_MS,\n );\n\n useEffect(() => {\n debouncedRefreshMaxForSelection.cancel();\n if (!nexusSDK || !inputs?.token || !inputs?.chain) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(null);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(\"0\");\n return;\n }\n const requestId = ++maxAmountRequestIdRef.current;\n debouncedRefreshMaxForSelection(requestId);\n }, [\n allAvailableSourceChainIds.length,\n configuredMaxAmount,\n debouncedRefreshMaxForSelection,\n inputs?.recipient,\n sourceSelectionKey,\n inputs?.chain,\n inputs?.token,\n nexusSDK,\n ]);\n\n useEffect(() => {\n if (type !== \"bridge\" || !connectedAddress) return;\n const previousConnectedAddress = previousConnectedAddressRef.current;\n if (!previousConnectedAddress) {\n previousConnectedAddressRef.current = connectedAddress;\n return;\n }\n if (connectedAddress === previousConnectedAddress) return;\n previousConnectedAddressRef.current = connectedAddress;\n if (prefill?.recipient) return;\n if (!inputs?.recipient || inputs.recipient === previousConnectedAddress) {\n dispatch({ type: \"setInputs\", payload: { recipient: connectedAddress } });\n }\n }, [type, connectedAddress, inputs?.recipient, prefill?.recipient]);\n\n useEffect(() => {\n invalidatePendingExecution();\n }, [inputs, invalidatePendingExecution]);\n\n useEffect(() => {\n setSelectedSourceChains(null);\n }, [inputs?.token]);\n\n useEffect(() => {\n if (isDialogOpen) return;\n stopwatch.stop();\n stopwatch.reset();\n if (state.status === \"success\" || state.status === \"error\") {\n resetSteps();\n setLastExplorerUrl(\"\");\n setStatus(\"idle\");\n }\n }, [isDialogOpen, resetSteps, setStatus, state.status, stopwatch]);\n\n useEffect(() => {\n if (txError) {\n setTxError(null);\n }\n }, [inputs, txError]);\n\n return {\n inputs,\n setInputs,\n timer: stopwatch.seconds,\n setIsDialogOpen,\n setTxError,\n loading,\n refreshing,\n isDialogOpen,\n txError,\n handleTransaction,\n reset,\n filteredBridgableBalance,\n startTransaction,\n commitAmount,\n lastExplorerUrl,\n steps,\n status: state.status,\n availableSources,\n selectedSourceChains: effectiveSelectedSourceChains,\n toggleSourceChain,\n isSourceSelectionInsufficient: sourceSelection.isBelowRequired,\n isSourceSelectionBelowSafetyBuffer: sourceSelection.isBelowSafetyBuffer,\n isSourceSelectionReadyForAccept:\n sourceSelection.coverageState === \"healthy\",\n sourceCoverageState: sourceSelection.coverageState,\n sourceCoveragePercent: sourceSelection.coverageToSafetyPercent,\n missingToProceed: sourceSelection.missingToProceed,\n missingToSafety: sourceSelection.missingToSafety,\n selectedTotal: sourceSelection.selectedTotal,\n requiredTotal: sourceSelection.requiredTotal,\n requiredSafetyTotal: sourceSelection.requiredSafetyTotal,\n maxAvailableAmount: selectedSourcesMaxAmount ?? undefined,\n isInputsValid: areInputsValid,\n };\n}\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type {\n NexusNetwork,\n OnAllowanceHookData,\n OnIntentHookData,\n TokenBalance,\n ChainBalance,\n} from \"@avail-project/nexus-sdk-v2\";\nimport { parseUnits } from \"viem\";\nimport {\n useEffect,\n useMemo,\n useCallback,\n useRef,\n useState,\n useReducer,\n type RefObject,\n} from \"react\";\nimport { type Address, isAddress } from \"viem\";\nimport { useNexusError } from \"./useNexusError\";\nimport { useTransactionExecution } from \"./useTransactionExecution\";\nimport { usePolling } from \"./usePolling\";\nimport { useStopwatch } from \"./useStopwatch\";\nimport { useDebouncedCallback } from \"./useDebouncedCallback\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport { useTransactionSteps } from \"../tx/useTransactionSteps\";\nimport {\n type SourceCoverageState,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n type TransactionFlowPrefill,\n type TransactionFlowType,\n} from \"../types/transaction-flow\";\nimport {\n MAX_AMOUNT_DEBOUNCE_MS,\n buildInitialInputs,\n clampAmountToMax,\n formatAmountForDisplay,\n getCoverageDecimals,\n normalizeMaxAmount,\n} from \"../utils/transaction-flow\";\n\ntype NexusClient = ReturnType;\n\n// v2 uses a generic step shape; minimal type to satisfy getStepKey constraint\ntype BridgePlanStep = {\n typeID?: string;\n type?: string;\n [key: string]: unknown;\n};\n\ninterface BaseTransactionFlowProps {\n type: TransactionFlowType;\n network: NexusNetwork;\n nexusSDK: NexusClient | null;\n intent: RefObject;\n allowance: RefObject;\n bridgableBalance: TokenBalance[] | null;\n prefill?: TransactionFlowPrefill;\n onComplete?: (explorerUrl?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n maxAmount?: string | number;\n isSourceMenuOpen?: boolean;\n notifyHistoryRefresh?: () => void;\n executeTransaction: TransactionFlowExecutor;\n}\n\nexport interface UseTransactionFlowProps extends BaseTransactionFlowProps {\n connectedAddress?: Address;\n}\n\ntype State = {\n inputs: TransactionFlowInputs;\n status: TransactionStatus;\n};\n\ntype Action =\n | { type: \"setInputs\"; payload: Partial }\n | { type: \"resetInputs\" }\n | { type: \"setStatus\"; payload: TransactionStatus };\n\nexport function useTransactionFlow(props: UseTransactionFlowProps) {\n const {\n type,\n network,\n nexusSDK,\n intent,\n bridgableBalance,\n prefill,\n onComplete,\n onStart,\n onError,\n fetchBalance,\n allowance,\n maxAmount,\n isSourceMenuOpen = false,\n notifyHistoryRefresh,\n executeTransaction,\n } = props;\n\n const connectedAddress = props.connectedAddress;\n const operationName = type === \"bridge\" ? \"bridge\" : \"transfer\";\n const handleNexusError = useNexusError();\n const initialState: State = {\n inputs: buildInitialInputs({ type, network, connectedAddress, prefill }),\n status: \"idle\",\n };\n\n function reducer(state: State, action: Action): State {\n switch (action.type) {\n case \"setInputs\":\n return { ...state, inputs: { ...state.inputs, ...action.payload } };\n case \"resetInputs\":\n return {\n ...state,\n inputs: buildInitialInputs({\n type,\n network,\n connectedAddress,\n prefill,\n }),\n };\n case \"setStatus\":\n return { ...state, status: action.payload };\n default:\n return state;\n }\n }\n\n const [state, dispatch] = useReducer(reducer, initialState);\n const inputs = state.inputs;\n const setInputs = (\n next: TransactionFlowInputs | Partial,\n ) => {\n dispatch({\n type: \"setInputs\",\n payload: next as Partial,\n });\n };\n\n const loading = state.status === \"executing\";\n const [refreshing, setRefreshing] = useState(false);\n const [isDialogOpen, setIsDialogOpen] = useState(false);\n const [txError, setTxError] = useState(null);\n const [lastExplorerUrl, setLastExplorerUrl] = useState(\"\");\n const previousConnectedAddressRef = useRef
(\n connectedAddress,\n );\n const maxAmountRequestIdRef = useRef(0);\n const [selectedSourceChains, setSelectedSourceChains] = useState<\n number[] | null\n >(null);\n const [selectedSourcesMaxAmount, setSelectedSourcesMaxAmount] = useState<\n string | null\n >(null);\n const [appliedSourceSelectionKey, setAppliedSourceSelectionKey] =\n useState(\"ALL\");\n const {\n steps,\n onStepsList,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n const configuredMaxAmount = useMemo(\n () => normalizeMaxAmount(maxAmount),\n [maxAmount],\n );\n\n const areInputsValid = useMemo(() => {\n const hasToken = inputs?.token !== undefined && inputs?.token !== null;\n const hasChain = inputs?.chain !== undefined && inputs?.chain !== null;\n const hasAmount = Boolean(inputs?.amount) && Number(inputs?.amount) > 0;\n const hasValidRecipient =\n Boolean(inputs?.recipient) && isAddress(inputs.recipient as string);\n return hasToken && hasChain && hasAmount && hasValidRecipient;\n }, [inputs]);\n\n const filteredBridgableBalance = useMemo(() => {\n return bridgableBalance?.find((bal) =>\n inputs?.token === \"USDM\"\n ? bal?.symbol === \"USDC\"\n : bal?.symbol === inputs?.token,\n );\n }, [bridgableBalance, inputs?.token]);\n\n const availableSources = useMemo(() => {\n // v2: chainBalances replaces breakdown\n const chainBalances = filteredBridgableBalance?.chainBalances ?? [];\n const destinationChainId = inputs?.chain;\n const nonZero = chainBalances.filter((source: ChainBalance) => {\n if (Number.parseFloat(source.balance ?? \"0\") <= 0) return false;\n if (typeof destinationChainId === \"number\") {\n return source.chain.id !== destinationChainId;\n }\n return true;\n });\n const decimals = filteredBridgableBalance?.decimals;\n if (!nexusSDK || typeof decimals !== \"number\") {\n return nonZero.sort(\n (a, b) => Number.parseFloat(b.balance) - Number.parseFloat(a.balance),\n );\n }\n return nonZero.sort((a: ChainBalance, b: ChainBalance) => {\n try {\n const aRaw = parseUnits(a.balance ?? \"0\", decimals);\n const bRaw = parseUnits(b.balance ?? \"0\", decimals);\n if (aRaw === bRaw) return 0;\n return aRaw > bRaw ? -1 : 1;\n } catch {\n return Number.parseFloat(b.balance) - Number.parseFloat(a.balance);\n }\n });\n }, [\n inputs?.chain,\n filteredBridgableBalance?.chainBalances,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n ]);\n\n const allAvailableSourceChainIds = useMemo(\n () => availableSources.map((source: ChainBalance) => source.chain.id),\n [availableSources],\n );\n\n const effectiveSelectedSourceChains = useMemo(() => {\n if (selectedSourceChains && selectedSourceChains.length > 0) {\n const availableSet = new Set(allAvailableSourceChainIds);\n const filteredSelection = selectedSourceChains.filter((id: number) =>\n availableSet.has(id),\n );\n if (filteredSelection.length > 0) {\n return filteredSelection;\n }\n }\n return allAvailableSourceChainIds;\n }, [selectedSourceChains, allAvailableSourceChainIds]);\n\n const sourceChainsForSdk =\n effectiveSelectedSourceChains.length > 0\n ? effectiveSelectedSourceChains\n : undefined;\n\n const sourceSelectionKey = useMemo(() => {\n if (allAvailableSourceChainIds.length === 0) return \"NONE\";\n if (!selectedSourceChains || selectedSourceChains.length === 0) {\n return \"ALL\";\n }\n return [...effectiveSelectedSourceChains].sort((a: number, b: number) => a - b).join(\"|\");\n }, [\n allAvailableSourceChainIds.length,\n effectiveSelectedSourceChains,\n selectedSourceChains,\n ]);\n const hasPendingSourceSelectionChanges =\n sourceSelectionKey !== appliedSourceSelectionKey;\n const intentSourceSpendAmount = intent.current?.intent?.sourcesTotal;\n\n /**\n * v2: calculateMaxForBridge is removed. Use simulateBridge to get the max amount,\n * or fall back to summing available source balances directly.\n */\n const getMaxForCurrentSelection = useCallback(async () => {\n if (!nexusSDK || !inputs?.token || !inputs?.chain) return undefined;\n\n // Sum balances from selected sources as a direct proxy for max\n const decimals = filteredBridgableBalance?.decimals;\n if (typeof decimals !== \"number\") return \"0\";\n\n const selectedSet = new Set(\n sourceChainsForSdk ?? allAvailableSourceChainIds,\n );\n const totalRaw = availableSources.reduce((sum: bigint, source: ChainBalance) => {\n if (!selectedSet.has(source.chain.id)) return sum;\n try {\n return sum + parseUnits(source.balance ?? \"0\", decimals);\n } catch {\n return sum;\n }\n }, BigInt(0));\n\n const totalReadable = formatToBigIntReadable(totalRaw, decimals);\n if (!totalReadable) return \"0\";\n\n return clampAmountToMax({\n amount: totalReadable,\n maxAmount: configuredMaxAmount,\n nexusSDK,\n token: inputs.token,\n chainId: inputs.chain,\n });\n }, [\n configuredMaxAmount,\n inputs?.chain,\n inputs?.token,\n nexusSDK,\n sourceChainsForSdk,\n allAvailableSourceChainIds,\n availableSources,\n filteredBridgableBalance?.decimals,\n ]);\n\n const toggleSourceChain = useCallback(\n (chainId: number) => {\n setSelectedSourceChains((prev) => {\n if (allAvailableSourceChainIds.length === 0) return prev;\n const current =\n prev && prev.length > 0 ? prev : allAvailableSourceChainIds;\n const next = current.includes(chainId)\n ? current.filter((id: number) => id !== chainId)\n : [...current, chainId];\n if (next.length === 0) {\n return current;\n }\n const isAllSelected =\n next.length === allAvailableSourceChainIds.length &&\n allAvailableSourceChainIds.every((id) => next.includes(id));\n return isAllSelected ? null : next;\n });\n },\n [allAvailableSourceChainIds],\n );\n\n const sourceSelection = useMemo(() => {\n const amount =\n intentSourceSpendAmount?.trim() ?? inputs?.amount?.trim() ?? \"\";\n const decimals = getCoverageDecimals({\n type,\n token: inputs?.token,\n chainId: inputs?.chain,\n fallback: filteredBridgableBalance?.decimals,\n });\n const selectedChainSet = new Set(effectiveSelectedSourceChains);\n const selectedTotalRaw =\n !nexusSDK || typeof decimals !== \"number\"\n ? BigInt(0)\n : availableSources.reduce((sum: bigint, source: ChainBalance) => {\n if (!selectedChainSet.has(source.chain.id)) return sum;\n try {\n return sum + parseUnits(source.balance ?? \"0\", decimals);\n } catch {\n return sum;\n }\n }, BigInt(0));\n const selectedTotal =\n !nexusSDK || typeof decimals !== \"number\"\n ? \"0\"\n : formatAmountForDisplay(selectedTotalRaw, decimals, nexusSDK);\n const baseSelection = {\n selectedTotal,\n requiredTotal: amount || \"0\",\n requiredSafetyTotal: amount || \"0\",\n missingToProceed: \"0\",\n missingToSafety: \"0\",\n coverageState: \"healthy\" as SourceCoverageState,\n coverageToSafetyPercent: 100,\n isBelowRequired: false,\n isBelowSafetyBuffer: false,\n };\n\n if (!nexusSDK || !inputs?.token || !inputs?.chain || !amount) {\n return baseSelection;\n }\n\n try {\n // v2: convertTokenReadableAmountToBigInt(amount, tokenSymbol, chainId)\n const requiredRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n inputs.token,\n inputs.chain,\n );\n if (requiredRaw <= BigInt(0)) {\n return baseSelection;\n }\n\n const missingToProceedRaw =\n selectedTotalRaw >= requiredRaw\n ? BigInt(0)\n : requiredRaw - selectedTotalRaw;\n const missingToSafetyRaw = missingToProceedRaw;\n\n const coverageState: SourceCoverageState =\n selectedTotalRaw < requiredRaw ? \"error\" : \"healthy\";\n\n const coverageBasisPoints =\n requiredRaw === BigInt(0)\n ? 10_000\n : selectedTotalRaw >= requiredRaw\n ? 10_000\n : Number((selectedTotalRaw * BigInt(10_000)) / requiredRaw);\n\n return {\n selectedTotal,\n requiredTotal: amount,\n requiredSafetyTotal: amount,\n missingToProceed: formatAmountForDisplay(\n missingToProceedRaw,\n decimals,\n nexusSDK,\n ),\n missingToSafety: formatAmountForDisplay(\n missingToSafetyRaw,\n decimals,\n nexusSDK,\n ),\n coverageState,\n coverageToSafetyPercent: coverageBasisPoints / 100,\n isBelowRequired: coverageState === \"error\",\n isBelowSafetyBuffer: coverageState === \"error\",\n };\n } catch {\n return baseSelection;\n }\n }, [\n type,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n inputs?.chain,\n inputs?.amount,\n inputs?.token,\n intentSourceSpendAmount,\n availableSources,\n effectiveSelectedSourceChains,\n ]);\n\n const stopwatch = useStopwatch({ intervalMs: 100 });\n const setStatus = useCallback(\n (status: TransactionStatus) =>\n dispatch({ type: \"setStatus\", payload: status }),\n [],\n );\n\n const resetInputs = useCallback(() => {\n dispatch({ type: \"resetInputs\" });\n }, []);\n\n const {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n } = useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n });\n\n usePolling(\n Boolean(intent.current) &&\n !isDialogOpen &&\n !isSourceMenuOpen &&\n !hasPendingSourceSelectionChanges,\n async () => {\n await refreshIntent();\n },\n 15000,\n );\n\n const debouncedRefreshMaxForSelection = useDebouncedCallback(\n async (requestId: number) => {\n try {\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (requestId !== maxAmountRequestIdRef.current) return;\n setSelectedSourcesMaxAmount(maxForCurrentSelection ?? \"0\");\n } catch (error) {\n if (requestId !== maxAmountRequestIdRef.current) return;\n console.error(\"Unable to calculate max for selected sources:\", error);\n setSelectedSourcesMaxAmount(\"0\");\n }\n },\n MAX_AMOUNT_DEBOUNCE_MS,\n );\n\n useEffect(() => {\n debouncedRefreshMaxForSelection.cancel();\n if (!nexusSDK || !inputs?.token || !inputs?.chain) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(null);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(\"0\");\n return;\n }\n const requestId = ++maxAmountRequestIdRef.current;\n debouncedRefreshMaxForSelection(requestId);\n }, [\n allAvailableSourceChainIds.length,\n configuredMaxAmount,\n debouncedRefreshMaxForSelection,\n inputs?.recipient,\n sourceSelectionKey,\n inputs?.chain,\n inputs?.token,\n nexusSDK,\n ]);\n\n useEffect(() => {\n if (type !== \"bridge\" || !connectedAddress) return;\n const previousConnectedAddress = previousConnectedAddressRef.current;\n if (!previousConnectedAddress) {\n previousConnectedAddressRef.current = connectedAddress;\n return;\n }\n if (connectedAddress === previousConnectedAddress) return;\n previousConnectedAddressRef.current = connectedAddress;\n if (prefill?.recipient) return;\n if (!inputs?.recipient || inputs.recipient === previousConnectedAddress) {\n dispatch({ type: \"setInputs\", payload: { recipient: connectedAddress } });\n }\n }, [type, connectedAddress, inputs?.recipient, prefill?.recipient]);\n\n useEffect(() => {\n invalidatePendingExecution();\n }, [inputs, invalidatePendingExecution]);\n\n useEffect(() => {\n setSelectedSourceChains(null);\n }, [inputs?.token]);\n\n // Safety-net: stop the stopwatch as soon as status reaches a terminal state.\n // This ensures the timer freezes even if the onEvent closure's stopwatch.stop()\n // didn't fire (e.g. stale closure reference or SDK promise resolved oddly).\n useEffect(() => {\n if (state.status === \"success\" || state.status === \"error\") {\n stopwatch.stop();\n }\n }, [state.status, stopwatch]);\n\n useEffect(() => {\n if (isDialogOpen) return;\n stopwatch.stop();\n stopwatch.reset();\n if (state.status === \"success\" || state.status === \"error\") {\n resetSteps();\n setLastExplorerUrl(\"\");\n setStatus(\"idle\");\n }\n }, [isDialogOpen, resetSteps, setStatus, state.status, stopwatch]);\n\n useEffect(() => {\n if (txError) {\n setTxError(null);\n }\n }, [inputs, txError]);\n\n\n return {\n inputs,\n setInputs,\n timer: stopwatch.seconds,\n setIsDialogOpen,\n setTxError,\n loading,\n refreshing,\n isDialogOpen,\n txError,\n handleTransaction,\n reset,\n filteredBridgableBalance,\n startTransaction,\n commitAmount,\n lastExplorerUrl,\n steps,\n status: state.status,\n availableSources,\n selectedSourceChains: effectiveSelectedSourceChains,\n toggleSourceChain,\n isSourceSelectionInsufficient: sourceSelection.isBelowRequired,\n isSourceSelectionBelowSafetyBuffer: sourceSelection.isBelowSafetyBuffer,\n isSourceSelectionReadyForAccept:\n sourceSelection.coverageState === \"healthy\",\n sourceCoverageState: sourceSelection.coverageState,\n sourceCoveragePercent: sourceSelection.coverageToSafetyPercent,\n missingToProceed: sourceSelection.missingToProceed,\n missingToSafety: sourceSelection.missingToSafety,\n selectedTotal: sourceSelection.selectedTotal,\n requiredTotal: sourceSelection.requiredTotal,\n requiredSafetyTotal: sourceSelection.requiredSafetyTotal,\n maxAvailableAmount: selectedSourcesMaxAmount ?? undefined,\n isInputsValid: areInputsValid,\n };\n}\n\n/** Helper: format a bigint rawAmount with decimals into a readable decimal string. */\nfunction formatToBigIntReadable(raw: bigint, decimals: number): string {\n if (raw <= BigInt(0)) return \"0\";\n const divisor = BigInt(10 ** decimals);\n const whole = raw / divisor;\n const fraction = raw % divisor;\n if (fraction === BigInt(0)) return whole.toString();\n const fractionStr = fraction.toString().padStart(decimals, \"0\").replace(/0+$/, \"\");\n return `${whole}.${fractionStr}`;\n}\n", "type": "registry:component", "target": "components/common/hooks/useTransactionFlow.ts" }, @@ -291,7 +291,7 @@ }, { "path": "registry/nexus-elements/common/tx/steps.ts", - "content": "import type { SwapStepType } from \"@avail-project/nexus-core\";\nimport type { GenericStep } from \"./types\";\nimport { getStepKey } from \"./types\";\n\n/**\n * Predefined expected steps for swaps to seed UI before events arrive.\n * Kept here to avoid duplication across exact-in and exact-out hooks.\n */\nexport const SWAP_EXPECTED_STEPS: SwapStepType[] = [\n { type: \"SWAP_START\", typeID: \"SWAP_START\" } as SwapStepType,\n { type: \"DETERMINING_SWAP\", typeID: \"DETERMINING_SWAP\" } as SwapStepType,\n {\n type: \"CREATE_PERMIT_FOR_SOURCE_SWAP\",\n typeID:\n \"CREATE_PERMIT_FOR_SOURCE_SWAP\" as unknown as SwapStepType[\"typeID\"],\n } as SwapStepType,\n {\n type: \"SOURCE_SWAP_BATCH_TX\",\n typeID: \"SOURCE_SWAP_BATCH_TX\",\n } as SwapStepType,\n {\n type: \"SOURCE_SWAP_HASH\",\n typeID: \"SOURCE_SWAP_HASH\" as unknown as SwapStepType[\"typeID\"],\n } as SwapStepType,\n { type: \"RFF_ID\", typeID: \"RFF_ID\" } as SwapStepType,\n {\n type: \"DESTINATION_SWAP_BATCH_TX\",\n typeID: \"DESTINATION_SWAP_BATCH_TX\",\n } as SwapStepType,\n {\n type: \"DESTINATION_SWAP_HASH\",\n typeID: \"DESTINATION_SWAP_HASH\" as unknown as SwapStepType[\"typeID\"],\n } as SwapStepType,\n { type: \"SWAP_COMPLETE\", typeID: \"SWAP_COMPLETE\" } as SwapStepType,\n];\n\nexport function seedSteps(expected: T[]): Array> {\n return expected.map((st, index) => ({\n id: index,\n completed: false,\n step: st,\n }));\n}\n\nexport function computeAllCompleted(steps: Array>): boolean {\n return steps.length > 0 && steps.every((s) => s.completed);\n}\n\n/**\n * Replace the current list of steps with a new list, preserving completion\n * for any steps that were already marked completed (matched by key).\n */\nexport function mergeStepsList(\n prev: Array>,\n list: T[]\n): Array> {\n const completedKeys = new Set();\n for (const prevStep of prev) {\n if (prevStep.completed) {\n completedKeys.add(getStepKey(prevStep.step));\n }\n }\n const next: Array> = [];\n for (let index = 0; index < list.length; index++) {\n const step = list[index];\n const key = getStepKey(step);\n next.push({\n id: index,\n completed: completedKeys.has(key),\n step,\n });\n }\n return next;\n}\n\n/**\n * Mark a step complete in-place; if the step doesn't yet exist, append it.\n */\nexport function mergeStepComplete(\n prev: Array>,\n step: T\n): Array> {\n const key = getStepKey(step);\n const updated: Array> = [];\n let found = false;\n for (const s of prev) {\n if (getStepKey(s.step) === key) {\n updated.push({ ...s, completed: true, step: { ...s.step, ...step } });\n found = true;\n } else {\n updated.push(s);\n }\n }\n if (!found) {\n updated.push({\n id: updated.length,\n completed: true,\n step,\n });\n }\n return updated;\n}\n", + "content": "// v2: SwapStepType is no longer exported from the SDK — use a local step shape\n// that matches v2 SwapPlanStep discriminator pattern\nexport type SwapStepType = {\n typeID?: string;\n type?: string;\n [key: string]: unknown;\n};\n\nimport type { GenericStep } from \"./types\";\nimport { getStepKey } from \"./types\";\n\n/**\n * Predefined expected steps for swaps to seed UI before events arrive.\n * Uses v2 stepType names that match SwapPlanProgressEvent.stepType discriminators.\n */\nexport const SWAP_EXPECTED_STEPS: SwapStepType[] = [\n { type: \"source_swap\", typeID: \"source_swap\" },\n { type: \"eoa_to_ephemeral_transfer\", typeID: \"eoa_to_ephemeral_transfer\" },\n { type: \"bridge_deposit\", typeID: \"bridge_deposit\" },\n { type: \"bridge_intent_submission\", typeID: \"bridge_intent_submission\" },\n { type: \"bridge_fill\", typeID: \"bridge_fill\" },\n { type: \"destination_swap\", typeID: \"destination_swap\" },\n];\n\nexport function seedSteps(expected: T[]): Array> {\n return expected.map((st, index) => ({\n id: index,\n completed: false,\n step: st,\n }));\n}\n\nexport function computeAllCompleted(steps: Array>): boolean {\n return steps.length > 0 && steps.every((s) => s.completed);\n}\n\n/**\n * Replace the current list of steps with a new list, preserving completion\n * for any steps that were already marked completed (matched by key).\n */\nexport function mergeStepsList(\n prev: Array>,\n list: T[]\n): Array> {\n const completedKeys = new Set();\n for (const prevStep of prev) {\n if (prevStep.completed) {\n completedKeys.add(getStepKey(prevStep.step));\n }\n }\n const next: Array> = [];\n for (let index = 0; index < list.length; index++) {\n const step = list[index];\n const key = getStepKey(step);\n next.push({\n id: index,\n completed: completedKeys.has(key),\n step,\n });\n }\n return next;\n}\n\n/**\n * Mark a step complete in-place; if the step doesn't yet exist, append it.\n */\nexport function mergeStepComplete(\n prev: Array>,\n step: T\n): Array> {\n const key = getStepKey(step);\n const updated: Array> = [];\n let found = false;\n for (const s of prev) {\n if (getStepKey(s.step) === key) {\n updated.push({ ...s, completed: true, step: { ...s.step, ...step } });\n found = true;\n } else {\n updated.push(s);\n }\n }\n if (!found) {\n updated.push({\n id: updated.length,\n completed: true,\n step,\n });\n }\n return updated;\n}\n", "type": "registry:component", "target": "components/common/tx/steps.ts" }, @@ -309,25 +309,25 @@ }, { "path": "registry/nexus-elements/common/types/transaction-flow.ts", - "content": "import {\n type NexusSDK,\n type SUPPORTED_CHAINS_IDS,\n type SUPPORTED_TOKENS,\n} from \"@avail-project/nexus-core\";\nimport { type Address } from \"viem\";\n\nexport type TransactionFlowType = \"bridge\" | \"transfer\";\n\nexport interface TransactionFlowInputs {\n chain: SUPPORTED_CHAINS_IDS;\n token: SUPPORTED_TOKENS;\n amount?: string;\n recipient?: `0x${string}`;\n}\n\nexport interface TransactionFlowPrefill {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n}\n\ntype BridgeOptions = NonNullable[1]>;\n\nexport type TransactionFlowEvent =\n NonNullable extends (event: infer E) => void\n ? E\n : never;\n\nexport type TransactionFlowOnEvent = NonNullable;\n\nexport interface TransactionFlowExecuteParams {\n token: SUPPORTED_TOKENS;\n amount: bigint;\n toChainId: SUPPORTED_CHAINS_IDS;\n recipient: `0x${string}`;\n sourceChains?: number[];\n onEvent: TransactionFlowOnEvent;\n}\n\nexport type TransactionFlowExecutor = (\n params: TransactionFlowExecuteParams,\n) => Promise<{ explorerUrl: string } | null>;\n\nexport type SourceCoverageState = \"healthy\" | \"warning\" | \"error\";\n\nexport interface SourceSelectionValidation {\n coverageState: SourceCoverageState;\n isBelowRequired: boolean;\n missingToProceed: string;\n missingToSafety: string;\n}\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport { type Address } from \"viem\";\n\ntype NexusClient = ReturnType;\n\n// v2 uses string token symbols (toTokenSymbol) with number chain IDs\nexport type TransactionFlowType = \"bridge\" | \"transfer\";\n\nexport interface TransactionFlowInputs {\n chain: number;\n token: string;\n amount?: string;\n recipient?: `0x${string}`;\n}\n\nexport interface TransactionFlowPrefill {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n}\n\n// v2 bridge onEvent uses typed discriminated union, not NEXUS_EVENTS\nexport type TransactionFlowEvent =\n | { type: \"status\"; status: string }\n | { type: \"plan_preview\"; plan: { steps: unknown[] } }\n | { type: \"plan_confirmed\"; plan: { steps: unknown[] } }\n | { type: \"plan_progress\"; stepType: string; state: string; step: unknown };\n\nexport type TransactionFlowOnEvent = (event: TransactionFlowEvent) => void;\n\nexport interface TransactionFlowExecuteParams {\n token: string;\n amount: bigint;\n toChainId: number;\n recipient: `0x${string}`;\n sources?: number[];\n onEvent: TransactionFlowOnEvent;\n}\n\nexport type TransactionFlowExecutor = (\n params: TransactionFlowExecuteParams,\n) => Promise<{ explorerUrl: string } | null>;\n\nexport type SourceCoverageState = \"healthy\" | \"warning\" | \"error\";\n\nexport interface SourceSelectionValidation {\n coverageState: SourceCoverageState;\n isBelowRequired: boolean;\n missingToProceed: string;\n missingToSafety: string;\n}\n", "type": "registry:component", "target": "components/common/types/transaction-flow.ts" }, { "path": "registry/nexus-elements/common/utils/constant.ts", - "content": "import { SUPPORTED_CHAINS } from \"@avail-project/nexus-core\";\nimport { formatUnits, parseUnits } from \"viem\";\n\nexport const SHORT_CHAIN_NAME: Record = {\n [SUPPORTED_CHAINS.ETHEREUM]: \"Ethereum\",\n [SUPPORTED_CHAINS.BASE]: \"Base\",\n [SUPPORTED_CHAINS.ARBITRUM]: \"Arbitrum\",\n [SUPPORTED_CHAINS.OPTIMISM]: \"Optimism\",\n [SUPPORTED_CHAINS.POLYGON]: \"Polygon\",\n [SUPPORTED_CHAINS.AVALANCHE]: \"Avalanche\",\n [SUPPORTED_CHAINS.SCROLL]: \"Scroll\",\n [SUPPORTED_CHAINS.MEGAETH]: \"MegaETH\",\n [SUPPORTED_CHAINS.KAIA]: \"Kaia\",\n [SUPPORTED_CHAINS.BNB]: \"BNB\",\n [SUPPORTED_CHAINS.MONAD]: \"Monad\",\n [SUPPORTED_CHAINS.HYPEREVM]: \"HyperEVM\",\n [SUPPORTED_CHAINS.CITREA]: \"Citrea\",\n // [SUPPORTED_CHAINS.TRON]: \"Tron\",\n [SUPPORTED_CHAINS.SEPOLIA]: \"Sepolia\",\n [SUPPORTED_CHAINS.BASE_SEPOLIA]: \"Base Sepolia\",\n [SUPPORTED_CHAINS.ARBITRUM_SEPOLIA]: \"Arbitrum Sepolia\",\n [SUPPORTED_CHAINS.OPTIMISM_SEPOLIA]: \"Optimism Sepolia\",\n [SUPPORTED_CHAINS.POLYGON_AMOY]: \"Polygon Amoy\",\n [SUPPORTED_CHAINS.MONAD_TESTNET]: \"Monad Testnet\",\n // [SUPPORTED_CHAINS.TRON_SHASTA]: \"Tron Shasta\",\n} as const;\n\nconst DEFAULT_SAFETY_MARGIN = 0.01; // 1%\n\n/**\n * Compute an amount string for fraction buttons (25%, 50%, 75%, 100%).\n *\n * @param balanceStr - user's balance as a human decimal string (e.g. \"12.345\") OR as base-unit integer string if `balanceIsBaseUnits` true\n * @param fraction - fraction e.g. 0.25, 0.5, 0.75, 1\n * @param decimals - token decimals (6 for USDC/USDT, 18 for ETH)\n * @param safetyMargin - 0.01 for 1% default\n * @param balanceIsBaseUnits - if true, balanceStr is already base units integer string (wei / smallest unit)\n * @returns decimal string clipped to token decimals (rounded down)\n */\nexport function computeAmountFromFraction(\n balanceStr: string,\n fraction: number,\n decimals: number,\n safetyMargin = DEFAULT_SAFETY_MARGIN,\n balanceIsBaseUnits = false,\n): string {\n if (!balanceStr) return \"0\";\n\n // parse balance into base units (BigInt)\n const balanceUnits: bigint = balanceIsBaseUnits\n ? BigInt(balanceStr)\n : parseUnits(balanceStr, decimals);\n\n if (balanceUnits === BigInt(0)) return \"0\";\n\n // Use an integer precision multiplier to avoid FP issues\n const PREC = 1_000_000; // 1e6 precision for fraction & safety margin\n const safetyMul = BigInt(Math.max(0, Math.floor((1 - safetyMargin) * PREC))); // (1 - safetyMargin) * PREC\n const fractionMul = BigInt(Math.max(0, Math.floor(fraction * PREC))); // fraction * PREC\n\n // Apply safety margin: floor(balance * (1 - safetyMargin))\n const maxAfterSafety = (balanceUnits * safetyMul) / BigInt(PREC);\n\n // Apply fraction and floor: floor(maxAfterSafety * fraction)\n let desiredUnits = (maxAfterSafety * fractionMul) / BigInt(PREC);\n\n // Extra clamp just in case\n if (desiredUnits > balanceUnits) desiredUnits = balanceUnits;\n if (desiredUnits < BigInt(0)) desiredUnits = BigInt(0);\n\n // format back to human readable decimal string with token decimals (formatUnits truncates/keeps decimals)\n // formatUnits will produce exactly decimals digits if fractional part exists; we'll strip trailing zeros.\n const raw = formatUnits(desiredUnits, decimals);\n // strip trailing zeros and possible trailing dot\n return raw\n .replace(/(\\.\\d*?[1-9])0+$/u, \"$1\")\n .replace(/\\.0+$/u, \"\")\n .replace(/^\\.$/u, \"0\");\n}\n\nexport const usdFormatter = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n});\n\nconst usdFormatterPrecise = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 3,\n maximumFractionDigits: 3,\n});\n\n/**\n * Formats USD values for UI.\n * Values between 0 and 0.001 are shown as \"< $0.001\".\n * Values between 0.001 and 0.01 are shown with 3 decimals.\n */\nexport function formatUsdForDisplay(value: number): string {\n if (!Number.isFinite(value)) return usdFormatter.format(0);\n const absValue = Math.abs(value);\n\n if (absValue === 0) return usdFormatter.format(0);\n if (absValue < 0.001) return \"< $0.001\";\n if (absValue < 0.01) {\n return usdFormatterPrecise.format(value);\n }\n\n return usdFormatter.format(value);\n}\n", + "content": "import { formatUnits, parseUnits } from \"viem\";\n\n// v2: SUPPORTED_CHAINS removed — using literal EVM chain IDs\nexport const SHORT_CHAIN_NAME: Record = {\n 1: \"Ethereum\",\n 8453: \"Base\",\n 42161: \"Arbitrum\",\n 10: \"Optimism\",\n 137: \"Polygon\",\n 43114: \"Avalanche\",\n 534352: \"Scroll\",\n 6342: \"MegaETH\",\n 8217: \"Kaia\",\n 56: \"BNB\",\n 10143: \"Monad\",\n 999: \"HyperEVM\",\n 5115: \"Citrea\",\n 11155111: \"Sepolia\",\n 84532: \"Base Sepolia\",\n 421614: \"Arbitrum Sepolia\",\n 11155420: \"Optimism Sepolia\",\n 80002: \"Polygon Amoy\",\n} as const;\n\nconst DEFAULT_SAFETY_MARGIN = 0.01; // 1%\n\n/**\n * Compute an amount string for fraction buttons (25%, 50%, 75%, 100%).\n *\n * @param balanceStr - user's balance as a human decimal string (e.g. \"12.345\") OR as base-unit integer string if `balanceIsBaseUnits` true\n * @param fraction - fraction e.g. 0.25, 0.5, 0.75, 1\n * @param decimals - token decimals (6 for USDC/USDT, 18 for ETH)\n * @param safetyMargin - 0.01 for 1% default\n * @param balanceIsBaseUnits - if true, balanceStr is already base units integer string (wei / smallest unit)\n * @returns decimal string clipped to token decimals (rounded down)\n */\nexport function computeAmountFromFraction(\n balanceStr: string,\n fraction: number,\n decimals: number,\n safetyMargin = DEFAULT_SAFETY_MARGIN,\n balanceIsBaseUnits = false,\n): string {\n if (!balanceStr) return \"0\";\n\n // parse balance into base units (BigInt)\n const balanceUnits: bigint = balanceIsBaseUnits\n ? BigInt(balanceStr)\n : parseUnits(balanceStr, decimals);\n\n if (balanceUnits === BigInt(0)) return \"0\";\n\n // Use an integer precision multiplier to avoid FP issues\n const PREC = 1_000_000; // 1e6 precision for fraction & safety margin\n const safetyMul = BigInt(Math.max(0, Math.floor((1 - safetyMargin) * PREC))); // (1 - safetyMargin) * PREC\n const fractionMul = BigInt(Math.max(0, Math.floor(fraction * PREC))); // fraction * PREC\n\n // Apply safety margin: floor(balance * (1 - safetyMargin))\n const maxAfterSafety = (balanceUnits * safetyMul) / BigInt(PREC);\n\n // Apply fraction and floor: floor(maxAfterSafety * fraction)\n let desiredUnits = (maxAfterSafety * fractionMul) / BigInt(PREC);\n\n // Extra clamp just in case\n if (desiredUnits > balanceUnits) desiredUnits = balanceUnits;\n if (desiredUnits < BigInt(0)) desiredUnits = BigInt(0);\n\n // format back to human readable decimal string with token decimals (formatUnits truncates/keeps decimals)\n // formatUnits will produce exactly decimals digits if fractional part exists; we'll strip trailing zeros.\n const raw = formatUnits(desiredUnits, decimals);\n // strip trailing zeros and possible trailing dot\n return raw\n .replace(/(\\.\\d*?[1-9])0+$/u, \"$1\")\n .replace(/\\.0+$/u, \"\")\n .replace(/^\\.$/u, \"0\");\n}\n\nexport const usdFormatter = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n});\n\nconst usdFormatterPrecise = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 3,\n maximumFractionDigits: 3,\n});\n\n/**\n * Formats USD values for UI.\n * Values between 0 and 0.001 are shown as \"< $0.001\".\n * Values between 0.001 and 0.01 are shown with 3 decimals.\n */\nexport function formatUsdForDisplay(value: number): string {\n if (!Number.isFinite(value)) return usdFormatter.format(0);\n const absValue = Math.abs(value);\n\n if (absValue === 0) return usdFormatter.format(0);\n if (absValue < 0.001) return \"< $0.001\";\n if (absValue < 0.01) {\n return usdFormatterPrecise.format(value);\n }\n\n return usdFormatter.format(value);\n}\n", "type": "registry:component", "target": "components/common/utils/constant.ts" }, { "path": "registry/nexus-elements/common/utils/token-pricing.ts", - "content": "import type { SupportedChainsAndTokensResult } from \"@avail-project/nexus-core\";\n\nconst COINBASE_SPOT_API_BASE = \"https://api.coinbase.com/v2/prices\";\nconst COINBASE_EXCHANGE_RATES_API_BASE =\n \"https://api.coinbase.com/v2/exchange-rates\";\n\nexport const DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS = 4_000;\nexport const USD_PEGGED_FALLBACK_RATE = 1;\nexport const DEFAULT_USD_PEGGED_TOKEN_SYMBOLS = [\n \"USDT\",\n \"USDC\",\n \"USDS\",\n \"DAI\",\n \"USDM\",\n \"FDUSD\",\n \"BUSD\",\n \"TUSD\",\n \"PYUSD\",\n \"GUSD\",\n \"LUSD\",\n \"USDE\",\n \"USDP\",\n] as const;\n\ntype CoinbaseSpotPriceResponse = {\n data?: {\n amount?: string | number;\n };\n};\n\ntype CoinbaseExchangeRatesResponse = {\n data?: {\n rates?: Record;\n };\n};\n\ntype SupportedTokenMetadata = {\n symbol?: string;\n equivalentCurrency?: string;\n};\n\ntype SupportedChainMetadata = {\n tokens?: SupportedTokenMetadata[];\n};\n\nexport function normalizeTokenSymbol(tokenSymbol: string): string {\n return tokenSymbol.trim().toUpperCase();\n}\n\nexport function toFinitePositiveNumber(value: unknown): number | null {\n const parsed = Number.parseFloat(String(value));\n if (!Number.isFinite(parsed) || parsed <= 0) {\n return null;\n }\n return parsed;\n}\n\nexport function getCoinbaseSymbolCandidates(tokenSymbol: string): string[] {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return [];\n\n const baseSymbol = normalized.split(/[._-]/)[0] ?? normalized;\n const wrappedBase =\n baseSymbol.startsWith(\"W\") && baseSymbol.length > 3\n ? baseSymbol.slice(1)\n : null;\n\n return Array.from(\n new Set(\n [normalized, baseSymbol, wrappedBase].filter(\n (symbol): symbol is string => Boolean(symbol),\n ),\n ),\n );\n}\n\nexport function buildUsdPeggedSymbolSet(\n supportedChains: SupportedChainsAndTokensResult | null,\n baseSymbols: Iterable = DEFAULT_USD_PEGGED_TOKEN_SYMBOLS,\n): Set {\n const symbolSet = new Set(baseSymbols);\n\n for (const chain of (supportedChains ?? []) as SupportedChainMetadata[]) {\n for (const token of chain.tokens ?? []) {\n const symbol = normalizeTokenSymbol(token.symbol ?? \"\");\n const equivalent = normalizeTokenSymbol(token.equivalentCurrency ?? \"\");\n if (!symbol) continue;\n\n if (equivalent && symbolSet.has(equivalent)) {\n symbolSet.add(symbol);\n }\n }\n }\n\n return symbolSet;\n}\n\nasync function fetchJsonWithTimeout(\n url: string,\n requestTimeoutMs: number,\n): Promise {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);\n try {\n const response = await fetch(url, {\n signal: controller.signal,\n });\n if (!response.ok) return null;\n return (await response.json()) as T;\n } catch {\n return null;\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nexport async function fetchCoinbaseUsdRate(\n tokenSymbol: string,\n requestTimeoutMs = DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS,\n): Promise {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return null;\n\n for (const candidate of getCoinbaseSymbolCandidates(normalized)) {\n const spotBody = await fetchJsonWithTimeout(\n `${COINBASE_SPOT_API_BASE}/${encodeURIComponent(candidate)}-USD/spot`,\n requestTimeoutMs,\n );\n const spotAmount = toFinitePositiveNumber(spotBody?.data?.amount);\n if (spotAmount) return spotAmount;\n\n const exchangeRatesBody =\n await fetchJsonWithTimeout(\n `${COINBASE_EXCHANGE_RATES_API_BASE}?currency=${encodeURIComponent(candidate)}`,\n requestTimeoutMs,\n );\n const exchangeRatesAmount = toFinitePositiveNumber(\n exchangeRatesBody?.data?.rates?.USD,\n );\n if (exchangeRatesAmount) return exchangeRatesAmount;\n }\n\n return null;\n}\n", + "content": "// v2: getSupportedChains() return type is inferred directly; define a structural type\ntype SupportedChainsAndTokensResult = readonly {\n tokens?: { symbol?: string; equivalentCurrency?: string }[];\n [key: string]: unknown;\n}[];\n\nconst COINBASE_SPOT_API_BASE = \"https://api.coinbase.com/v2/prices\";\nconst COINBASE_EXCHANGE_RATES_API_BASE =\n \"https://api.coinbase.com/v2/exchange-rates\";\n\nexport const DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS = 4_000;\nexport const USD_PEGGED_FALLBACK_RATE = 1;\nexport const DEFAULT_USD_PEGGED_TOKEN_SYMBOLS = [\n \"USDT\",\n \"USDC\",\n \"USDS\",\n \"DAI\",\n \"USDM\",\n \"FDUSD\",\n \"BUSD\",\n \"TUSD\",\n \"PYUSD\",\n \"GUSD\",\n \"LUSD\",\n \"USDE\",\n \"USDP\",\n] as const;\n\ntype CoinbaseSpotPriceResponse = {\n data?: {\n amount?: string | number;\n };\n};\n\ntype CoinbaseExchangeRatesResponse = {\n data?: {\n rates?: Record;\n };\n};\n\ntype SupportedTokenMetadata = {\n symbol?: string;\n equivalentCurrency?: string;\n};\n\ntype SupportedChainMetadata = {\n tokens?: SupportedTokenMetadata[];\n};\n\nexport function normalizeTokenSymbol(tokenSymbol: string): string {\n return tokenSymbol.trim().toUpperCase();\n}\n\nexport function toFinitePositiveNumber(value: unknown): number | null {\n const parsed = Number.parseFloat(String(value));\n if (!Number.isFinite(parsed) || parsed <= 0) {\n return null;\n }\n return parsed;\n}\n\nexport function getCoinbaseSymbolCandidates(tokenSymbol: string): string[] {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return [];\n\n const baseSymbol = normalized.split(/[._-]/)[0] ?? normalized;\n const wrappedBase =\n baseSymbol.startsWith(\"W\") && baseSymbol.length > 3\n ? baseSymbol.slice(1)\n : null;\n\n return Array.from(\n new Set(\n [normalized, baseSymbol, wrappedBase].filter(\n (symbol): symbol is string => Boolean(symbol),\n ),\n ),\n );\n}\n\nexport function buildUsdPeggedSymbolSet(\n supportedChains: SupportedChainsAndTokensResult | null,\n baseSymbols: Iterable = DEFAULT_USD_PEGGED_TOKEN_SYMBOLS,\n): Set {\n const symbolSet = new Set(baseSymbols);\n\n for (const chain of (supportedChains ?? []) as SupportedChainMetadata[]) {\n for (const token of chain.tokens ?? []) {\n const symbol = normalizeTokenSymbol(token.symbol ?? \"\");\n const equivalent = normalizeTokenSymbol(token.equivalentCurrency ?? \"\");\n if (!symbol) continue;\n\n if (equivalent && symbolSet.has(equivalent)) {\n symbolSet.add(symbol);\n }\n }\n }\n\n return symbolSet;\n}\n\nasync function fetchJsonWithTimeout(\n url: string,\n requestTimeoutMs: number,\n): Promise {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);\n try {\n const response = await fetch(url, {\n signal: controller.signal,\n });\n if (!response.ok) return null;\n return (await response.json()) as T;\n } catch {\n return null;\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nexport async function fetchCoinbaseUsdRate(\n tokenSymbol: string,\n requestTimeoutMs = DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS,\n): Promise {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return null;\n\n for (const candidate of getCoinbaseSymbolCandidates(normalized)) {\n const spotBody = await fetchJsonWithTimeout(\n `${COINBASE_SPOT_API_BASE}/${encodeURIComponent(candidate)}-USD/spot`,\n requestTimeoutMs,\n );\n const spotAmount = toFinitePositiveNumber(spotBody?.data?.amount);\n if (spotAmount) return spotAmount;\n\n const exchangeRatesBody =\n await fetchJsonWithTimeout(\n `${COINBASE_EXCHANGE_RATES_API_BASE}?currency=${encodeURIComponent(candidate)}`,\n requestTimeoutMs,\n );\n const exchangeRatesAmount = toFinitePositiveNumber(\n exchangeRatesBody?.data?.rates?.USD,\n );\n if (exchangeRatesAmount) return exchangeRatesAmount;\n }\n\n return null;\n}\n", "type": "registry:component", "target": "components/common/utils/token-pricing.ts" }, { "path": "registry/nexus-elements/common/utils/transaction-flow.ts", - "content": "import {\n formatUnits,\n type NexusNetwork,\n NexusSDK,\n SUPPORTED_CHAINS,\n type SUPPORTED_CHAINS_IDS,\n type SUPPORTED_TOKENS,\n} from \"@avail-project/nexus-core\";\nimport { type Address } from \"viem\";\n\nconst MAX_AMOUNT_REGEX = /^\\d*\\.?\\d+$/;\n\nexport const MAX_AMOUNT_DEBOUNCE_MS = 300;\n\nexport const normalizeMaxAmount = (\n maxAmount?: string | number,\n): string | undefined => {\n if (maxAmount === undefined || maxAmount === null) return undefined;\n const value = String(maxAmount).trim();\n if (!value || value === \".\" || !MAX_AMOUNT_REGEX.test(value)) {\n return undefined;\n }\n const parsed = Number.parseFloat(value);\n if (!Number.isFinite(parsed) || parsed <= 0) return undefined;\n return value;\n};\n\nexport const clampAmountToMax = ({\n amount,\n maxAmount,\n nexusSDK,\n token,\n chainId,\n}: {\n amount: string;\n maxAmount?: string;\n nexusSDK: NexusSDK;\n token: SUPPORTED_TOKENS;\n chainId: SUPPORTED_CHAINS_IDS;\n}): string => {\n if (!maxAmount) return amount;\n try {\n const amountRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n token,\n chainId,\n );\n const maxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxAmount,\n token,\n chainId,\n );\n return amountRaw > maxRaw ? maxAmount : amount;\n } catch {\n return amount;\n }\n};\n\nexport const formatAmountForDisplay = (\n amount: bigint,\n decimals: number | undefined,\n nexusSDK: NexusSDK,\n): string => {\n if (typeof decimals !== \"number\") return amount.toString();\n const formatted = formatUnits(amount, decimals);\n if (!formatted.includes(\".\")) return formatted;\n const [whole, fraction] = formatted.split(\".\");\n const trimmedFraction = fraction.slice(0, 6).replace(/0+$/, \"\");\n if (!trimmedFraction && whole === \"0\" && amount > BigInt(0)) {\n return \"0.000001\";\n }\n return trimmedFraction ? `${whole}.${trimmedFraction}` : whole;\n};\n\nexport const buildInitialInputs = ({\n type,\n network,\n connectedAddress,\n prefill,\n}: {\n type: \"bridge\" | \"transfer\";\n network: NexusNetwork;\n connectedAddress?: Address;\n prefill?: {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n };\n}) => {\n return {\n chain:\n (prefill?.chainId as SUPPORTED_CHAINS_IDS) ??\n (network === \"testnet\"\n ? SUPPORTED_CHAINS.SEPOLIA\n : SUPPORTED_CHAINS.ETHEREUM),\n token: (prefill?.token as SUPPORTED_TOKENS) ?? \"USDC\",\n amount: prefill?.amount ?? undefined,\n recipient:\n (prefill?.recipient as `0x${string}`) ??\n (type === \"bridge\" ? connectedAddress : undefined),\n };\n};\n\nexport const getCoverageDecimals = ({\n type,\n token,\n chainId,\n fallback,\n}: {\n type: \"bridge\" | \"transfer\";\n token?: SUPPORTED_TOKENS;\n chainId?: SUPPORTED_CHAINS_IDS;\n fallback: number | undefined;\n}) => {\n if (token === \"USDM\") return 18;\n if (\n type === \"bridge\" &&\n token === \"USDC\" &&\n chainId === SUPPORTED_CHAINS.BNB\n ) {\n return 18;\n }\n return fallback;\n};\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type { NexusNetwork } from \"@avail-project/nexus-sdk-v2\";\nimport { formatUnits, type Address } from \"viem\";\n\ntype NexusClient = ReturnType;\n\nconst MAX_AMOUNT_REGEX = /^\\d*\\.?\\d+$/;\n\n// v2 chain IDs for defaults\nconst SEPOLIA_CHAIN_ID = 11155111;\nconst ETHEREUM_CHAIN_ID = 1;\n// v2: BNB chain ID for edge-case decimal override\nconst BNB_CHAIN_ID = 56;\n\nexport const MAX_AMOUNT_DEBOUNCE_MS = 300;\n\nexport const normalizeMaxAmount = (\n maxAmount?: string | number,\n): string | undefined => {\n if (maxAmount === undefined || maxAmount === null) return undefined;\n const value = String(maxAmount).trim();\n if (!value || value === \".\" || !MAX_AMOUNT_REGEX.test(value)) {\n return undefined;\n }\n const parsed = Number.parseFloat(value);\n if (!Number.isFinite(parsed) || parsed <= 0) return undefined;\n return value;\n};\n\nexport const clampAmountToMax = ({\n amount,\n maxAmount,\n nexusSDK,\n token,\n chainId,\n}: {\n amount: string;\n maxAmount?: string;\n nexusSDK: NexusClient;\n token: string;\n chainId: number;\n}): string => {\n if (!maxAmount) return amount;\n try {\n // v2: convertTokenReadableAmountToBigInt(amount, tokenSymbol, chainId)\n const amountRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n token,\n chainId,\n );\n const maxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxAmount,\n token,\n chainId,\n );\n return amountRaw > maxRaw ? maxAmount : amount;\n } catch {\n return amount;\n }\n};\n\nexport const formatAmountForDisplay = (\n amount: bigint,\n decimals: number | undefined,\n // nexusSDK kept for API compatibility but formatUnits is now imported directly\n _nexusSDK: NexusClient,\n): string => {\n if (typeof decimals !== \"number\") return amount.toString();\n const formatted = formatUnits(amount, decimals);\n if (!formatted.includes(\".\")) return formatted;\n const [whole, fraction] = formatted.split(\".\");\n const trimmedFraction = fraction.slice(0, 6).replace(/0+$/, \"\");\n if (!trimmedFraction && whole === \"0\" && amount > BigInt(0)) {\n return \"0.000001\";\n }\n return trimmedFraction ? `${whole}.${trimmedFraction}` : whole;\n};\n\nexport const buildInitialInputs = ({\n type,\n network,\n connectedAddress,\n prefill,\n}: {\n type: \"bridge\" | \"transfer\";\n network: NexusNetwork;\n connectedAddress?: Address;\n prefill?: {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n };\n}) => {\n return {\n // v2 uses plain number chain IDs and string token symbols\n chain:\n prefill?.chainId ??\n (network === \"testnet\" ? SEPOLIA_CHAIN_ID : ETHEREUM_CHAIN_ID),\n token: prefill?.token ?? \"USDC\",\n amount: prefill?.amount ?? undefined,\n recipient:\n (prefill?.recipient as `0x${string}`) ??\n (type === \"bridge\" ? connectedAddress : undefined),\n };\n};\n\nexport const getCoverageDecimals = ({\n type,\n token,\n chainId,\n fallback,\n}: {\n type: \"bridge\" | \"transfer\";\n token?: string;\n chainId?: number;\n fallback: number | undefined;\n}) => {\n if (token === \"USDM\") return 18;\n if (type === \"bridge\" && token === \"USDC\" && chainId === BNB_CHAIN_ID) {\n return 18;\n }\n return fallback;\n};\n", "type": "registry:component", "target": "components/common/utils/transaction-flow.ts" } diff --git a/public/r/dialog.json b/public/r/dialog.json index c656b2a..aca8181 100644 --- a/public/r/dialog.json +++ b/public/r/dialog.json @@ -6,6 +6,7 @@ "description": "UI primitive: Dialog", "dependencies": [ "@radix-ui/react-dialog", + "@radix-ui/react-visually-hidden", "lucide-react" ], "registryDependencies": [ @@ -14,7 +15,7 @@ "files": [ { "path": "registry/nexus-elements/ui/dialog.tsx", - "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { XIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Dialog({\n ...props\n}: React.ComponentProps) {\n return \n}\n\nfunction DialogTrigger({\n ...props\n}: React.ComponentProps) {\n return \n}\n\nfunction DialogPortal({\n ...props\n}: React.ComponentProps) {\n return \n}\n\nfunction DialogClose({\n ...props\n}: React.ComponentProps) {\n return \n}\n\nfunction DialogOverlay({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n )\n}\n\nfunction DialogContent({\n className,\n children,\n showCloseButton = true,\n dismissible = true,\n onInteractOutside,\n onEscapeKeyDown,\n ...props\n}: React.ComponentProps & {\n showCloseButton?: boolean\n dismissible?: boolean\n}) {\n return (\n \n \n {\n if (!dismissible) {\n event.preventDefault()\n }\n onInteractOutside?.(event)\n }}\n onEscapeKeyDown={(event) => {\n if (!dismissible) {\n event.preventDefault()\n }\n onEscapeKeyDown?.(event)\n }}\n {...props}\n >\n {children}\n {showCloseButton && dismissible && (\n \n \n Close\n \n )}\n \n \n )\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n )\n}\n\nfunction DialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n )\n}\n\nfunction DialogTitle({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n )\n}\n\nfunction DialogDescription({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n )\n}\n\nexport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n DialogTrigger,\n}\n", + "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { XIcon } from \"lucide-react\";\nimport { VisuallyHidden } from \"@radix-ui/react-visually-hidden\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Dialog({\n ...props\n}: React.ComponentProps) {\n return ;\n}\n\nfunction DialogTrigger({\n ...props\n}: React.ComponentProps) {\n return ;\n}\n\nfunction DialogPortal({\n ...props\n}: React.ComponentProps) {\n return ;\n}\n\nfunction DialogClose({\n ...props\n}: React.ComponentProps) {\n return ;\n}\n\nfunction DialogOverlay({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction DialogContent({\n className,\n children,\n showCloseButton = true,\n dismissible = true,\n onInteractOutside,\n onEscapeKeyDown,\n ...props\n}: React.ComponentProps & {\n showCloseButton?: boolean;\n dismissible?: boolean;\n}) {\n return (\n \n \n {\n if (!dismissible) {\n event.preventDefault();\n }\n onInteractOutside?.(event);\n }}\n onEscapeKeyDown={(event) => {\n if (!dismissible) {\n event.preventDefault();\n }\n onEscapeKeyDown?.(event);\n }}\n {...props}\n >\n \n Deposit Dialog\n \n {children}\n {showCloseButton && dismissible && (\n \n \n Close\n \n )}\n \n \n );\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction DialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction DialogTitle({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction DialogDescription({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nexport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n DialogTrigger,\n};\n", "type": "registry:component", "target": "components/ui/dialog.tsx" } diff --git a/public/r/fast-bridge.json b/public/r/fast-bridge.json index 9f80518..90e6224 100644 --- a/public/r/fast-bridge.json +++ b/public/r/fast-bridge.json @@ -5,7 +5,7 @@ "title": "Fast Bridge", "description": "A simple component built with Nexus to enable cross chain bridging", "dependencies": [ - "@avail-project/nexus-core@1.2.0", + "@avail-project/nexus-sdk-v2", "lucide-react", "viem" ], @@ -27,61 +27,61 @@ "files": [ { "path": "registry/nexus-elements/fast-bridge/components/allowance-modal.tsx", - "content": "\"use client\";\nimport React, {\n type FC,\n memo,\n type RefObject,\n useEffect,\n useMemo,\n useState,\n} from \"react\";\nimport { Button } from \"../../ui/button\";\nimport { Input } from \"../../ui/input\";\nimport { Label } from \"../../ui/label\";\nimport {\n type AllowanceHookSource,\n CHAIN_METADATA,\n formatTokenBalance,\n type OnAllowanceHookData,\n parseUnits,\n} from \"@avail-project/nexus-core\";\nimport { useNexusError } from \"../../common\";\n\ninterface AllowanceModalProps {\n allowance: RefObject;\n callback?: () => void;\n onCloseCallback?: () => void;\n onError?: (message: string) => void;\n}\n\ntype AllowanceChoice = \"min\" | \"max\" | \"custom\";\n\ninterface AllowanceOptionProps {\n index: number;\n name: string;\n choice: AllowanceChoice;\n selectedChoice?: AllowanceChoice;\n onSelect: (index: number, choice: AllowanceChoice) => void;\n title: string;\n description?: string;\n children?: React.ReactNode;\n allowanceValue?: string;\n}\n\nconst ALLOWANCE_CHOICES: Array<{\n choice: AllowanceChoice;\n title: string;\n description: string;\n}> = [\n {\n choice: \"min\",\n title: \"Minimum\",\n description: \"Grant the lowest allowance required for this action.\",\n },\n {\n choice: \"max\",\n title: \"Maximum\",\n description: \"Approve once and skip future approvals for this token.\",\n },\n {\n choice: \"custom\",\n title: \"Custom amount\",\n description: \"Specify an allowance that fits your threshold.\",\n },\n];\n\nconst AllowanceOption: FC = ({\n index,\n name,\n choice,\n selectedChoice,\n onSelect,\n title,\n description,\n children,\n allowanceValue,\n}) => {\n const isActive = selectedChoice === choice;\n\n return (\n \n );\n};\n\nconst AllowanceModal: FC = ({\n allowance,\n callback,\n onCloseCallback,\n onError,\n}) => {\n const handleNexusError = useNexusError();\n const [selectedOption, setSelectedOption] = useState([]);\n const [customValues, setCustomValues] = useState([]);\n\n const { sources, allow, deny } = allowance.current ?? {\n sources: [],\n allow: () => {},\n deny: () => {},\n };\n\n const defaultChoices = useMemo(\n () => Array.from({ length: sources.length }, () => \"min\"),\n [sources.length],\n );\n\n const isCustomValueValid = (\n value: string,\n minimumRaw: bigint,\n decimals: number,\n ): boolean => {\n if (!value || value.trim() === \"\") return false;\n try {\n const parsedValue = parseUnits(value, decimals);\n if (parsedValue === undefined) return false;\n return parsedValue >= minimumRaw;\n } catch {\n return false;\n }\n };\n\n const hasValidationErrors = useMemo(() => {\n return sources.some((source, index) => {\n if (selectedOption[index] !== \"custom\") return false;\n const value = customValues[index];\n if (!value || value.trim() === \"\") return false;\n return !isCustomValueValid(\n value,\n source.allowance.minimumRaw,\n source.token.decimals,\n );\n });\n }, [sources, selectedOption, customValues]);\n\n const onClose = () => {\n deny();\n allowance.current = null;\n onCloseCallback?.();\n };\n\n const onApprove = () => {\n const processed = sources.map((_, i) => {\n const opt = selectedOption[i];\n if (opt === \"min\" || opt === \"max\") return opt;\n const rawValue = customValues[i]?.trim();\n if (!rawValue) return \"min\";\n const parsed = Number(rawValue);\n if (!Number.isFinite(parsed) || parsed < 0) return \"min\";\n return rawValue;\n });\n try {\n allow(processed);\n allowance.current = null;\n callback?.();\n } catch (error) {\n const { message } = handleNexusError(error);\n console.error(\"AllowanceModal onApprove error\", error);\n allowance.current = null;\n onError?.(message);\n onCloseCallback?.();\n }\n };\n\n const handleChoiceChange = (index: number, value: AllowanceChoice) => {\n setSelectedOption((prev) => {\n const next = [...(prev.length ? prev : defaultChoices)];\n next[index] = value;\n return next;\n });\n };\n\n const formatAmount = (value: string | bigint, source: AllowanceHookSource) =>\n formatTokenBalance(value, {\n symbol: source.token.symbol,\n decimals: source.token.decimals,\n }) ?? \"—\";\n\n useEffect(() => {\n setSelectedOption(defaultChoices);\n }, [defaultChoices]);\n\n useEffect(() => {\n setCustomValues(Array.from({ length: sources.length }, () => \"\"));\n }, [sources.length]);\n\n return (\n <>\n
\n

\n Set Token Allowances\n

\n

\n Review every required token and choose the minimum, an unlimited max,\n or define a custom amount before approving.\n

\n
\n\n
\n {sources?.map((source: AllowanceHookSource, index: number) => (\n \n
\n
\n
\n \n
\n
\n

\n {source.token.symbol}\n

\n

\n {source.chain.name}\n

\n
\n
\n\n
\n

\n Current allowance\n

\n

\n {formatAmount(source.allowance.currentRaw, source)}\n

\n
\n
\n\n
\n {ALLOWANCE_CHOICES.map((choice) => {\n if (choice.choice === \"custom\") {\n const customValue = customValues[index] ?? \"\";\n const isCustomSelected = selectedOption[index] === \"custom\";\n const showError =\n isCustomSelected &&\n customValue.trim() !== \"\" &&\n !isCustomValueValid(\n customValue,\n source.allowance.minimumRaw,\n source.token.decimals,\n );\n return (\n \n
\n {\n const next = [...customValues];\n next[index] = e.target.value;\n setCustomValues(next);\n }}\n maxLength={source.token.decimals}\n className={`h-9 w-40 rounded-lg border bg-background/80 text-sm disabled:opacity-60 ${\n showError ? \"border-destructive\" : \"\"\n }`}\n disabled={!isCustomSelected}\n />\n {showError && (\n

\n Min: {source.allowance.minimum}\n

\n )}\n
\n \n );\n }\n return (\n \n );\n })}\n
\n
\n ))}\n
\n\n
\n \n \n Approve Selected\n \n
\n \n );\n};\n\nAllowanceModal.displayName = \"AllowanceModal\";\n\nexport default memo(AllowanceModal);\n", + "content": "\"use client\";\nimport React, {\n type FC,\n memo,\n type RefObject,\n useEffect,\n useMemo,\n useState,\n} from \"react\";\nimport { Button } from \"../../ui/button\";\nimport { Input } from \"../../ui/input\";\nimport { Label } from \"../../ui/label\";\nimport {\n type AllowanceHookSource,\n type OnAllowanceHookData,\n} from \"@avail-project/nexus-sdk-v2\";\nimport { parseUnits } from \"viem\";\nimport { useNexusError } from \"../../common\";\nimport { formatTokenBalance } from \"@avail-project/nexus-sdk-v2/utils\";\n\ninterface AllowanceModalProps {\n allowance: RefObject;\n callback?: () => void;\n onCloseCallback?: () => void;\n onError?: (message: string) => void;\n}\n\ntype AllowanceChoice = \"min\" | \"max\" | \"custom\";\n\ninterface AllowanceOptionProps {\n index: number;\n name: string;\n choice: AllowanceChoice;\n selectedChoice?: AllowanceChoice;\n onSelect: (index: number, choice: AllowanceChoice) => void;\n title: string;\n description?: string;\n children?: React.ReactNode;\n allowanceValue?: string;\n}\n\nconst ALLOWANCE_CHOICES: Array<{\n choice: AllowanceChoice;\n title: string;\n description: string;\n}> = [\n {\n choice: \"min\",\n title: \"Minimum\",\n description: \"Grant the lowest allowance required for this action.\",\n },\n {\n choice: \"max\",\n title: \"Maximum\",\n description: \"Approve once and skip future approvals for this token.\",\n },\n {\n choice: \"custom\",\n title: \"Custom amount\",\n description: \"Specify an allowance that fits your threshold.\",\n },\n];\n\nconst AllowanceOption: FC = ({\n index,\n name,\n choice,\n selectedChoice,\n onSelect,\n title,\n description,\n children,\n allowanceValue,\n}) => {\n const isActive = selectedChoice === choice;\n\n return (\n \n );\n};\n\nconst AllowanceModal: FC = ({\n allowance,\n callback,\n onCloseCallback,\n onError,\n}) => {\n const handleNexusError = useNexusError();\n const [selectedOption, setSelectedOption] = useState([]);\n const [customValues, setCustomValues] = useState([]);\n\n const { sources, allow, deny } = allowance.current ?? {\n sources: [],\n allow: () => {},\n deny: () => {},\n };\n\n const defaultChoices = useMemo(\n () => Array.from({ length: sources.length }, () => \"min\"),\n [sources.length],\n );\n\n const isCustomValueValid = (\n value: string,\n minimumRaw: bigint,\n decimals: number,\n ): boolean => {\n if (!value || value.trim() === \"\") return false;\n try {\n const parsedValue = parseUnits(value, decimals);\n if (parsedValue === undefined) return false;\n return parsedValue >= minimumRaw;\n } catch {\n return false;\n }\n };\n\n const hasValidationErrors = useMemo(() => {\n return sources.some((source, index) => {\n if (selectedOption[index] !== \"custom\") return false;\n const value = customValues[index];\n if (!value || value.trim() === \"\") return false;\n return !isCustomValueValid(\n value,\n source.allowance.minimumRaw,\n source.token.decimals,\n );\n });\n }, [sources, selectedOption, customValues]);\n\n const onClose = () => {\n deny();\n allowance.current = null;\n onCloseCallback?.();\n };\n\n const onApprove = () => {\n const processed = sources.map((_, i) => {\n const opt = selectedOption[i];\n if (opt === \"min\" || opt === \"max\") return opt;\n const rawValue = customValues[i]?.trim();\n if (!rawValue) return \"min\";\n const parsed = Number(rawValue);\n if (!Number.isFinite(parsed) || parsed < 0) return \"min\";\n return rawValue;\n });\n try {\n allow(processed);\n allowance.current = null;\n callback?.();\n } catch (error) {\n const { message } = handleNexusError(error);\n console.error(\"AllowanceModal onApprove error\", error);\n allowance.current = null;\n onError?.(message);\n onCloseCallback?.();\n }\n };\n\n const handleChoiceChange = (index: number, value: AllowanceChoice) => {\n setSelectedOption((prev) => {\n const next = [...(prev.length ? prev : defaultChoices)];\n next[index] = value;\n return next;\n });\n };\n\n const formatAmount = (value: string | bigint, source: AllowanceHookSource) =>\n formatTokenBalance(value, {\n symbol: source.token.symbol,\n decimals: source.token.decimals,\n }) ?? \"—\";\n\n useEffect(() => {\n setSelectedOption(defaultChoices);\n }, [defaultChoices]);\n\n useEffect(() => {\n setCustomValues(Array.from({ length: sources.length }, () => \"\"));\n }, [sources.length]);\n\n return (\n <>\n
\n

\n Set Token Allowances\n

\n

\n Review every required token and choose the minimum, an unlimited max,\n or define a custom amount before approving.\n

\n
\n\n
\n {sources?.map((source: AllowanceHookSource, index: number) => (\n \n
\n
\n
\n \n
\n
\n

\n {source.token.symbol}\n

\n

\n {source.chain.name}\n

\n
\n
\n\n
\n

\n Current allowance\n

\n

\n {formatAmount(source.allowance.currentRaw, source)}\n

\n
\n
\n\n
\n {ALLOWANCE_CHOICES.map((choice) => {\n if (choice.choice === \"custom\") {\n const customValue = customValues[index] ?? \"\";\n const isCustomSelected = selectedOption[index] === \"custom\";\n const showError =\n isCustomSelected &&\n customValue.trim() !== \"\" &&\n !isCustomValueValid(\n customValue,\n source.allowance.minimumRaw,\n source.token.decimals,\n );\n return (\n \n
\n {\n const next = [...customValues];\n next[index] = e.target.value;\n setCustomValues(next);\n }}\n maxLength={source.token.decimals}\n className={`h-9 w-40 rounded-lg border bg-background/80 text-sm disabled:opacity-60 ${\n showError ? \"border-destructive\" : \"\"\n }`}\n disabled={!isCustomSelected}\n />\n {showError && (\n

\n Min: {source.allowance.minimum}\n

\n )}\n
\n \n );\n }\n return (\n \n );\n })}\n
\n
\n ))}\n
\n\n
\n \n \n Approve Selected\n \n
\n \n );\n};\n\nAllowanceModal.displayName = \"AllowanceModal\";\n\nexport default memo(AllowanceModal);\n", "type": "registry:component", "target": "components/fast-bridge/components/allowance-modal.tsx" }, { "path": "registry/nexus-elements/fast-bridge/components/amount-input.tsx", - "content": "import { type FC, Fragment, useEffect, useMemo, useRef } from \"react\";\nimport { Input } from \"../../ui/input\";\nimport { Button } from \"../../ui/button\";\nimport { formatTokenBalance, type UserAsset } from \"@avail-project/nexus-core\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { type FastBridgeState } from \"../hooks/useBridge\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"../../ui/accordion\";\nimport { SHORT_CHAIN_NAME } from \"../../common\";\nimport {\n clampAmountToMax,\n normalizeMaxAmount,\n} from \"../../common/utils/transaction-flow\";\nimport { LoaderCircle } from \"lucide-react\";\n\ninterface AmountInputProps {\n amount?: string;\n onChange: (value: string) => void;\n bridgableBalance?: UserAsset;\n onCommit?: (value: string) => void;\n disabled?: boolean;\n inputs: FastBridgeState;\n maxAmount?: string | number;\n maxAvailableAmount?: string;\n}\n\nconst AmountInput: FC = ({\n amount,\n onChange,\n bridgableBalance,\n onCommit,\n disabled,\n inputs,\n maxAmount,\n maxAvailableAmount,\n}) => {\n const { nexusSDK, loading } = useNexus();\n const commitTimerRef = useRef(null);\n const normalizedMaxAmount = useMemo(\n () => normalizeMaxAmount(maxAmount),\n [maxAmount],\n );\n\n const applyMaxCap = (value: string) => {\n if (!nexusSDK || !inputs?.token || !inputs?.chain) {\n return value;\n }\n return clampAmountToMax({\n amount: value,\n maxAmount: normalizedMaxAmount,\n nexusSDK,\n token: inputs.token,\n chainId: inputs.chain,\n });\n };\n\n const scheduleCommit = (val: string) => {\n if (!onCommit || disabled) return;\n if (commitTimerRef.current) clearTimeout(commitTimerRef.current);\n commitTimerRef.current = setTimeout(() => {\n onCommit(val);\n }, 800);\n };\n\n const onMaxClick = () => {\n if (!maxAvailableAmount) return;\n const capped = applyMaxCap(maxAvailableAmount);\n onChange(capped);\n onCommit?.(capped);\n };\n\n useEffect(() => {\n return () => {\n if (commitTimerRef.current) {\n clearTimeout(commitTimerRef.current);\n commitTimerRef.current = null;\n }\n };\n }, []);\n\n return (\n
\n
\n {\n let next = e.target.value.replaceAll(/[^0-9.]/g, \"\");\n const parts = next.split(\".\");\n if (parts.length > 2)\n next = parts[0] + \".\" + parts.slice(1).join(\"\");\n if (next === \".\") next = \"0.\";\n onChange(next);\n scheduleCommit(next);\n }}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n if (commitTimerRef.current) {\n clearTimeout(commitTimerRef.current);\n commitTimerRef.current = null;\n }\n onCommit?.(amount ?? \"\");\n }\n }}\n className=\"w-full border-none bg-transparent rounded-r-none focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none py-0 px-3 h-12!\"\n aria-invalid={Boolean(amount) && Number.isNaN(Number(amount))}\n disabled={disabled || loading}\n />\n
\n {bridgableBalance && (\n

\n {formatTokenBalance(bridgableBalance?.balance, {\n symbol: bridgableBalance?.symbol,\n decimals: bridgableBalance?.decimals,\n })}\n

\n )}\n {loading && !bridgableBalance && (\n \n )}\n \n MAX\n \n
\n
\n \n \n \n View Balance Breakdown\n \n \n
\n {bridgableBalance?.breakdown.map((chain) => {\n if (Number.parseFloat(chain.balance) === 0) return null;\n if (inputs?.chain === chain.chain.id) return null;\n return (\n \n
\n
\n
\n \n
\n \n {SHORT_CHAIN_NAME[chain.chain.id]}\n \n
\n

\n {formatTokenBalance(chain.balance, {\n symbol: chain.symbol,\n decimals: chain.decimals,\n })}\n

\n
\n
\n );\n })}\n
\n
\n
\n
\n
\n );\n};\n\nexport default AmountInput;\n", + "content": "import { type FC, Fragment, useEffect, useMemo, useRef } from \"react\";\nimport { Input } from \"../../ui/input\";\nimport { Button } from \"../../ui/button\";\nimport { type TokenBalance as UserAsset } from \"@avail-project/nexus-sdk-v2\";\nimport { formatTokenBalance } from \"@avail-project/nexus-sdk-v2/utils\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { type FastBridgeState } from \"../hooks/useBridge\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"../../ui/accordion\";\nimport { SHORT_CHAIN_NAME } from \"../../common\";\nimport {\n clampAmountToMax,\n normalizeMaxAmount,\n} from \"../../common/utils/transaction-flow\";\nimport { LoaderCircle } from \"lucide-react\";\n\ninterface AmountInputProps {\n amount?: string;\n onChange: (value: string) => void;\n bridgableBalance?: UserAsset;\n onCommit?: (value: string) => void;\n disabled?: boolean;\n inputs: FastBridgeState;\n maxAmount?: string | number;\n maxAvailableAmount?: string;\n}\n\nconst AmountInput: FC = ({\n amount,\n onChange,\n bridgableBalance,\n onCommit,\n disabled,\n inputs,\n maxAmount,\n maxAvailableAmount,\n}) => {\n const { nexusSDK, loading } = useNexus();\n const commitTimerRef = useRef(null);\n const normalizedMaxAmount = useMemo(\n () => normalizeMaxAmount(maxAmount),\n [maxAmount],\n );\n\n const applyMaxCap = (value: string) => {\n if (!nexusSDK || !inputs?.token || !inputs?.chain) {\n return value;\n }\n return clampAmountToMax({\n amount: value,\n maxAmount: normalizedMaxAmount,\n nexusSDK,\n token: inputs.token,\n chainId: inputs.chain,\n });\n };\n\n const scheduleCommit = (val: string) => {\n if (!onCommit || disabled) return;\n if (commitTimerRef.current) clearTimeout(commitTimerRef.current);\n commitTimerRef.current = setTimeout(() => {\n onCommit(val);\n }, 800);\n };\n\n const onMaxClick = () => {\n if (!maxAvailableAmount) return;\n const capped = applyMaxCap(maxAvailableAmount);\n onChange(capped);\n onCommit?.(capped);\n };\n\n useEffect(() => {\n return () => {\n if (commitTimerRef.current) {\n clearTimeout(commitTimerRef.current);\n commitTimerRef.current = null;\n }\n };\n }, []);\n\n return (\n
\n
\n {\n let next = e.target.value.replaceAll(/[^0-9.]/g, \"\");\n const parts = next.split(\".\");\n if (parts.length > 2)\n next = parts[0] + \".\" + parts.slice(1).join(\"\");\n if (next === \".\") next = \"0.\";\n onChange(next);\n scheduleCommit(next);\n }}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n if (commitTimerRef.current) {\n clearTimeout(commitTimerRef.current);\n commitTimerRef.current = null;\n }\n onCommit?.(amount ?? \"\");\n }\n }}\n className=\"w-full border-none bg-transparent rounded-r-none focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none py-0 px-3 h-12!\"\n aria-invalid={Boolean(amount) && Number.isNaN(Number(amount))}\n disabled={disabled || loading}\n />\n
\n {bridgableBalance && (\n

\n {formatTokenBalance(bridgableBalance?.balance, {\n symbol: bridgableBalance?.symbol,\n decimals: bridgableBalance?.decimals,\n })}\n

\n )}\n {loading && !bridgableBalance && (\n \n )}\n \n MAX\n \n
\n
\n \n \n \n View Balance Breakdown\n \n \n
\n {bridgableBalance?.chainBalances?.map((chain: any) => {\n if (Number.parseFloat(chain.balance) === 0) return null;\n if (inputs?.chain === chain.chain.id) return null;\n return (\n \n
\n
\n
\n \n
\n \n {SHORT_CHAIN_NAME[chain.chain.id]}\n \n
\n

\n {formatTokenBalance(chain.balance, {\n symbol: chain.symbol,\n decimals: chain.decimals,\n })}\n

\n
\n
\n );\n })}\n
\n
\n
\n
\n
\n );\n};\n\nexport default AmountInput;\n", "type": "registry:component", "target": "components/fast-bridge/components/amount-input.tsx" }, { "path": "registry/nexus-elements/fast-bridge/components/chain-select.tsx", - "content": "import { type FC, useMemo } from \"react\";\nimport { Label } from \"../../ui/label\";\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../../ui/select\";\nimport { type SUPPORTED_CHAINS_IDS } from \"@avail-project/nexus-core\";\nimport { cn } from \"@/lib/utils\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\n\ninterface ChainSelectProps {\n selectedChain: number;\n disabled?: boolean;\n hidden?: boolean;\n className?: string;\n label?: string;\n handleSelect: (chainId: SUPPORTED_CHAINS_IDS) => void;\n}\n\nconst ChainSelect: FC = ({\n selectedChain,\n disabled,\n hidden = false,\n className,\n label,\n handleSelect,\n}) => {\n const { supportedChainsAndTokens } = useNexus();\n\n const selectedChainData = useMemo(() => {\n if (!supportedChainsAndTokens) return null;\n return supportedChainsAndTokens.find((c) => c.id === selectedChain);\n }, [selectedChain, supportedChainsAndTokens]);\n\n if (hidden) return null;\n return (\n {\n if (!disabled) {\n handleSelect(Number.parseInt(value) as SUPPORTED_CHAINS_IDS);\n }\n }}\n >\n
\n {label && }\n \n \n {selectedChainData && (\n \n \n

\n {selectedChainData?.name}\n

\n
\n )}\n \n \n
\n\n \n \n {supportedChainsAndTokens?.map((chain) => {\n return (\n \n
\n \n

{chain.name}

\n
\n
\n );\n })}\n
\n
\n \n );\n};\n\nexport default ChainSelect;\n", + "content": "import { type FC, useMemo } from \"react\";\nimport { Label } from \"../../ui/label\";\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../../ui/select\";\n// v2: SUPPORTED_CHAINS_IDS removed — use plain number\nimport { cn } from \"@/lib/utils\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\n\ninterface ChainSelectProps {\n selectedChain: number;\n disabled?: boolean;\n hidden?: boolean;\n className?: string;\n label?: string;\n handleSelect: (chainId: number) => void;\n}\n\nconst ChainSelect: FC = ({\n selectedChain,\n disabled,\n hidden = false,\n className,\n label,\n handleSelect,\n}) => {\n const { supportedChainsAndTokens } = useNexus();\n\n const selectedChainData = useMemo(() => {\n if (!supportedChainsAndTokens) return null;\n return supportedChainsAndTokens.find((c) => c.id === selectedChain);\n }, [selectedChain, supportedChainsAndTokens]);\n\n if (hidden) return null;\n return (\n {\n if (!disabled) {\n handleSelect(Number.parseInt(value));\n }\n }}\n >\n
\n {label && }\n \n \n {selectedChainData && (\n \n \n

\n {selectedChainData?.name}\n

\n
\n )}\n \n \n
\n\n \n \n {supportedChainsAndTokens?.map((chain) => {\n return (\n \n
\n \n

{chain.name}

\n
\n
\n );\n })}\n
\n
\n \n );\n};\n\nexport default ChainSelect;\n", "type": "registry:component", "target": "components/fast-bridge/components/chain-select.tsx" }, { "path": "registry/nexus-elements/fast-bridge/components/fee-breakdown.tsx", - "content": "import { type FC } from \"react\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"../../ui/accordion\";\nimport {\n formatTokenBalance,\n SUPPORTED_TOKENS,\n type ReadableIntent,\n} from \"@avail-project/nexus-core\";\nimport { Skeleton } from \"../../ui/skeleton\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../../ui/tooltip\";\nimport { MessageCircleQuestion } from \"lucide-react\";\n\ninterface FeeBreakdownProps {\n intent: ReadableIntent;\n tokenSymbol: SUPPORTED_TOKENS;\n isLoading?: boolean;\n}\n\nconst FeeBreakdown: FC = ({\n intent,\n tokenSymbol,\n isLoading = false,\n}) => {\n const { nexusSDK } = useNexus();\n\n const feeRows = [\n {\n key: \"caGas\",\n label: \"Fast Bridge Gas Fees\",\n value: intent?.fees?.caGas,\n description:\n \"The gas fee required for executing the fast bridge transaction on the destination chain.\",\n },\n {\n key: \"gasSupplied\",\n label: \"Gas Supplied\",\n value: intent?.fees?.gasSupplied,\n description:\n \"The amount of gas tokens supplied to cover transaction costs on the destination chain.\",\n },\n {\n key: \"solver\",\n label: \"Solver Fees\",\n value: intent?.fees?.solver,\n description:\n \"Fees paid to the solver that executes the bridge transaction and ensures fast completion.\",\n },\n {\n key: \"protocol\",\n label: \"Protocol Fees\",\n value: intent?.fees?.protocol,\n description:\n \"Fees collected by the protocol for maintaining and operating the bridge infrastructure.\",\n },\n ];\n\n return (\n \n \n
\n

Total fees

\n\n
\n {isLoading ? (\n \n ) : (\n

\n {formatTokenBalance(intent.fees?.total, {\n symbol: tokenSymbol,\n decimals: intent?.token?.decimals,\n })}\n

\n )}\n \n

View Breakup

\n \n
\n
\n \n
\n {feeRows.map(({ key, label, value, description }) => {\n if (Number.parseFloat(value ?? \"0\") <= 0) return null;\n return (\n \n
\n
\n

{label}

\n \n \n \n
\n {isLoading ? (\n \n ) : (\n

\n {formatTokenBalance(value, {\n symbol: tokenSymbol,\n decimals: intent?.token?.decimals,\n })}\n

\n )}\n
\n \n

{description}

\n
\n
\n );\n })}\n
\n
\n
\n
\n );\n};\n\nexport default FeeBreakdown;\n", + "content": "import { type FC } from \"react\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"../../ui/accordion\";\nimport { type BridgeIntent } from \"@avail-project/nexus-sdk-v2\";\nimport { formatTokenBalance } from \"@avail-project/nexus-sdk-v2/utils\";\nimport { Skeleton } from \"../../ui/skeleton\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../../ui/tooltip\";\nimport { MessageCircleQuestion } from \"lucide-react\";\n\ninterface FeeBreakdownProps {\n intent: BridgeIntent;\n tokenSymbol?: string; // v2: was SUPPORTED_TOKENS\n isLoading?: boolean;\n}\n\nconst FeeBreakdown: FC = ({\n intent,\n tokenSymbol,\n isLoading = false,\n}) => {\n const { nexusSDK } = useNexus();\n\n const feeRows = [\n {\n key: \"caGas\",\n label: \"Fast Bridge Gas Fees\",\n value: intent?.fees?.caGas,\n description:\n \"The gas fee required for executing the fast bridge transaction on the destination chain.\",\n },\n {\n key: \"gasSupplied\",\n label: \"Gas Supplied\",\n value: intent?.fees?.caGas,\n description:\n \"The amount of gas tokens supplied to cover transaction costs on the destination chain.\",\n },\n {\n key: \"solver\",\n label: \"Solver Fees\",\n value: intent?.fees?.solver,\n description:\n \"Fees paid to the solver that executes the bridge transaction and ensures fast completion.\",\n },\n {\n key: \"protocol\",\n label: \"Protocol Fees\",\n value: intent?.fees?.protocol,\n description:\n \"Fees collected by the protocol for maintaining and operating the bridge infrastructure.\",\n },\n ];\n\n return (\n \n \n
\n

Total fees

\n\n
\n {isLoading ? (\n \n ) : (\n

\n {formatTokenBalance(intent.fees?.total, {\n symbol: tokenSymbol,\n decimals: intent?.availableSources?.[0]?.token?.decimals,\n })}\n

\n )}\n \n

View Breakup

\n \n
\n
\n \n
\n {feeRows.map(({ key, label, value, description }) => {\n if (Number.parseFloat(value ?? \"0\") <= 0) return null;\n return (\n \n
\n
\n

{label}

\n \n \n \n
\n {isLoading ? (\n \n ) : (\n

\n {formatTokenBalance(value, {\n symbol: tokenSymbol,\n decimals: intent?.availableSources?.[0]?.token?.decimals,\n })}\n

\n )}\n
\n \n

{description}

\n
\n
\n );\n })}\n
\n
\n
\n
\n );\n};\n\nexport default FeeBreakdown;\n", "type": "registry:component", "target": "components/fast-bridge/components/fee-breakdown.tsx" }, { "path": "registry/nexus-elements/fast-bridge/components/recipient-address.tsx", - "content": "\"use client\";\nimport { type FC, useState } from \"react\";\nimport { Input } from \"../../ui/input\";\nimport { Check, Edit } from \"lucide-react\";\nimport { Button } from \"../../ui/button\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { type Address } from \"viem\";\nimport { truncateAddress } from \"@avail-project/nexus-core\";\n\ninterface RecipientAddressProps {\n address?: Address;\n onChange: (address: string) => void;\n disabled?: boolean;\n}\n\nconst RecipientAddress: FC = ({\n address,\n onChange,\n disabled,\n}) => {\n const { nexusSDK } = useNexus();\n const [isEditing, setIsEditing] = useState(false);\n return (\n
\n {isEditing ? (\n
\n onChange(e.target.value)}\n className=\"w-full\"\n />\n {\n setIsEditing(false);\n }}\n >\n \n \n
\n ) : (\n
\n

Recipient Address

\n
\n {address && (\n

\n {truncateAddress(address, 6, 6)}\n

\n )}\n\n {\n setIsEditing(true);\n }}\n className=\"px-0 size-5\"\n disabled={disabled}\n >\n \n \n
\n
\n )}\n
\n );\n};\n\nexport default RecipientAddress;\n", + "content": "\"use client\";\nimport { type FC, useState } from \"react\";\nimport { Input } from \"../../ui/input\";\nimport { Check, Edit } from \"lucide-react\";\nimport { Button } from \"../../ui/button\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { type Address } from \"viem\";\nimport { truncateAddress } from \"@avail-project/nexus-sdk-v2/utils\";\n\ninterface RecipientAddressProps {\n address?: Address;\n onChange: (address: string) => void;\n disabled?: boolean;\n}\n\nconst RecipientAddress: FC = ({\n address,\n onChange,\n disabled,\n}) => {\n const { nexusSDK } = useNexus();\n const [isEditing, setIsEditing] = useState(false);\n return (\n
\n {isEditing ? (\n
\n onChange(e.target.value)}\n className=\"w-full\"\n />\n {\n setIsEditing(false);\n }}\n >\n \n \n
\n ) : (\n
\n

Recipient Address

\n
\n {address && (\n

\n {truncateAddress(address, 6, 6)}\n

\n )}\n\n {\n setIsEditing(true);\n }}\n className=\"px-0 size-5\"\n disabled={disabled}\n >\n \n \n
\n
\n )}\n
\n );\n};\n\nexport default RecipientAddress;\n", "type": "registry:component", "target": "components/fast-bridge/components/recipient-address.tsx" }, { "path": "registry/nexus-elements/fast-bridge/components/source-breakdown.tsx", - "content": "import {\n formatTokenBalance,\n type ReadableIntent,\n type SUPPORTED_TOKENS,\n type UserAsset,\n} from \"@avail-project/nexus-core\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"../../ui/accordion\";\nimport { Skeleton } from \"../../ui/skeleton\";\nimport { Checkbox } from \"../../ui/checkbox\";\nimport { cn } from \"@/lib/utils\";\n\ntype SourceCoverageState = \"healthy\" | \"warning\" | \"error\";\n\ninterface SourceBreakdownProps {\n intent?: ReadableIntent;\n tokenSymbol: SUPPORTED_TOKENS;\n isLoading?: boolean;\n availableSources: UserAsset[\"breakdown\"];\n selectedSourceChains: number[];\n onToggleSourceChain: (chainId: number) => void;\n onSourceMenuOpenChange?: (open: boolean) => void;\n isSourceSelectionInsufficient?: boolean;\n sourceCoverageState?: SourceCoverageState;\n sourceCoveragePercent?: number;\n missingToProceed?: string;\n missingToSafety?: string;\n selectedTotal?: string;\n requiredTotal?: string;\n requiredSafetyTotal?: string;\n}\n\nconst SourceBreakdown = ({\n intent,\n tokenSymbol,\n isLoading = false,\n availableSources,\n selectedSourceChains,\n onToggleSourceChain,\n onSourceMenuOpenChange,\n isSourceSelectionInsufficient = false,\n sourceCoverageState = \"healthy\",\n sourceCoveragePercent = 100,\n missingToProceed,\n selectedTotal,\n requiredTotal,\n requiredSafetyTotal,\n}: SourceBreakdownProps) => {\n const displayTokenSymbol = availableSources[0]?.symbol ?? tokenSymbol;\n const normalizedCoverage = Math.max(0, Math.min(100, sourceCoveragePercent));\n const progressRadius = 16;\n const progressCircumference = 2 * Math.PI * progressRadius;\n const progressOffset =\n progressCircumference - (normalizedCoverage / 100) * progressCircumference;\n const showCoverageFeedback = Boolean(\n selectedTotal && requiredTotal && requiredSafetyTotal,\n );\n const shouldShowProceedMessage =\n sourceCoverageState === \"error\" &&\n Number.parseFloat(missingToProceed ?? \"0\") > 0;\n\n const coverageToneClass =\n sourceCoverageState === \"error\"\n ? \"text-rose-500\"\n : sourceCoverageState === \"warning\"\n ? \"text-amber-500\"\n : \"text-emerald-500\";\n\n const coverageSurfaceClass =\n sourceCoverageState === \"error\"\n ? \" text-rose-950 dark:text-rose-200\"\n : sourceCoverageState === \"warning\"\n ? \" text-amber-950 dark:text-amber-200\"\n : \" text-emerald-950 dark:text-emerald-200\";\n const selectedSourceSet = new Set(selectedSourceChains);\n const bulkActionLabel =\n selectedSourceChains.length > 1 ? \"Deselect all\" : \"Select all\";\n const isBulkActionDisabled = availableSources.length <= 1;\n const handleBulkSourceAction = () => {\n if (isBulkActionDisabled) return;\n\n if (bulkActionLabel === \"Select all\") {\n availableSources.forEach((source) => {\n const chainId = source.chain.id;\n if (!selectedSourceSet.has(chainId)) {\n onToggleSourceChain(chainId);\n }\n });\n return;\n }\n\n const chainToKeep =\n availableSources.find((source) => selectedSourceSet.has(source.chain.id))\n ?.chain.id ?? selectedSourceChains[0];\n\n if (typeof chainToKeep !== \"number\") return;\n\n selectedSourceChains.forEach((chainId) => {\n if (chainId !== chainToKeep) {\n onToggleSourceChain(chainId);\n }\n });\n };\n\n return (\n onSourceMenuOpenChange?.(value === \"sources\")}\n >\n \n
\n {isLoading ? (\n <>\n
\n

You Spend

\n \n
\n
\n \n
\n \n
\n
\n \n ) : (\n intent?.sources && (\n <>\n
\n

You Spend

\n

\n {`${displayTokenSymbol} on ${\n intent?.sources?.length\n } ${intent?.sources?.length > 1 ? \"chains\" : \"chain\"}`}\n

\n
\n\n
\n

\n {formatTokenBalance(intent?.sourcesTotal, {\n symbol: displayTokenSymbol,\n decimals: intent?.token?.decimals,\n })}\n

\n \n

View Sources

\n \n
\n \n )\n )}\n
\n {!isLoading && (\n \n {showCoverageFeedback && (\n \n
\n
\n \n \n \n \n \n {Math.round(normalizedCoverage)}%\n \n
\n\n
\n

\n Available on selected chains:{\" \"}\n \n {formatTokenBalance(parseFloat(selectedTotal ?? \"0\"), {\n symbol: displayTokenSymbol,\n decimals: intent?.token?.decimals,\n })}\n \n

\n

\n Required for this transaction:{\" \"}\n \n {formatTokenBalance(\n parseFloat(requiredSafetyTotal ?? \"0\"),\n {\n symbol: displayTokenSymbol,\n decimals: intent?.token?.decimals,\n },\n )}\n \n

\n {shouldShowProceedMessage && (\n

\n Need{\" \"}\n \n {missingToProceed} {displayTokenSymbol}\n {\" \"}\n more on selected chains to continue.\n

\n )}\n {!isSourceSelectionInsufficient &&\n sourceCoverageState === \"healthy\" && (\n

\n You're all set. We'll only use what's\n needed from these selected chains.\n

\n )}\n
\n
\n
\n )}\n\n {availableSources.length === 0 ? (\n

\n No source balances available for this token.\n

\n ) : (\n
\n \n {bulkActionLabel}\n \n {availableSources.map((source) => {\n const chainId = source.chain.id;\n const isSelected = selectedSourceChains.includes(chainId);\n const isLastSelected = isSelected\n ? selectedSourceChains.length === 1\n : false;\n const willUseAmount = intent?.sources?.find(\n (s) => s.chainID === chainId,\n )?.amount;\n\n return (\n {\n if (isLastSelected) return;\n onToggleSourceChain(chainId);\n }}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (isLastSelected) return;\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onToggleSourceChain(chainId);\n }\n }}\n >\n
\n {\n if (isLastSelected) return;\n onToggleSourceChain(chainId);\n }}\n onClick={(e) => e.stopPropagation()}\n aria-label={`Select ${source.chain.name} as a source`}\n />\n \n

\n {source.chain.name}\n

\n
\n\n
\n

\n {formatTokenBalance(source.balance, {\n symbol: source.symbol,\n decimals: source.decimals,\n })}\n

\n {willUseAmount && (\n

\n Estimated to use:{\" \"}\n {formatTokenBalance(willUseAmount, {\n symbol: source.symbol,\n decimals: intent?.token?.decimals,\n })}\n

\n )}\n
\n
\n );\n })}\n
\n )}\n\n {availableSources.length > 0 && (\n
\n

Keep at least 1 chain selected.

\n
\n )}\n \n )}\n \n \n );\n};\n\nexport default SourceBreakdown;\n", + "content": "import {\n type BridgeIntent,\n type TokenBalance, type ChainBalance,\n} from \"@avail-project/nexus-sdk-v2\";\nimport { formatTokenBalance } from \"@avail-project/nexus-sdk-v2/utils\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"../../ui/accordion\";\nimport { Skeleton } from \"../../ui/skeleton\";\nimport { Checkbox } from \"../../ui/checkbox\";\nimport { cn } from \"@/lib/utils\";\n\ntype SourceCoverageState = \"healthy\" | \"warning\" | \"error\";\n\ninterface SourceBreakdownProps {\n intent?: BridgeIntent;\n tokenSymbol?: string; // v2: was SUPPORTED_TOKENS\n isLoading?: boolean;\n availableSources: ChainBalance[]; // v2: was UserAsset[\"breakdown\"]\n selectedSourceChains: number[];\n onToggleSourceChain: (chainId: number) => void;\n onSourceMenuOpenChange?: (open: boolean) => void;\n isSourceSelectionInsufficient?: boolean;\n sourceCoverageState?: SourceCoverageState;\n sourceCoveragePercent?: number;\n missingToProceed?: string;\n missingToSafety?: string;\n selectedTotal?: string;\n requiredTotal?: string;\n requiredSafetyTotal?: string;\n}\n\nconst SourceBreakdown = ({\n intent,\n tokenSymbol,\n isLoading = false,\n availableSources,\n selectedSourceChains,\n onToggleSourceChain,\n onSourceMenuOpenChange,\n isSourceSelectionInsufficient = false,\n sourceCoverageState = \"healthy\",\n sourceCoveragePercent = 100,\n missingToProceed,\n selectedTotal,\n requiredTotal,\n requiredSafetyTotal,\n}: SourceBreakdownProps) => {\n const displayTokenSymbol = tokenSymbol ?? availableSources[0]?.chain?.name;\n const normalizedCoverage = Math.max(0, Math.min(100, sourceCoveragePercent));\n const progressRadius = 16;\n const progressCircumference = 2 * Math.PI * progressRadius;\n const progressOffset =\n progressCircumference - (normalizedCoverage / 100) * progressCircumference;\n const showCoverageFeedback = Boolean(\n selectedTotal && requiredTotal && requiredSafetyTotal,\n );\n const shouldShowProceedMessage =\n sourceCoverageState === \"error\" &&\n Number.parseFloat(missingToProceed ?? \"0\") > 0;\n\n const coverageToneClass =\n sourceCoverageState === \"error\"\n ? \"text-rose-500\"\n : sourceCoverageState === \"warning\"\n ? \"text-amber-500\"\n : \"text-emerald-500\";\n\n const coverageSurfaceClass =\n sourceCoverageState === \"error\"\n ? \" text-rose-950 dark:text-rose-200\"\n : sourceCoverageState === \"warning\"\n ? \" text-amber-950 dark:text-amber-200\"\n : \" text-emerald-950 dark:text-emerald-200\";\n const selectedSourceSet = new Set(selectedSourceChains);\n const bulkActionLabel =\n selectedSourceChains.length > 1 ? \"Deselect all\" : \"Select all\";\n const isBulkActionDisabled = availableSources.length <= 1;\n const handleBulkSourceAction = () => {\n if (isBulkActionDisabled) return;\n\n if (bulkActionLabel === \"Select all\") {\n availableSources.forEach((source) => {\n const chainId = source.chain.id;\n if (!selectedSourceSet.has(chainId)) {\n onToggleSourceChain(chainId);\n }\n });\n return;\n }\n\n const chainToKeep =\n availableSources.find((source) => selectedSourceSet.has(source.chain.id))\n ?.chain.id ?? selectedSourceChains[0];\n\n if (typeof chainToKeep !== \"number\") return;\n\n selectedSourceChains.forEach((chainId) => {\n if (chainId !== chainToKeep) {\n onToggleSourceChain(chainId);\n }\n });\n };\n\n return (\n onSourceMenuOpenChange?.(value === \"sources\")}\n >\n \n
\n {isLoading ? (\n <>\n
\n

You Spend

\n \n
\n
\n \n
\n \n
\n
\n \n ) : (\n intent?.availableSources && (\n <>\n
\n

You Spend

\n

\n {`${displayTokenSymbol} on ${\n intent?.availableSources?.length\n } ${intent?.availableSources?.length > 1 ? \"chains\" : \"chain\"}`}\n

\n
\n\n
\n

\n {formatTokenBalance(intent?.sourcesTotal, {\n symbol: displayTokenSymbol,\n decimals: intent?.availableSources?.[0]?.token?.decimals,\n })}\n

\n \n

View Sources

\n \n
\n \n )\n )}\n
\n {!isLoading && (\n \n {showCoverageFeedback && (\n \n
\n
\n \n \n \n \n \n {Math.round(normalizedCoverage)}%\n \n
\n\n
\n

\n Available on selected chains:{\" \"}\n \n {formatTokenBalance(parseFloat(selectedTotal ?? \"0\"), {\n symbol: displayTokenSymbol,\n decimals: intent?.availableSources?.[0]?.token?.decimals,\n })}\n \n

\n

\n Required for this transaction:{\" \"}\n \n {formatTokenBalance(\n parseFloat(requiredSafetyTotal ?? \"0\"),\n {\n symbol: displayTokenSymbol,\n decimals: intent?.availableSources?.[0]?.token?.decimals,\n },\n )}\n \n

\n {shouldShowProceedMessage && (\n

\n Need{\" \"}\n \n {missingToProceed} {displayTokenSymbol}\n {\" \"}\n more on selected chains to continue.\n

\n )}\n {!isSourceSelectionInsufficient &&\n sourceCoverageState === \"healthy\" && (\n

\n You're all set. We'll only use what's\n needed from these selected chains.\n

\n )}\n
\n
\n
\n )}\n\n {availableSources.length === 0 ? (\n

\n No source balances available for this token.\n

\n ) : (\n
\n \n {bulkActionLabel}\n \n {availableSources.map((source) => {\n const chainId = source.chain.id;\n const isSelected = selectedSourceChains.includes(chainId);\n const isLastSelected = isSelected\n ? selectedSourceChains.length === 1\n : false;\n const willUseAmount = intent?.availableSources?.find(\n (s: any) => s.chain.id === chainId,\n )?.amount;\n\n return (\n {\n if (isLastSelected) return;\n onToggleSourceChain(chainId);\n }}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (isLastSelected) return;\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onToggleSourceChain(chainId);\n }\n }}\n >\n
\n {\n if (isLastSelected) return;\n onToggleSourceChain(chainId);\n }}\n onClick={(e) => e.stopPropagation()}\n aria-label={`Select ${source.chain.name} as a source`}\n />\n \n

\n {source.chain.name}\n

\n
\n\n
\n

\n {formatTokenBalance(source.balance, {\n symbol: tokenSymbol ?? source.chain?.name,\n decimals: source.decimals,\n })}\n

\n {willUseAmount && (\n

\n Estimated to use:{\" \"}\n {formatTokenBalance(willUseAmount, {\n symbol: tokenSymbol ?? source.chain?.name,\n decimals:\n intent?.availableSources?.[0]?.token?.decimals,\n })}\n

\n )}\n
\n
\n );\n })}\n
\n )}\n\n {availableSources.length > 0 && (\n
\n

Keep at least 1 chain selected.

\n
\n )}\n \n )}\n \n \n );\n};\n\nexport default SourceBreakdown;\n", "type": "registry:component", "target": "components/fast-bridge/components/source-breakdown.tsx" }, { "path": "registry/nexus-elements/fast-bridge/components/token-select.tsx", - "content": "import {\n type SUPPORTED_CHAINS_IDS,\n type SUPPORTED_TOKENS,\n} from \"@avail-project/nexus-core\";\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../../ui/select\";\nimport { Label } from \"../../ui/label\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { useMemo } from \"react\";\n\ninterface TokenSelectProps {\n selectedToken?: SUPPORTED_TOKENS;\n selectedChain: SUPPORTED_CHAINS_IDS;\n handleTokenSelect: (token: SUPPORTED_TOKENS) => void;\n isTestnet?: boolean;\n disabled?: boolean;\n label?: string;\n}\n\nconst TokenSelect = ({\n selectedToken,\n selectedChain,\n handleTokenSelect,\n isTestnet = false,\n disabled = false,\n label,\n}: TokenSelectProps) => {\n const { supportedChainsAndTokens } = useNexus();\n const tokenData = useMemo(() => {\n return supportedChainsAndTokens\n ?.filter((chain) => chain.id === selectedChain)\n .flatMap((chain) => chain.tokens);\n }, [selectedChain, supportedChainsAndTokens]);\n\n const selectedTokenData = tokenData?.find((token) => {\n return token.symbol === selectedToken;\n });\n\n return (\n \n !disabled && handleTokenSelect(value as SUPPORTED_TOKENS)\n }\n >\n
\n {label && }\n \n \n {selectedChain && selectedTokenData && (\n
\n \n {selectedToken}\n
\n )}\n
\n \n
\n\n \n \n {tokenData?.map((token) => (\n \n
\n \n
\n \n {isTestnet ? `${token.symbol} (Testnet)` : token.symbol}\n \n
\n
\n
\n ))}\n
\n
\n \n );\n};\n\nexport default TokenSelect;\n", + "content": "// v2: SUPPORTED_CHAINS_IDS, SUPPORTED_TOKENS removed — use plain string/number\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../../ui/select\";\nimport { Label } from \"../../ui/label\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { useMemo } from \"react\";\n\ninterface TokenSelectProps {\n selectedToken?: string;\n selectedChain: number;\n handleTokenSelect: (token: string) => void;\n isTestnet?: boolean;\n disabled?: boolean;\n label?: string;\n}\n\nconst TokenSelect = ({\n selectedToken,\n selectedChain,\n handleTokenSelect,\n isTestnet = false,\n disabled = false,\n label,\n}: TokenSelectProps) => {\n const { supportedChainsAndTokens } = useNexus();\n const tokenData = useMemo(() => {\n return supportedChainsAndTokens\n ?.filter((chain) => chain.id === selectedChain)\n .flatMap((chain) => chain.tokens);\n }, [selectedChain, supportedChainsAndTokens]);\n\n const selectedTokenData = tokenData?.find((token) => {\n return token.symbol === selectedToken;\n });\n\n return (\n \n !disabled && handleTokenSelect(value)\n }\n >\n
\n {label && }\n \n \n {selectedChain && selectedTokenData && (\n
\n \n {selectedToken}\n
\n )}\n
\n \n
\n\n \n \n {tokenData?.map((token) => (\n \n
\n \n
\n \n {isTestnet ? `${token.symbol} (Testnet)` : token.symbol}\n \n
\n
\n
\n ))}\n
\n
\n \n );\n};\n\nexport default TokenSelect;\n", "type": "registry:component", "target": "components/fast-bridge/components/token-select.tsx" }, { "path": "registry/nexus-elements/fast-bridge/components/transaction-progress.tsx", - "content": "import { Check, Circle, LoaderPinwheel, SquareArrowOutUpRight } from \"lucide-react\";\nimport { type FC, memo, useMemo } from \"react\";\nimport {\n type BridgeStepType,\n type SwapStepType,\n} from \"@avail-project/nexus-core\";\nimport { Button } from \"../../ui/button\";\n\ntype ProgressStep = BridgeStepType | SwapStepType;\n\ninterface TransactionProgressProps {\n timer: number;\n steps: Array<{ id: number; completed: boolean; step: ProgressStep }>;\n viewIntentUrl?: string;\n operationType?: string;\n completed?: boolean;\n}\n\nexport const getOperationText = (type: string) => {\n switch (type) {\n case \"bridge\":\n return \"Transaction\";\n case \"transfer\":\n return \"Transferring\";\n case \"bridgeAndExecute\":\n return \"Bridge & Execute\";\n case \"swap\":\n return \"Swapping\";\n default:\n return \"Processing\";\n }\n};\n\ntype DisplayStep = { id: string; label: string; completed: boolean };\n\nconst StepList: FC<{ steps: DisplayStep[]; currentIndex: number }> = memo(\n ({ steps, currentIndex }) => {\n return (\n
\n {steps.map((s, idx) => {\n const isCompleted = !!s.completed;\n const isCurrent = currentIndex === -1 ? false : idx === currentIndex;\n\n let rightIcon = ;\n if (isCompleted) {\n rightIcon = ;\n } else if (isCurrent) {\n rightIcon = (\n \n );\n }\n\n return (\n \n
\n {s.label}\n
\n {rightIcon}\n
\n );\n })}\n \n );\n }\n);\nStepList.displayName = \"StepList\";\n\nconst TransactionProgress: FC = ({\n timer,\n steps,\n viewIntentUrl,\n operationType = \"bridge\",\n completed = false,\n}) => {\n const totalSteps = Array.isArray(steps) ? steps.length : 0;\n const completedSteps = Array.isArray(steps)\n ? steps.reduce((acc, s) => acc + (s?.completed ? 1 : 0), 0)\n : 0;\n const rawPercent = totalSteps > 0 ? completedSteps / totalSteps : 0;\n const percent = completed ? 1 : rawPercent;\n const allCompleted = completed || percent >= 1;\n const opText = getOperationText(operationType);\n const headerText = allCompleted\n ? `${opText} Completed`\n : `${opText} In Progress...`;\n const ctaText = allCompleted ? `View Explorer` : \"View Intent\";\n\n const { effectiveSteps, currentIndex } = useMemo(() => {\n const milestones = [\n \"Intent verified\",\n \"Collected on sources\",\n \"Filled on destination\",\n ];\n const thresholds = milestones.map(\n (_, idx) => (idx + 1) / milestones.length\n );\n const displaySteps: DisplayStep[] = milestones.map((label, idx) => ({\n id: `M${idx}`,\n label,\n completed: idx === 0 ? timer > 0 : percent >= thresholds[idx],\n }));\n const current = displaySteps.findIndex((st) => !st.completed);\n return { effectiveSteps: displaySteps, currentIndex: current };\n }, [percent, timer, completed]);\n\n return (\n
\n
\n {allCompleted ? (\n \n ) : (\n \n )}\n

{headerText}

\n
\n \n {Math.floor(timer)}\n \n \n .\n \n \n {String(Math.floor((timer % 1) * 1000)).padStart(3, \"0\")}s\n \n
\n
\n\n \n\n {viewIntentUrl && (\n \n )}\n
\n );\n};\n\nexport default TransactionProgress;\n", + "content": "import { Check, Circle, LoaderPinwheel, SquareArrowOutUpRight } from \"lucide-react\";\nimport { type FC, memo, useMemo } from \"react\";\nimport { Button } from \"../../ui/button\";\n\ntype ProgressStep = { type?: string; typeID?: string; [key: string]: unknown };\n\ninterface TransactionProgressProps {\n timer: number;\n steps: Array<{ id: number; completed: boolean; step: ProgressStep }>;\n viewIntentUrl?: string;\n operationType?: string;\n completed?: boolean;\n}\n\nexport const getOperationText = (type: string) => {\n switch (type) {\n case \"bridge\":\n return \"Transaction\";\n case \"transfer\":\n return \"Transferring\";\n case \"bridgeAndExecute\":\n return \"Bridge & Execute\";\n case \"swap\":\n return \"Swapping\";\n default:\n return \"Processing\";\n }\n};\n\ntype DisplayStep = { id: string; label: string; completed: boolean };\n\nconst StepList: FC<{ steps: DisplayStep[]; currentIndex: number }> = memo(\n ({ steps, currentIndex }) => {\n return (\n
\n {steps.map((s, idx) => {\n const isCompleted = !!s.completed;\n const isCurrent = currentIndex === -1 ? false : idx === currentIndex;\n\n let rightIcon = ;\n if (isCompleted) {\n rightIcon = ;\n } else if (isCurrent) {\n rightIcon = (\n \n );\n }\n\n return (\n \n
\n {s.label}\n
\n {rightIcon}\n
\n );\n })}\n \n );\n }\n);\nStepList.displayName = \"StepList\";\n\nconst TransactionProgress: FC = ({\n timer,\n steps,\n viewIntentUrl,\n operationType = \"bridge\",\n completed = false,\n}) => {\n // Map step types to their completion status for explicit milestone detection\n const stepMap = useMemo(() => {\n const m = new Map();\n if (Array.isArray(steps)) {\n for (const s of steps) {\n const type = s?.step?.type;\n if (type) m.set(type, !!s.completed);\n }\n }\n return m;\n }, [steps]);\n\n // Milestone logic — uses VALUE check (get() === true), NOT key presence (has())\n // plan_preview seeds all step types with completed=false, so has() would fire immediately.\n\n // Intent verified = signing has started or intent submitted\n const intentVerified =\n completed ||\n stepMap.get(\"request_signing\") === true ||\n stepMap.get(\"request_submission\") === true ||\n stepMap.get(\"allowance_approval\") === true;\n\n // Collected on sources = relayer picked up the intent (bridge_fill seen in any state)\n // bridge_fill gets marked completed=true when EITHER bridge_fill:waiting OR bridge_fill:completed fires\n const collectedOnSources =\n completed ||\n stepMap.get(\"vault_deposit\") === true ||\n stepMap.get(\"bridge_fill\") === true;\n\n // Filled on destination = ONLY when SDK status is \"success\"\n // (fires from bridge_fill:completed terminal event → onSuccess → status=\"success\")\n const filledOnDestination = completed;\n\n // Overall done: ONLY from the completed prop (event-driven), never from step percent\n const allCompleted = completed;\n\n const opText = getOperationText(operationType);\n const headerText = allCompleted\n ? `${opText} Completed`\n : `${opText} In Progress...`;\n const ctaText = allCompleted ? `View Explorer` : \"View Intent\";\n\n const { effectiveSteps, currentIndex } = useMemo(() => {\n const displaySteps: DisplayStep[] = [\n { id: \"M0\", label: \"Intent verified\", completed: intentVerified },\n { id: \"M1\", label: \"Collected on sources\", completed: collectedOnSources },\n { id: \"M2\", label: \"Filled on destination\", completed: filledOnDestination },\n ];\n const current = allCompleted ? -1 : displaySteps.findIndex((st) => !st.completed);\n return { effectiveSteps: displaySteps, currentIndex: current };\n }, [intentVerified, collectedOnSources, filledOnDestination, allCompleted]);\n\n\n return (\n
\n
\n {allCompleted ? (\n \n ) : (\n \n )}\n

{headerText}

\n
\n \n {Math.floor(timer)}\n \n \n .\n \n \n {String(Math.floor((timer % 1) * 1000)).padStart(3, \"0\")}s\n \n
\n
\n\n \n\n {viewIntentUrl && (\n \n )}\n
\n );\n};\n\nexport default TransactionProgress;\n", "type": "registry:component", "target": "components/fast-bridge/components/transaction-progress.tsx" }, { "path": "registry/nexus-elements/fast-bridge/fast-bridge.tsx", - "content": "\"use client\";\nimport { type FC, useEffect, useState } from \"react\";\nimport { Card, CardContent } from \"../ui/card\";\nimport ChainSelect from \"./components/chain-select\";\nimport TokenSelect from \"./components/token-select\";\nimport { Button } from \"../ui/button\";\nimport { LoaderPinwheel, X } from \"lucide-react\";\nimport { useNexus } from \"../nexus/NexusProvider\";\nimport AmountInput from \"./components/amount-input\";\nimport FeeBreakdown from \"./components/fee-breakdown\";\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from \"../ui/dialog\";\nimport TransactionProgress from \"./components/transaction-progress\";\nimport AllowanceModal from \"./components/allowance-modal\";\nimport useBridge from \"./hooks/useBridge\";\nimport SourceBreakdown from \"./components/source-breakdown\";\nimport {\n type SUPPORTED_CHAINS_IDS,\n type SUPPORTED_TOKENS,\n} from \"@avail-project/nexus-core\";\nimport { type Address } from \"viem\";\nimport { Skeleton } from \"../ui/skeleton\";\nimport RecipientAddress from \"./components/recipient-address\";\nimport ViewHistory from \"../view-history/view-history\";\n\ninterface FastBridgeProps {\n connectedAddress: Address;\n maxAmount?: string | number;\n prefill?: {\n token: SUPPORTED_TOKENS;\n chainId: SUPPORTED_CHAINS_IDS;\n amount?: string;\n recipient?: Address;\n };\n onComplete?: (explorerUrl?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n}\n\nconst FastBridge: FC = ({\n connectedAddress,\n maxAmount,\n onComplete,\n onStart,\n onError,\n prefill,\n}) => {\n const handleComplete = (explorerUrl?: string) => {\n onComplete?.(explorerUrl);\n };\n\n const [isSourceMenuOpen, setIsSourceMenuOpen] = useState(false);\n const {\n nexusSDK,\n intent,\n bridgableBalance,\n allowance,\n network,\n fetchBridgableBalance,\n } = useNexus();\n\n const {\n inputs,\n setInputs,\n timer,\n loading,\n refreshing,\n isDialogOpen,\n txError,\n setTxError,\n handleTransaction,\n reset,\n filteredBridgableBalance,\n startTransaction,\n setIsDialogOpen,\n commitAmount,\n lastExplorerUrl,\n steps,\n status,\n availableSources,\n selectedSourceChains,\n toggleSourceChain,\n isSourceSelectionInsufficient,\n isSourceSelectionReadyForAccept,\n sourceCoverageState,\n sourceCoveragePercent,\n missingToProceed,\n missingToSafety,\n selectedTotal,\n requiredTotal,\n requiredSafetyTotal,\n maxAvailableAmount,\n isInputsValid,\n } = useBridge({\n prefill,\n network: network ?? \"mainnet\",\n connectedAddress,\n nexusSDK,\n intent,\n bridgableBalance,\n allowance,\n onComplete: handleComplete,\n onStart,\n onError,\n fetchBalance: fetchBridgableBalance,\n maxAmount,\n isSourceMenuOpen,\n });\n\n useEffect(() => {\n if (!intent.current?.intent) {\n setIsSourceMenuOpen(false);\n }\n }, [intent.current?.intent]);\n\n return (\n \n \n \n \n setInputs({\n ...inputs,\n chain,\n })\n }\n label=\"To\"\n disabled={!!prefill?.chainId}\n />\n setInputs({ ...inputs, token })}\n disabled={!!prefill?.token}\n />\n setInputs({ ...inputs, amount })}\n bridgableBalance={filteredBridgableBalance}\n onCommit={() => void commitAmount()}\n disabled={refreshing || !!prefill?.amount}\n inputs={inputs}\n maxAmount={maxAmount}\n maxAvailableAmount={maxAvailableAmount}\n />\n \n setInputs({ ...inputs, recipient: address as `0x${string}` })\n }\n disabled={!!prefill?.recipient}\n />\n {intent?.current?.intent && (\n <>\n \n\n
\n

You receive

\n
\n {refreshing ? (\n \n ) : (\n

\n {`${\n connectedAddress === inputs?.recipient\n ? intent?.current?.intent?.destination?.amount\n : inputs.amount\n } ${inputs?.token === \"USDM\" ? \"USDM\" : filteredBridgableBalance?.symbol}`}\n

\n )}\n {refreshing ? (\n \n ) : (\n

\n on {intent?.current?.intent?.destination?.chainName}\n

\n )}\n
\n
\n \n \n )}\n\n {!intent.current && (\n \n {loading ? (\n \n ) : (\n \"Bridge\"\n )}\n \n )}\n\n {\n if (loading) return;\n setIsDialogOpen(open);\n }}\n >\n {intent.current && !isDialogOpen && (\n
\n \n \n \n {refreshing ? \"Refreshing...\" : \"Accept\"}\n \n \n
\n )}\n\n \n \n Transaction Progress\n \n {allowance.current ? (\n \n ) : (\n \n )}\n \n \n\n {txError && (\n
\n {txError}\n {\n reset();\n setTxError(null);\n }}\n className=\"text-destructive-foreground/80 hover:text-destructive-foreground focus:outline-none\"\n aria-label=\"Dismiss error\"\n >\n \n \n
\n )}\n
\n
\n );\n};\n\nexport default FastBridge;\n", + "content": "\"use client\";\nimport { type FC, useEffect, useState } from \"react\";\nimport { Card, CardContent } from \"../ui/card\";\nimport ChainSelect from \"./components/chain-select\";\nimport TokenSelect from \"./components/token-select\";\nimport { Button } from \"../ui/button\";\nimport { LoaderPinwheel, X } from \"lucide-react\";\nimport { useNexus } from \"../nexus/NexusProvider\";\nimport AmountInput from \"./components/amount-input\";\nimport FeeBreakdown from \"./components/fee-breakdown\";\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from \"../ui/dialog\";\nimport TransactionProgress from \"./components/transaction-progress\";\nimport AllowanceModal from \"./components/allowance-modal\";\nimport useBridge from \"./hooks/useBridge\";\nimport SourceBreakdown from \"./components/source-breakdown\";\nimport { type Address } from \"viem\";\nimport { Skeleton } from \"../ui/skeleton\";\nimport RecipientAddress from \"./components/recipient-address\";\nimport ViewHistory from \"../view-history/view-history\";\n\ninterface FastBridgeProps {\n connectedAddress: Address;\n maxAmount?: string | number;\n prefill?: {\n token: string; // v2: was SUPPORTED_TOKENS\n chainId: number; // v2: was SUPPORTED_CHAINS_IDS\n amount?: string;\n recipient?: Address;\n };\n onComplete?: (explorerUrl?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n}\n\nconst FastBridge: FC = ({\n connectedAddress,\n maxAmount,\n onComplete,\n onStart,\n onError,\n prefill,\n}) => {\n const handleComplete = (explorerUrl?: string) => {\n onComplete?.(explorerUrl);\n };\n\n const [isSourceMenuOpen, setIsSourceMenuOpen] = useState(false);\n const {\n nexusSDK,\n intent,\n bridgableBalance,\n allowance,\n network,\n fetchBridgableBalance,\n } = useNexus();\n\n const {\n inputs,\n setInputs,\n timer,\n loading,\n refreshing,\n isDialogOpen,\n txError,\n setTxError,\n handleTransaction,\n reset,\n filteredBridgableBalance,\n startTransaction,\n setIsDialogOpen,\n commitAmount,\n lastExplorerUrl,\n steps,\n status,\n availableSources,\n selectedSourceChains,\n toggleSourceChain,\n isSourceSelectionInsufficient,\n isSourceSelectionReadyForAccept,\n sourceCoverageState,\n sourceCoveragePercent,\n missingToProceed,\n missingToSafety,\n selectedTotal,\n requiredTotal,\n requiredSafetyTotal,\n maxAvailableAmount,\n isInputsValid,\n } = useBridge({\n prefill,\n network: network ?? \"mainnet\",\n connectedAddress,\n nexusSDK,\n intent,\n bridgableBalance,\n allowance,\n onComplete: handleComplete,\n onStart,\n onError,\n fetchBalance: fetchBridgableBalance,\n maxAmount,\n isSourceMenuOpen,\n });\n\n useEffect(() => {\n if (!intent.current?.intent) {\n setIsSourceMenuOpen(false);\n }\n }, [intent.current?.intent]);\n\n return (\n \n \n \n \n setInputs({\n ...inputs,\n chain,\n })\n }\n label=\"To\"\n disabled={!!prefill?.chainId}\n />\n setInputs({ ...inputs, token })}\n disabled={!!prefill?.token}\n />\n setInputs({ ...inputs, amount })}\n bridgableBalance={filteredBridgableBalance}\n onCommit={() => void commitAmount()}\n disabled={refreshing || !!prefill?.amount}\n inputs={inputs}\n maxAmount={maxAmount}\n maxAvailableAmount={maxAvailableAmount}\n />\n \n setInputs({ ...inputs, recipient: address as `0x${string}` })\n }\n disabled={!!prefill?.recipient}\n />\n {intent?.current?.intent && (\n <>\n \n\n
\n

You receive

\n
\n {refreshing ? (\n \n ) : (\n

\n {`${\n connectedAddress === inputs?.recipient\n ? intent?.current?.intent?.destination?.amount\n : inputs.amount\n } ${inputs?.token === \"USDM\" ? \"USDM\" : filteredBridgableBalance?.symbol}`}\n

\n )}\n {refreshing ? (\n \n ) : (\n

\n on {(intent?.current?.intent?.destination as { chain?: { name?: string } })?.chain?.name}\n

\n )}\n
\n
\n \n \n )}\n\n {!intent.current && (\n \n {loading ? (\n \n ) : (\n \"Bridge\"\n )}\n \n )}\n\n {\n if (loading) return;\n setIsDialogOpen(open);\n }}\n >\n {intent.current && !isDialogOpen && (\n
\n \n \n \n {refreshing ? \"Refreshing...\" : \"Accept\"}\n \n \n
\n )}\n\n \n \n Transaction Progress\n \n {allowance.current ? (\n \n ) : (\n \n )}\n \n \n\n {txError && (\n
\n {txError}\n {\n reset();\n setTxError(null);\n }}\n className=\"text-destructive-foreground/80 hover:text-destructive-foreground focus:outline-none\"\n aria-label=\"Dismiss error\"\n >\n \n \n
\n )}\n
\n
\n );\n};\n\nexport default FastBridge;\n", "type": "registry:component", "target": "components/fast-bridge/fast-bridge.tsx" }, { "path": "registry/nexus-elements/fast-bridge/hooks/useBridge.ts", - "content": "import {\n type NexusNetwork,\n NexusSDK,\n type OnAllowanceHookData,\n type OnIntentHookData,\n type SUPPORTED_CHAINS_IDS,\n type SUPPORTED_TOKENS,\n type UserAsset,\n} from \"@avail-project/nexus-core\";\nimport { useCallback, type RefObject } from \"react\";\nimport { type Address } from \"viem\";\nimport {\n type TransactionFlowExecuteParams,\n type TransactionFlowInputs,\n type TransactionFlowPrefill,\n useTransactionFlow,\n} from \"../../common\";\nimport { notifyIntentHistoryRefresh } from \"../../view-history/history-events\";\n\nexport type FastBridgeState = TransactionFlowInputs;\n\ninterface UseBridgeProps {\n network: NexusNetwork;\n connectedAddress: Address;\n nexusSDK: NexusSDK | null;\n intent: RefObject;\n allowance: RefObject;\n bridgableBalance: UserAsset[] | null;\n prefill?: {\n token: SUPPORTED_TOKENS;\n chainId: SUPPORTED_CHAINS_IDS;\n amount?: string;\n recipient?: Address;\n };\n onComplete?: (explorerUrl?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n maxAmount?: string | number;\n isSourceMenuOpen?: boolean;\n}\n\nconst useBridge = ({\n network,\n connectedAddress,\n nexusSDK,\n intent,\n bridgableBalance,\n prefill,\n onComplete,\n onStart,\n onError,\n fetchBalance,\n allowance,\n maxAmount,\n isSourceMenuOpen = false,\n}: UseBridgeProps) => {\n const executeTransaction = useCallback(\n async ({\n token,\n amount,\n toChainId,\n recipient,\n sourceChains,\n onEvent,\n }: TransactionFlowExecuteParams) => {\n if (!nexusSDK) return null;\n return nexusSDK.bridge(\n {\n token,\n amount,\n toChainId,\n recipient: recipient ?? connectedAddress,\n sourceChains,\n },\n { onEvent },\n );\n },\n [connectedAddress, nexusSDK],\n );\n\n const flow = useTransactionFlow({\n type: \"bridge\",\n network,\n connectedAddress,\n nexusSDK,\n intent,\n bridgableBalance,\n prefill: prefill as TransactionFlowPrefill | undefined,\n onComplete,\n onStart,\n onError,\n fetchBalance,\n allowance,\n maxAmount,\n isSourceMenuOpen,\n notifyHistoryRefresh: notifyIntentHistoryRefresh,\n executeTransaction,\n });\n\n return {\n ...flow,\n inputs: flow.inputs as FastBridgeState,\n setInputs: flow.setInputs as (\n next: FastBridgeState | Partial,\n ) => void,\n };\n};\n\nexport default useBridge;\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type {\n NexusNetwork,\n OnAllowanceHookData,\n OnIntentHookData,\n TokenBalance,\n} from \"@avail-project/nexus-sdk-v2\";\nimport { useCallback, type RefObject } from \"react\";\nimport { type Address } from \"viem\";\nimport {\n type TransactionFlowExecuteParams,\n type TransactionFlowInputs,\n type TransactionFlowPrefill,\n useTransactionFlow,\n} from \"../../common\";\nimport { notifyIntentHistoryRefresh } from \"../../view-history/history-events\";\n\ntype NexusClient = ReturnType;\n\nexport type FastBridgeState = TransactionFlowInputs;\n\ninterface UseBridgeProps {\n network: NexusNetwork;\n connectedAddress: Address;\n nexusSDK: NexusClient | null;\n intent: RefObject;\n allowance: RefObject;\n bridgableBalance: TokenBalance[] | null;\n prefill?: {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n };\n onComplete?: (explorerUrl?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n maxAmount?: string | number;\n isSourceMenuOpen?: boolean;\n}\n\nconst useBridge = ({\n network,\n connectedAddress,\n nexusSDK,\n intent,\n bridgableBalance,\n prefill,\n onComplete,\n onStart,\n onError,\n fetchBalance,\n allowance,\n maxAmount,\n isSourceMenuOpen = false,\n}: UseBridgeProps) => {\n const executeTransaction = useCallback(\n async ({\n token,\n amount,\n toChainId,\n recipient,\n sources,\n onEvent,\n }: TransactionFlowExecuteParams) => {\n if (!nexusSDK) return null;\n // v2 params: toTokenSymbol, toAmountRaw, sources (not sourceChains)\n return nexusSDK.bridge(\n {\n toTokenSymbol: token,\n toAmountRaw: amount,\n toChainId,\n recipient: recipient ?? connectedAddress,\n sources,\n },\n {\n onEvent,\n hooks: {\n onIntent: (data) => {\n // hooks are per-operation in v2\n (intent as RefObject).current = data;\n },\n onAllowance: (data) => {\n (allowance as RefObject).current = data;\n },\n },\n },\n );\n },\n [connectedAddress, nexusSDK, intent, allowance],\n );\n\n const flow = useTransactionFlow({\n type: \"bridge\",\n network,\n connectedAddress,\n nexusSDK,\n intent,\n bridgableBalance,\n prefill: prefill as TransactionFlowPrefill | undefined,\n onComplete,\n onStart,\n onError,\n fetchBalance,\n allowance,\n maxAmount,\n isSourceMenuOpen,\n notifyHistoryRefresh: notifyIntentHistoryRefresh,\n executeTransaction,\n });\n\n return {\n ...flow,\n inputs: flow.inputs as FastBridgeState,\n setInputs: flow.setInputs as (\n next: FastBridgeState | Partial,\n ) => void,\n };\n};\n\nexport default useBridge;\n", "type": "registry:component", "target": "components/fast-bridge/hooks/useBridge.ts" }, @@ -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-sdk-v2\";\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.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.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.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 // v2: RFF_FEE_EXPIRED was removed; use string key for forward compat\n [\"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 );\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 as unknown as { data?: { context?: unknown } })?.data?.context,\n details: (err as unknown as { data?: { details?: unknown } })?.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" }, @@ -141,13 +141,13 @@ }, { "path": "registry/nexus-elements/common/hooks/useTransactionExecution.ts", - "content": "import {\n type BridgeStepType,\n NEXUS_EVENTS,\n type NexusSDK,\n type OnAllowanceHookData,\n type OnIntentHookData,\n} from \"@avail-project/nexus-core\";\nimport {\n type Dispatch,\n type RefObject,\n type SetStateAction,\n useCallback,\n useRef,\n} from \"react\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport {\n type SourceSelectionValidation,\n type TransactionFlowEvent,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n} from \"../types/transaction-flow\";\n\ninterface NexusErrorInfo {\n code: string;\n message: string;\n context?: unknown;\n details?: unknown;\n}\n\ntype NexusErrorHandler = (error: unknown) => NexusErrorInfo;\n\ninterface UseTransactionExecutionProps {\n operationName: \"bridge\" | \"transfer\";\n nexusSDK: NexusSDK | null;\n intent: RefObject;\n allowance: RefObject;\n inputs: TransactionFlowInputs;\n configuredMaxAmount?: string;\n allAvailableSourceChainIds: number[];\n sourceChainsForSdk?: number[];\n sourceSelectionKey: string;\n sourceSelection: SourceSelectionValidation;\n loading: boolean;\n txError: string | null;\n areInputsValid: boolean;\n executeTransaction: TransactionFlowExecutor;\n getMaxForCurrentSelection: () => Promise;\n onStepsList: (steps: BridgeStepType[]) => void;\n onStepComplete: (step: BridgeStepType) => void;\n resetSteps: () => void;\n setStatus: (status: TransactionStatus) => void;\n resetInputs: () => void;\n setRefreshing: Dispatch>;\n setIsDialogOpen: Dispatch>;\n setTxError: Dispatch>;\n setLastExplorerUrl: Dispatch>;\n setSelectedSourceChains: Dispatch>;\n setAppliedSourceSelectionKey: Dispatch>;\n stopwatch: {\n start: () => void;\n stop: () => void;\n reset: () => void;\n };\n handleNexusError: NexusErrorHandler;\n onStart?: () => void;\n onComplete?: (explorerUrl?: string) => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n notifyHistoryRefresh?: () => void;\n}\n\nexport function useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n}: UseTransactionExecutionProps) {\n const commitLockRef = useRef(false);\n const runIdRef = useRef(0);\n\n const refreshIntent = async (options?: { reportError?: boolean }) => {\n if (!intent.current) return false;\n const activeRunId = runIdRef.current;\n setRefreshing(true);\n try {\n const updated = await intent.current.refresh(sourceChainsForSdk);\n if (activeRunId !== runIdRef.current) return false;\n if (updated) {\n intent.current.intent = updated;\n }\n setAppliedSourceSelectionKey(sourceSelectionKey);\n return true;\n } catch (error) {\n if (activeRunId !== runIdRef.current) return false;\n console.error(\"Transaction failed:\", error);\n if (options?.reportError) {\n const message = \"Unable to refresh source selection. Please try again.\";\n setTxError(message);\n onError?.(message);\n }\n return false;\n } finally {\n if (activeRunId === runIdRef.current) {\n setRefreshing(false);\n }\n }\n };\n\n const onSuccess = async (explorerUrl?: string) => {\n stopwatch.stop();\n setStatus(\"success\");\n onComplete?.(explorerUrl);\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n await fetchBalance();\n notifyHistoryRefresh?.();\n };\n\n const handleTransaction = async () => {\n if (commitLockRef.current) return;\n commitLockRef.current = true;\n const currentRunId = ++runIdRef.current;\n let didEnterExecutingState = false;\n const cleanupSupersededExecution = () => {\n if (!didEnterExecutingState) return;\n setRefreshing(false);\n setIsDialogOpen(false);\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n setStatus(\"idle\");\n };\n\n try {\n if (\n !inputs?.amount ||\n !inputs?.recipient ||\n !inputs?.chain ||\n !inputs?.token\n ) {\n console.error(\"Missing required inputs\");\n return;\n }\n if (!nexusSDK) {\n const message = \"Nexus SDK not initialized\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n const parsedAmount = Number(inputs.amount);\n if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) {\n const message = \"Enter a valid amount greater than 0.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n const amountBigInt = nexusSDK.convertTokenReadableAmountToBigInt(\n inputs.amount,\n inputs.token,\n inputs.chain,\n );\n\n if (configuredMaxAmount) {\n const configuredMaxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n configuredMaxAmount,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > configuredMaxRaw) {\n const message = `Amount exceeds maximum limit of ${configuredMaxAmount} ${inputs.token}.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n }\n\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (currentRunId !== runIdRef.current) return;\n if (!maxForCurrentSelection) {\n const message = `Unable to determine max ${operationName} amount for selected sources. Please try again.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n const maxForSelectionRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxForCurrentSelection,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > maxForSelectionRaw) {\n const message = `Selected sources can provide up to ${maxForCurrentSelection} ${inputs.token}. Reduce amount or enable more sources.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n setStatus(\"executing\");\n didEnterExecutingState = true;\n setTxError(null);\n onStart?.();\n setLastExplorerUrl(\"\");\n setAppliedSourceSelectionKey(sourceSelectionKey);\n\n const onEvent = (event: TransactionFlowEvent) => {\n if (currentRunId !== runIdRef.current) return;\n if (event.name === NEXUS_EVENTS.STEPS_LIST) {\n const list = Array.isArray(event.args) ? event.args : [];\n onStepsList(list as BridgeStepType[]);\n }\n if (event.name === NEXUS_EVENTS.STEP_COMPLETE) {\n if (\n !Array.isArray(event.args) &&\n \"type\" in event.args &&\n event.args.type === \"INTENT_HASH_SIGNED\"\n ) {\n stopwatch.start();\n }\n if (!Array.isArray(event.args)) {\n onStepComplete(event.args as BridgeStepType);\n }\n }\n };\n\n const transactionResult = await executeTransaction({\n token: inputs.token,\n amount: amountBigInt,\n toChainId: inputs.chain,\n recipient: inputs.recipient,\n sourceChains: sourceChainsForSdk,\n onEvent,\n });\n\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution();\n return;\n }\n if (!transactionResult) {\n throw new Error(\"Transaction rejected by user\");\n }\n setLastExplorerUrl(transactionResult.explorerUrl);\n await onSuccess(transactionResult.explorerUrl);\n } catch (error) {\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution();\n return;\n }\n const { message, code, context, details } = handleNexusError(error);\n console.error(`Fast ${operationName} transaction failed:`, {\n code,\n message,\n context,\n details,\n });\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n setTxError(message);\n onError?.(message);\n setIsDialogOpen(false);\n setSelectedSourceChains(null);\n setRefreshing(false);\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n void fetchBalance();\n setStatus(\"error\");\n } finally {\n commitLockRef.current = false;\n }\n };\n\n const reset = () => {\n runIdRef.current += 1;\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setStatus(\"idle\");\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n };\n\n const startTransaction = () => {\n if (!intent.current) return;\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (sourceSelection.isBelowRequired && inputs?.token) {\n const message = `Selected sources are not enough. Add ${sourceSelection.missingToProceed} ${inputs.token} more to make this transaction.`;\n setTxError(message);\n onError?.(message);\n return;\n }\n void (async () => {\n const refreshed = await refreshIntent({ reportError: true });\n if (!refreshed || !intent.current) return;\n intent.current.allow();\n setIsDialogOpen(true);\n setTxError(null);\n })();\n };\n\n const commitAmount = async () => {\n if (intent.current || loading || txError || !areInputsValid) return;\n await handleTransaction();\n };\n\n const invalidatePendingExecution = useCallback(() => {\n runIdRef.current += 1;\n if (intent.current) {\n intent.current.deny();\n intent.current = null;\n }\n setRefreshing(false);\n setAppliedSourceSelectionKey(\"ALL\");\n }, [intent, setAppliedSourceSelectionKey, setRefreshing]);\n\n return {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n };\n}\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type { OnAllowanceHookData, OnIntentHookData } from \"@avail-project/nexus-sdk-v2\";\nimport {\n type Dispatch,\n type RefObject,\n type SetStateAction,\n useCallback,\n useRef,\n} from \"react\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport {\n type SourceSelectionValidation,\n type TransactionFlowEvent,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n} from \"../types/transaction-flow\";\n\ntype NexusClient = ReturnType;\n\ninterface NexusErrorInfo {\n code: string;\n message: string;\n context?: unknown;\n details?: unknown;\n}\n\ntype NexusErrorHandler = (error: unknown) => NexusErrorInfo;\n\n// v2 plan_progress step types for bridge\nconst BRIDGE_STEP_INTENT_SIGNED = \"request_signing\";\n\ninterface UseTransactionExecutionProps {\n operationName: \"bridge\" | \"transfer\";\n nexusSDK: NexusClient | null;\n intent: RefObject;\n allowance: RefObject;\n inputs: TransactionFlowInputs;\n configuredMaxAmount?: string;\n allAvailableSourceChainIds: number[];\n sourceChainsForSdk?: number[];\n sourceSelectionKey: string;\n sourceSelection: SourceSelectionValidation;\n loading: boolean;\n txError: string | null;\n areInputsValid: boolean;\n executeTransaction: TransactionFlowExecutor;\n getMaxForCurrentSelection: () => Promise;\n onStepsList: (steps: { typeID?: string; type?: string; [key: string]: unknown }[]) => void;\n onStepComplete: (step: { typeID?: string; type?: string; [key: string]: unknown }) => void;\n resetSteps: () => void;\n setStatus: (status: TransactionStatus) => void;\n resetInputs: () => void;\n setRefreshing: Dispatch>;\n setIsDialogOpen: Dispatch>;\n setTxError: Dispatch>;\n setLastExplorerUrl: Dispatch>;\n setSelectedSourceChains: Dispatch>;\n setAppliedSourceSelectionKey: Dispatch>;\n stopwatch: {\n start: () => void;\n stop: () => void;\n reset: () => void;\n };\n handleNexusError: NexusErrorHandler;\n onStart?: () => void;\n onComplete?: (explorerUrl?: string) => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n notifyHistoryRefresh?: () => void;\n}\n\nexport function useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n}: UseTransactionExecutionProps) {\n const commitLockRef = useRef(false);\n const runIdRef = useRef(0);\n\n const refreshIntent = async (options?: { reportError?: boolean }) => {\n if (!intent.current) return false;\n const activeRunId = runIdRef.current;\n setRefreshing(true);\n try {\n const updated = await intent.current.refresh(sourceChainsForSdk);\n if (activeRunId !== runIdRef.current) return false;\n if (updated) {\n intent.current.intent = updated;\n }\n setAppliedSourceSelectionKey(sourceSelectionKey);\n return true;\n } catch (error) {\n if (activeRunId !== runIdRef.current) return false;\n console.error(\"Transaction failed:\", error);\n if (options?.reportError) {\n const message = \"Unable to refresh source selection. Please try again.\";\n setTxError(message);\n onError?.(message);\n }\n return false;\n } finally {\n if (activeRunId === runIdRef.current) {\n setRefreshing(false);\n }\n }\n };\n\n const onSuccess = async (explorerUrl?: string) => {\n stopwatch.stop();\n setStatus(\"success\");\n onComplete?.(explorerUrl);\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n await fetchBalance();\n notifyHistoryRefresh?.();\n };\n\n const handleTransaction = async () => {\n if (commitLockRef.current) return;\n commitLockRef.current = true;\n const currentRunId = ++runIdRef.current;\n let didEnterExecutingState = false;\n // Declared here (outside try/catch) so both the event handler and the catch block\n // can read/write it — prevents the catch from clobbering event-driven completions\n let completedFromEvent = false;\n const cleanupSupersededExecution = () => {\n if (!didEnterExecutingState) return;\n // Don't tear down the dialog if an event already handled success/failure —\n // resetInputs() inside onSuccess triggers invalidatePendingExecution which\n // increments runIdRef, making this branch fire spuriously.\n if (completedFromEvent) return;\n setRefreshing(false);\n setIsDialogOpen(false);\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n setStatus(\"idle\");\n };\n\n\n try {\n if (\n !inputs?.amount ||\n !inputs?.recipient ||\n !inputs?.chain ||\n !inputs?.token\n ) {\n console.error(\"Missing required inputs\");\n return;\n }\n if (!nexusSDK) {\n const message = \"Nexus SDK not initialized\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n const parsedAmount = Number(inputs.amount);\n if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) {\n const message = \"Enter a valid amount greater than 0.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n // v2: convertTokenReadableAmountToBigInt(amount, tokenSymbol, chainId)\n const amountBigInt = nexusSDK.convertTokenReadableAmountToBigInt(\n inputs.amount,\n inputs.token,\n inputs.chain,\n );\n\n if (configuredMaxAmount) {\n const configuredMaxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n configuredMaxAmount,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > configuredMaxRaw) {\n const message = `Amount exceeds maximum limit of ${configuredMaxAmount} ${inputs.token}.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n }\n\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (currentRunId !== runIdRef.current) return;\n if (!maxForCurrentSelection) {\n const message = `Unable to determine max ${operationName} amount for selected sources. Please try again.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n const maxForSelectionRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxForCurrentSelection,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > maxForSelectionRaw) {\n const message = `Selected sources can provide up to ${maxForCurrentSelection} ${inputs.token}. Reduce amount or enable more sources.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n setStatus(\"executing\");\n didEnterExecutingState = true;\n setTxError(null);\n onStart?.();\n setLastExplorerUrl(\"\");\n setAppliedSourceSelectionKey(sourceSelectionKey);\n\n // Terminal step types — when state:\"completed\" fires on these, the operation is done\n const TERMINAL_STEP_TYPES = new Set([\n \"bridge_fill\", // bridge & transfer final fill\n \"destination_swap\", // swap final step\n ]);\n\n // v2 onEvent uses typed discriminated union: { type, ... }\n const onEvent = (event: TransactionFlowEvent) => {\n if (currentRunId !== runIdRef.current) return;\n\n if (event.type === \"plan_preview\") {\n // Seed UI with the step list from the plan\n type StepShape = { typeID?: string; type?: string; [key: string]: unknown };\n const steps = ((event as { type: string; plan: { steps: StepShape[] } }).plan?.steps ?? []) as StepShape[];\n onStepsList(steps);\n }\n\n if (event.type === \"plan_progress\") {\n const progressEvent = event as {\n type: string;\n stepType: string;\n state: string;\n step: { typeID?: string; type?: string; [key: string]: unknown };\n error?: string;\n };\n\n // Always mark step as complete/updated in UI\n onStepComplete(progressEvent.step);\n\n const isTerminal = TERMINAL_STEP_TYPES.has(progressEvent.stepType);\n\n if (progressEvent.state === \"failed\") {\n // Any step failure → abort\n if (!completedFromEvent) {\n completedFromEvent = true;\n const errorMessage = progressEvent.error ?? \"Transaction failed\";\n stopwatch.stop();\n setTxError(errorMessage);\n onError?.(errorMessage);\n setStatus(\"error\");\n }\n return;\n }\n\n if (isTerminal && progressEvent.state === \"completed\") {\n // Terminal step completed → success\n if (!completedFromEvent) {\n completedFromEvent = true;\n stopwatch.stop();\n // explorerUrl is on the event itself, not the step object\n const explorerUrl = (event as { explorerUrl?: string }).explorerUrl;\n if (explorerUrl) setLastExplorerUrl(explorerUrl);\n void onSuccess(explorerUrl);\n }\n }\n }\n\n if (event.type === \"status\") {\n const statusEvent = event as { type: string; status: string };\n if (statusEvent.status === \"completed\" && !completedFromEvent) {\n completedFromEvent = true;\n stopwatch.stop();\n void onSuccess(undefined);\n }\n }\n };\n\n const transactionResult = await executeTransaction({\n token: inputs.token,\n amount: amountBigInt,\n toChainId: inputs.chain,\n recipient: inputs.recipient,\n sources: sourceChainsForSdk,\n onEvent,\n });\n\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution(); // no-op when completedFromEvent=true\n if (!completedFromEvent) return; // only bail if not already completed\n // else fall through — still want to capture explorerUrl from the result\n }\n if (!transactionResult) {\n if (!completedFromEvent) {\n throw new Error(\"Transaction rejected by user\");\n }\n // Already handled via events\n return;\n }\n\n // SDK promise resolved — use result for explorerUrl if event-driven success didn't set it\n if (!completedFromEvent) {\n // Fallback: SDK resolved but we never got a terminal event (e.g. single-step flows)\n setLastExplorerUrl(transactionResult.explorerUrl ?? \"\");\n await onSuccess(transactionResult.explorerUrl);\n } else {\n // Event-driven success already ran — capture the explorerUrl from the resolved result\n if (transactionResult.explorerUrl) {\n setLastExplorerUrl(transactionResult.explorerUrl);\n }\n }\n\n } catch (error) {\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution();\n return;\n }\n // If event-driven success/failure already handled this transaction, ignore SDK-level errors\n // (the SDK may throw or return oddly after a successful fill event)\n if (completedFromEvent) return;\n const { message, code, context, details } = handleNexusError(error);\n console.error(`Fast ${operationName} transaction failed:`, {\n code,\n message,\n context,\n details,\n });\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n setTxError(message);\n onError?.(message);\n setIsDialogOpen(false);\n setSelectedSourceChains(null);\n setRefreshing(false);\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n void fetchBalance();\n setStatus(\"error\");\n } finally {\n commitLockRef.current = false;\n }\n };\n\n const reset = () => {\n runIdRef.current += 1;\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setStatus(\"idle\");\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n };\n\n const startTransaction = () => {\n if (!intent.current) return;\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (sourceSelection.isBelowRequired && inputs?.token) {\n const message = `Selected sources are not enough. Add ${sourceSelection.missingToProceed} ${inputs.token} more to make this transaction.`;\n setTxError(message);\n onError?.(message);\n return;\n }\n void (async () => {\n const refreshed = await refreshIntent({ reportError: true });\n if (!refreshed || !intent.current) return;\n intent.current.allow();\n setIsDialogOpen(true);\n // Start the stopwatch AFTER the dialog opens so the isDialogOpen effect\n // does not immediately reset it (the effect only resets when dialog is closed)\n stopwatch.reset();\n stopwatch.start();\n setTxError(null);\n })();\n };\n\n const commitAmount = async () => {\n if (intent.current || loading || txError || !areInputsValid) return;\n await handleTransaction();\n };\n\n const invalidatePendingExecution = useCallback(() => {\n runIdRef.current += 1;\n if (intent.current) {\n intent.current.deny();\n intent.current = null;\n }\n setRefreshing(false);\n setAppliedSourceSelectionKey(\"ALL\");\n }, [intent, setAppliedSourceSelectionKey, setRefreshing]);\n\n return {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n };\n}\n", "type": "registry:component", "target": "components/common/hooks/useTransactionExecution.ts" }, { "path": "registry/nexus-elements/common/hooks/useTransactionFlow.ts", - "content": "import {\n type BridgeStepType,\n type NexusNetwork,\n NexusSDK,\n type OnAllowanceHookData,\n type OnIntentHookData,\n parseUnits,\n type UserAsset,\n} from \"@avail-project/nexus-core\";\nimport {\n useEffect,\n useMemo,\n useCallback,\n useRef,\n useState,\n useReducer,\n type RefObject,\n} from \"react\";\nimport { type Address, isAddress } from \"viem\";\nimport { useNexusError } from \"./useNexusError\";\nimport { useTransactionExecution } from \"./useTransactionExecution\";\nimport { usePolling } from \"./usePolling\";\nimport { useStopwatch } from \"./useStopwatch\";\nimport { useDebouncedCallback } from \"./useDebouncedCallback\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport { useTransactionSteps } from \"../tx/useTransactionSteps\";\nimport {\n type SourceCoverageState,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n type TransactionFlowPrefill,\n type TransactionFlowType,\n} from \"../types/transaction-flow\";\nimport {\n MAX_AMOUNT_DEBOUNCE_MS,\n buildInitialInputs,\n clampAmountToMax,\n formatAmountForDisplay,\n getCoverageDecimals,\n normalizeMaxAmount,\n} from \"../utils/transaction-flow\";\n\ninterface BaseTransactionFlowProps {\n type: TransactionFlowType;\n network: NexusNetwork;\n nexusSDK: NexusSDK | null;\n intent: RefObject;\n allowance: RefObject;\n bridgableBalance: UserAsset[] | null;\n prefill?: TransactionFlowPrefill;\n onComplete?: (explorerUrl?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n maxAmount?: string | number;\n isSourceMenuOpen?: boolean;\n notifyHistoryRefresh?: () => void;\n executeTransaction: TransactionFlowExecutor;\n}\n\nexport interface UseTransactionFlowProps extends BaseTransactionFlowProps {\n connectedAddress?: Address;\n}\n\ntype State = {\n inputs: TransactionFlowInputs;\n status: TransactionStatus;\n};\n\ntype Action =\n | { type: \"setInputs\"; payload: Partial }\n | { type: \"resetInputs\" }\n | { type: \"setStatus\"; payload: TransactionStatus };\n\nexport function useTransactionFlow(props: UseTransactionFlowProps) {\n const {\n type,\n network,\n nexusSDK,\n intent,\n bridgableBalance,\n prefill,\n onComplete,\n onStart,\n onError,\n fetchBalance,\n allowance,\n maxAmount,\n isSourceMenuOpen = false,\n notifyHistoryRefresh,\n executeTransaction,\n } = props;\n\n const connectedAddress = props.connectedAddress;\n const operationName = type === \"bridge\" ? \"bridge\" : \"transfer\";\n const handleNexusError = useNexusError();\n const initialState: State = {\n inputs: buildInitialInputs({ type, network, connectedAddress, prefill }),\n status: \"idle\",\n };\n\n function reducer(state: State, action: Action): State {\n switch (action.type) {\n case \"setInputs\":\n return { ...state, inputs: { ...state.inputs, ...action.payload } };\n case \"resetInputs\":\n return {\n ...state,\n inputs: buildInitialInputs({\n type,\n network,\n connectedAddress,\n prefill,\n }),\n };\n case \"setStatus\":\n return { ...state, status: action.payload };\n default:\n return state;\n }\n }\n\n const [state, dispatch] = useReducer(reducer, initialState);\n const inputs = state.inputs;\n const setInputs = (\n next: TransactionFlowInputs | Partial,\n ) => {\n dispatch({\n type: \"setInputs\",\n payload: next as Partial,\n });\n };\n\n const loading = state.status === \"executing\";\n const [refreshing, setRefreshing] = useState(false);\n const [isDialogOpen, setIsDialogOpen] = useState(false);\n const [txError, setTxError] = useState(null);\n const [lastExplorerUrl, setLastExplorerUrl] = useState(\"\");\n const previousConnectedAddressRef = useRef
(\n connectedAddress,\n );\n const maxAmountRequestIdRef = useRef(0);\n const [selectedSourceChains, setSelectedSourceChains] = useState<\n number[] | null\n >(null);\n const [selectedSourcesMaxAmount, setSelectedSourcesMaxAmount] = useState<\n string | null\n >(null);\n const [appliedSourceSelectionKey, setAppliedSourceSelectionKey] =\n useState(\"ALL\");\n const {\n steps,\n onStepsList,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n const configuredMaxAmount = useMemo(\n () => normalizeMaxAmount(maxAmount),\n [maxAmount],\n );\n\n const areInputsValid = useMemo(() => {\n const hasToken = inputs?.token !== undefined && inputs?.token !== null;\n const hasChain = inputs?.chain !== undefined && inputs?.chain !== null;\n const hasAmount = Boolean(inputs?.amount) && Number(inputs?.amount) > 0;\n const hasValidRecipient =\n Boolean(inputs?.recipient) && isAddress(inputs.recipient as string);\n return hasToken && hasChain && hasAmount && hasValidRecipient;\n }, [inputs]);\n\n const filteredBridgableBalance = useMemo(() => {\n return bridgableBalance?.find((bal) =>\n inputs?.token === \"USDM\"\n ? bal?.symbol === \"USDC\"\n : bal?.symbol === inputs?.token,\n );\n }, [bridgableBalance, inputs?.token]);\n\n const availableSources = useMemo(() => {\n const breakdown = filteredBridgableBalance?.breakdown ?? [];\n const destinationChainId = inputs?.chain;\n const nonZero = breakdown.filter((source) => {\n if (Number.parseFloat(source.balance ?? \"0\") <= 0) return false;\n if (typeof destinationChainId === \"number\") {\n return source.chain.id !== destinationChainId;\n }\n return true;\n });\n const decimals = filteredBridgableBalance?.decimals;\n if (!nexusSDK || typeof decimals !== \"number\") {\n return nonZero.sort(\n (a, b) => Number.parseFloat(b.balance) - Number.parseFloat(a.balance),\n );\n }\n return nonZero.sort((a, b) => {\n try {\n const aRaw = parseUnits(a.balance ?? \"0\", decimals);\n const bRaw = parseUnits(b.balance ?? \"0\", decimals);\n if (aRaw === bRaw) return 0;\n return aRaw > bRaw ? -1 : 1;\n } catch {\n return Number.parseFloat(b.balance) - Number.parseFloat(a.balance);\n }\n });\n }, [\n inputs?.chain,\n filteredBridgableBalance?.breakdown,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n ]);\n\n const allAvailableSourceChainIds = useMemo(\n () => availableSources.map((source) => source.chain.id),\n [availableSources],\n );\n\n const effectiveSelectedSourceChains = useMemo(() => {\n if (selectedSourceChains && selectedSourceChains.length > 0) {\n const availableSet = new Set(allAvailableSourceChainIds);\n const filteredSelection = selectedSourceChains.filter((id) =>\n availableSet.has(id),\n );\n if (filteredSelection.length > 0) {\n return filteredSelection;\n }\n }\n return allAvailableSourceChainIds;\n }, [selectedSourceChains, allAvailableSourceChainIds]);\n\n const sourceChainsForSdk =\n effectiveSelectedSourceChains.length > 0\n ? effectiveSelectedSourceChains\n : undefined;\n\n const sourceSelectionKey = useMemo(() => {\n if (allAvailableSourceChainIds.length === 0) return \"NONE\";\n if (!selectedSourceChains || selectedSourceChains.length === 0) {\n return \"ALL\";\n }\n return [...effectiveSelectedSourceChains].sort((a, b) => a - b).join(\"|\");\n }, [\n allAvailableSourceChainIds.length,\n effectiveSelectedSourceChains,\n selectedSourceChains,\n ]);\n const hasPendingSourceSelectionChanges =\n sourceSelectionKey !== appliedSourceSelectionKey;\n const intentSourceSpendAmount = intent.current?.intent?.sourcesTotal;\n\n const getMaxForCurrentSelection = useCallback(async () => {\n if (!nexusSDK || !inputs?.token || !inputs?.chain) return undefined;\n const maxBalAvailable = await nexusSDK.calculateMaxForBridge({\n token: inputs.token,\n toChainId: inputs.chain,\n recipient: inputs.recipient,\n sourceChains: sourceChainsForSdk,\n });\n if (!maxBalAvailable?.amount) return \"0\";\n return clampAmountToMax({\n amount: maxBalAvailable.amount,\n maxAmount: configuredMaxAmount,\n nexusSDK,\n token: inputs.token,\n chainId: inputs.chain,\n });\n }, [\n configuredMaxAmount,\n inputs?.chain,\n inputs?.recipient,\n inputs?.token,\n nexusSDK,\n sourceChainsForSdk,\n ]);\n\n const toggleSourceChain = useCallback(\n (chainId: number) => {\n setSelectedSourceChains((prev) => {\n if (allAvailableSourceChainIds.length === 0) return prev;\n const current =\n prev && prev.length > 0 ? prev : allAvailableSourceChainIds;\n const next = current.includes(chainId)\n ? current.filter((id) => id !== chainId)\n : [...current, chainId];\n if (next.length === 0) {\n return current;\n }\n const isAllSelected =\n next.length === allAvailableSourceChainIds.length &&\n allAvailableSourceChainIds.every((id) => next.includes(id));\n return isAllSelected ? null : next;\n });\n },\n [allAvailableSourceChainIds],\n );\n\n const sourceSelection = useMemo(() => {\n const amount =\n intentSourceSpendAmount?.trim() ?? inputs?.amount?.trim() ?? \"\";\n const decimals = getCoverageDecimals({\n type,\n token: inputs?.token,\n chainId: inputs?.chain,\n fallback: filteredBridgableBalance?.decimals,\n });\n const selectedChainSet = new Set(effectiveSelectedSourceChains);\n const selectedTotalRaw =\n !nexusSDK || typeof decimals !== \"number\"\n ? BigInt(0)\n : availableSources.reduce((sum, source) => {\n if (!selectedChainSet.has(source.chain.id)) return sum;\n try {\n return sum + parseUnits(source.balance ?? \"0\", decimals);\n } catch {\n return sum;\n }\n }, BigInt(0));\n const selectedTotal =\n !nexusSDK || typeof decimals !== \"number\"\n ? \"0\"\n : formatAmountForDisplay(selectedTotalRaw, decimals, nexusSDK);\n const baseSelection = {\n selectedTotal,\n requiredTotal: amount || \"0\",\n requiredSafetyTotal: amount || \"0\",\n missingToProceed: \"0\",\n missingToSafety: \"0\",\n coverageState: \"healthy\" as SourceCoverageState,\n coverageToSafetyPercent: 100,\n isBelowRequired: false,\n isBelowSafetyBuffer: false,\n };\n\n if (!nexusSDK || !inputs?.token || !inputs?.chain || !amount) {\n return baseSelection;\n }\n\n try {\n const requiredRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n inputs.token,\n inputs.chain,\n );\n if (requiredRaw <= BigInt(0)) {\n return baseSelection;\n }\n\n const missingToProceedRaw =\n selectedTotalRaw >= requiredRaw\n ? BigInt(0)\n : requiredRaw - selectedTotalRaw;\n const missingToSafetyRaw = missingToProceedRaw;\n\n const coverageState: SourceCoverageState =\n selectedTotalRaw < requiredRaw ? \"error\" : \"healthy\";\n\n const coverageBasisPoints =\n requiredRaw === BigInt(0)\n ? 10_000\n : selectedTotalRaw >= requiredRaw\n ? 10_000\n : Number((selectedTotalRaw * BigInt(10_000)) / requiredRaw);\n\n return {\n selectedTotal,\n requiredTotal: amount,\n requiredSafetyTotal: amount,\n missingToProceed: formatAmountForDisplay(\n missingToProceedRaw,\n decimals,\n nexusSDK,\n ),\n missingToSafety: formatAmountForDisplay(\n missingToSafetyRaw,\n decimals,\n nexusSDK,\n ),\n coverageState,\n coverageToSafetyPercent: coverageBasisPoints / 100,\n isBelowRequired: coverageState === \"error\",\n isBelowSafetyBuffer: coverageState === \"error\",\n };\n } catch {\n return baseSelection;\n }\n }, [\n type,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n inputs?.chain,\n inputs?.amount,\n inputs?.token,\n intentSourceSpendAmount,\n availableSources,\n effectiveSelectedSourceChains,\n ]);\n\n const stopwatch = useStopwatch({ intervalMs: 100 });\n const setStatus = useCallback(\n (status: TransactionStatus) =>\n dispatch({ type: \"setStatus\", payload: status }),\n [],\n );\n\n const resetInputs = useCallback(() => {\n dispatch({ type: \"resetInputs\" });\n }, []);\n\n const {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n } = useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n });\n\n usePolling(\n Boolean(intent.current) &&\n !isDialogOpen &&\n !isSourceMenuOpen &&\n !hasPendingSourceSelectionChanges,\n async () => {\n await refreshIntent();\n },\n 15000,\n );\n\n const debouncedRefreshMaxForSelection = useDebouncedCallback(\n async (requestId: number) => {\n try {\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (requestId !== maxAmountRequestIdRef.current) return;\n setSelectedSourcesMaxAmount(maxForCurrentSelection ?? \"0\");\n } catch (error) {\n if (requestId !== maxAmountRequestIdRef.current) return;\n console.error(\"Unable to calculate max for selected sources:\", error);\n setSelectedSourcesMaxAmount(\"0\");\n }\n },\n MAX_AMOUNT_DEBOUNCE_MS,\n );\n\n useEffect(() => {\n debouncedRefreshMaxForSelection.cancel();\n if (!nexusSDK || !inputs?.token || !inputs?.chain) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(null);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(\"0\");\n return;\n }\n const requestId = ++maxAmountRequestIdRef.current;\n debouncedRefreshMaxForSelection(requestId);\n }, [\n allAvailableSourceChainIds.length,\n configuredMaxAmount,\n debouncedRefreshMaxForSelection,\n inputs?.recipient,\n sourceSelectionKey,\n inputs?.chain,\n inputs?.token,\n nexusSDK,\n ]);\n\n useEffect(() => {\n if (type !== \"bridge\" || !connectedAddress) return;\n const previousConnectedAddress = previousConnectedAddressRef.current;\n if (!previousConnectedAddress) {\n previousConnectedAddressRef.current = connectedAddress;\n return;\n }\n if (connectedAddress === previousConnectedAddress) return;\n previousConnectedAddressRef.current = connectedAddress;\n if (prefill?.recipient) return;\n if (!inputs?.recipient || inputs.recipient === previousConnectedAddress) {\n dispatch({ type: \"setInputs\", payload: { recipient: connectedAddress } });\n }\n }, [type, connectedAddress, inputs?.recipient, prefill?.recipient]);\n\n useEffect(() => {\n invalidatePendingExecution();\n }, [inputs, invalidatePendingExecution]);\n\n useEffect(() => {\n setSelectedSourceChains(null);\n }, [inputs?.token]);\n\n useEffect(() => {\n if (isDialogOpen) return;\n stopwatch.stop();\n stopwatch.reset();\n if (state.status === \"success\" || state.status === \"error\") {\n resetSteps();\n setLastExplorerUrl(\"\");\n setStatus(\"idle\");\n }\n }, [isDialogOpen, resetSteps, setStatus, state.status, stopwatch]);\n\n useEffect(() => {\n if (txError) {\n setTxError(null);\n }\n }, [inputs, txError]);\n\n return {\n inputs,\n setInputs,\n timer: stopwatch.seconds,\n setIsDialogOpen,\n setTxError,\n loading,\n refreshing,\n isDialogOpen,\n txError,\n handleTransaction,\n reset,\n filteredBridgableBalance,\n startTransaction,\n commitAmount,\n lastExplorerUrl,\n steps,\n status: state.status,\n availableSources,\n selectedSourceChains: effectiveSelectedSourceChains,\n toggleSourceChain,\n isSourceSelectionInsufficient: sourceSelection.isBelowRequired,\n isSourceSelectionBelowSafetyBuffer: sourceSelection.isBelowSafetyBuffer,\n isSourceSelectionReadyForAccept:\n sourceSelection.coverageState === \"healthy\",\n sourceCoverageState: sourceSelection.coverageState,\n sourceCoveragePercent: sourceSelection.coverageToSafetyPercent,\n missingToProceed: sourceSelection.missingToProceed,\n missingToSafety: sourceSelection.missingToSafety,\n selectedTotal: sourceSelection.selectedTotal,\n requiredTotal: sourceSelection.requiredTotal,\n requiredSafetyTotal: sourceSelection.requiredSafetyTotal,\n maxAvailableAmount: selectedSourcesMaxAmount ?? undefined,\n isInputsValid: areInputsValid,\n };\n}\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type {\n NexusNetwork,\n OnAllowanceHookData,\n OnIntentHookData,\n TokenBalance,\n ChainBalance,\n} from \"@avail-project/nexus-sdk-v2\";\nimport { parseUnits } from \"viem\";\nimport {\n useEffect,\n useMemo,\n useCallback,\n useRef,\n useState,\n useReducer,\n type RefObject,\n} from \"react\";\nimport { type Address, isAddress } from \"viem\";\nimport { useNexusError } from \"./useNexusError\";\nimport { useTransactionExecution } from \"./useTransactionExecution\";\nimport { usePolling } from \"./usePolling\";\nimport { useStopwatch } from \"./useStopwatch\";\nimport { useDebouncedCallback } from \"./useDebouncedCallback\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport { useTransactionSteps } from \"../tx/useTransactionSteps\";\nimport {\n type SourceCoverageState,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n type TransactionFlowPrefill,\n type TransactionFlowType,\n} from \"../types/transaction-flow\";\nimport {\n MAX_AMOUNT_DEBOUNCE_MS,\n buildInitialInputs,\n clampAmountToMax,\n formatAmountForDisplay,\n getCoverageDecimals,\n normalizeMaxAmount,\n} from \"../utils/transaction-flow\";\n\ntype NexusClient = ReturnType;\n\n// v2 uses a generic step shape; minimal type to satisfy getStepKey constraint\ntype BridgePlanStep = {\n typeID?: string;\n type?: string;\n [key: string]: unknown;\n};\n\ninterface BaseTransactionFlowProps {\n type: TransactionFlowType;\n network: NexusNetwork;\n nexusSDK: NexusClient | null;\n intent: RefObject;\n allowance: RefObject;\n bridgableBalance: TokenBalance[] | null;\n prefill?: TransactionFlowPrefill;\n onComplete?: (explorerUrl?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n maxAmount?: string | number;\n isSourceMenuOpen?: boolean;\n notifyHistoryRefresh?: () => void;\n executeTransaction: TransactionFlowExecutor;\n}\n\nexport interface UseTransactionFlowProps extends BaseTransactionFlowProps {\n connectedAddress?: Address;\n}\n\ntype State = {\n inputs: TransactionFlowInputs;\n status: TransactionStatus;\n};\n\ntype Action =\n | { type: \"setInputs\"; payload: Partial }\n | { type: \"resetInputs\" }\n | { type: \"setStatus\"; payload: TransactionStatus };\n\nexport function useTransactionFlow(props: UseTransactionFlowProps) {\n const {\n type,\n network,\n nexusSDK,\n intent,\n bridgableBalance,\n prefill,\n onComplete,\n onStart,\n onError,\n fetchBalance,\n allowance,\n maxAmount,\n isSourceMenuOpen = false,\n notifyHistoryRefresh,\n executeTransaction,\n } = props;\n\n const connectedAddress = props.connectedAddress;\n const operationName = type === \"bridge\" ? \"bridge\" : \"transfer\";\n const handleNexusError = useNexusError();\n const initialState: State = {\n inputs: buildInitialInputs({ type, network, connectedAddress, prefill }),\n status: \"idle\",\n };\n\n function reducer(state: State, action: Action): State {\n switch (action.type) {\n case \"setInputs\":\n return { ...state, inputs: { ...state.inputs, ...action.payload } };\n case \"resetInputs\":\n return {\n ...state,\n inputs: buildInitialInputs({\n type,\n network,\n connectedAddress,\n prefill,\n }),\n };\n case \"setStatus\":\n return { ...state, status: action.payload };\n default:\n return state;\n }\n }\n\n const [state, dispatch] = useReducer(reducer, initialState);\n const inputs = state.inputs;\n const setInputs = (\n next: TransactionFlowInputs | Partial,\n ) => {\n dispatch({\n type: \"setInputs\",\n payload: next as Partial,\n });\n };\n\n const loading = state.status === \"executing\";\n const [refreshing, setRefreshing] = useState(false);\n const [isDialogOpen, setIsDialogOpen] = useState(false);\n const [txError, setTxError] = useState(null);\n const [lastExplorerUrl, setLastExplorerUrl] = useState(\"\");\n const previousConnectedAddressRef = useRef
(\n connectedAddress,\n );\n const maxAmountRequestIdRef = useRef(0);\n const [selectedSourceChains, setSelectedSourceChains] = useState<\n number[] | null\n >(null);\n const [selectedSourcesMaxAmount, setSelectedSourcesMaxAmount] = useState<\n string | null\n >(null);\n const [appliedSourceSelectionKey, setAppliedSourceSelectionKey] =\n useState(\"ALL\");\n const {\n steps,\n onStepsList,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n const configuredMaxAmount = useMemo(\n () => normalizeMaxAmount(maxAmount),\n [maxAmount],\n );\n\n const areInputsValid = useMemo(() => {\n const hasToken = inputs?.token !== undefined && inputs?.token !== null;\n const hasChain = inputs?.chain !== undefined && inputs?.chain !== null;\n const hasAmount = Boolean(inputs?.amount) && Number(inputs?.amount) > 0;\n const hasValidRecipient =\n Boolean(inputs?.recipient) && isAddress(inputs.recipient as string);\n return hasToken && hasChain && hasAmount && hasValidRecipient;\n }, [inputs]);\n\n const filteredBridgableBalance = useMemo(() => {\n return bridgableBalance?.find((bal) =>\n inputs?.token === \"USDM\"\n ? bal?.symbol === \"USDC\"\n : bal?.symbol === inputs?.token,\n );\n }, [bridgableBalance, inputs?.token]);\n\n const availableSources = useMemo(() => {\n // v2: chainBalances replaces breakdown\n const chainBalances = filteredBridgableBalance?.chainBalances ?? [];\n const destinationChainId = inputs?.chain;\n const nonZero = chainBalances.filter((source: ChainBalance) => {\n if (Number.parseFloat(source.balance ?? \"0\") <= 0) return false;\n if (typeof destinationChainId === \"number\") {\n return source.chain.id !== destinationChainId;\n }\n return true;\n });\n const decimals = filteredBridgableBalance?.decimals;\n if (!nexusSDK || typeof decimals !== \"number\") {\n return nonZero.sort(\n (a, b) => Number.parseFloat(b.balance) - Number.parseFloat(a.balance),\n );\n }\n return nonZero.sort((a: ChainBalance, b: ChainBalance) => {\n try {\n const aRaw = parseUnits(a.balance ?? \"0\", decimals);\n const bRaw = parseUnits(b.balance ?? \"0\", decimals);\n if (aRaw === bRaw) return 0;\n return aRaw > bRaw ? -1 : 1;\n } catch {\n return Number.parseFloat(b.balance) - Number.parseFloat(a.balance);\n }\n });\n }, [\n inputs?.chain,\n filteredBridgableBalance?.chainBalances,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n ]);\n\n const allAvailableSourceChainIds = useMemo(\n () => availableSources.map((source: ChainBalance) => source.chain.id),\n [availableSources],\n );\n\n const effectiveSelectedSourceChains = useMemo(() => {\n if (selectedSourceChains && selectedSourceChains.length > 0) {\n const availableSet = new Set(allAvailableSourceChainIds);\n const filteredSelection = selectedSourceChains.filter((id: number) =>\n availableSet.has(id),\n );\n if (filteredSelection.length > 0) {\n return filteredSelection;\n }\n }\n return allAvailableSourceChainIds;\n }, [selectedSourceChains, allAvailableSourceChainIds]);\n\n const sourceChainsForSdk =\n effectiveSelectedSourceChains.length > 0\n ? effectiveSelectedSourceChains\n : undefined;\n\n const sourceSelectionKey = useMemo(() => {\n if (allAvailableSourceChainIds.length === 0) return \"NONE\";\n if (!selectedSourceChains || selectedSourceChains.length === 0) {\n return \"ALL\";\n }\n return [...effectiveSelectedSourceChains].sort((a: number, b: number) => a - b).join(\"|\");\n }, [\n allAvailableSourceChainIds.length,\n effectiveSelectedSourceChains,\n selectedSourceChains,\n ]);\n const hasPendingSourceSelectionChanges =\n sourceSelectionKey !== appliedSourceSelectionKey;\n const intentSourceSpendAmount = intent.current?.intent?.sourcesTotal;\n\n /**\n * v2: calculateMaxForBridge is removed. Use simulateBridge to get the max amount,\n * or fall back to summing available source balances directly.\n */\n const getMaxForCurrentSelection = useCallback(async () => {\n if (!nexusSDK || !inputs?.token || !inputs?.chain) return undefined;\n\n // Sum balances from selected sources as a direct proxy for max\n const decimals = filteredBridgableBalance?.decimals;\n if (typeof decimals !== \"number\") return \"0\";\n\n const selectedSet = new Set(\n sourceChainsForSdk ?? allAvailableSourceChainIds,\n );\n const totalRaw = availableSources.reduce((sum: bigint, source: ChainBalance) => {\n if (!selectedSet.has(source.chain.id)) return sum;\n try {\n return sum + parseUnits(source.balance ?? \"0\", decimals);\n } catch {\n return sum;\n }\n }, BigInt(0));\n\n const totalReadable = formatToBigIntReadable(totalRaw, decimals);\n if (!totalReadable) return \"0\";\n\n return clampAmountToMax({\n amount: totalReadable,\n maxAmount: configuredMaxAmount,\n nexusSDK,\n token: inputs.token,\n chainId: inputs.chain,\n });\n }, [\n configuredMaxAmount,\n inputs?.chain,\n inputs?.token,\n nexusSDK,\n sourceChainsForSdk,\n allAvailableSourceChainIds,\n availableSources,\n filteredBridgableBalance?.decimals,\n ]);\n\n const toggleSourceChain = useCallback(\n (chainId: number) => {\n setSelectedSourceChains((prev) => {\n if (allAvailableSourceChainIds.length === 0) return prev;\n const current =\n prev && prev.length > 0 ? prev : allAvailableSourceChainIds;\n const next = current.includes(chainId)\n ? current.filter((id: number) => id !== chainId)\n : [...current, chainId];\n if (next.length === 0) {\n return current;\n }\n const isAllSelected =\n next.length === allAvailableSourceChainIds.length &&\n allAvailableSourceChainIds.every((id) => next.includes(id));\n return isAllSelected ? null : next;\n });\n },\n [allAvailableSourceChainIds],\n );\n\n const sourceSelection = useMemo(() => {\n const amount =\n intentSourceSpendAmount?.trim() ?? inputs?.amount?.trim() ?? \"\";\n const decimals = getCoverageDecimals({\n type,\n token: inputs?.token,\n chainId: inputs?.chain,\n fallback: filteredBridgableBalance?.decimals,\n });\n const selectedChainSet = new Set(effectiveSelectedSourceChains);\n const selectedTotalRaw =\n !nexusSDK || typeof decimals !== \"number\"\n ? BigInt(0)\n : availableSources.reduce((sum: bigint, source: ChainBalance) => {\n if (!selectedChainSet.has(source.chain.id)) return sum;\n try {\n return sum + parseUnits(source.balance ?? \"0\", decimals);\n } catch {\n return sum;\n }\n }, BigInt(0));\n const selectedTotal =\n !nexusSDK || typeof decimals !== \"number\"\n ? \"0\"\n : formatAmountForDisplay(selectedTotalRaw, decimals, nexusSDK);\n const baseSelection = {\n selectedTotal,\n requiredTotal: amount || \"0\",\n requiredSafetyTotal: amount || \"0\",\n missingToProceed: \"0\",\n missingToSafety: \"0\",\n coverageState: \"healthy\" as SourceCoverageState,\n coverageToSafetyPercent: 100,\n isBelowRequired: false,\n isBelowSafetyBuffer: false,\n };\n\n if (!nexusSDK || !inputs?.token || !inputs?.chain || !amount) {\n return baseSelection;\n }\n\n try {\n // v2: convertTokenReadableAmountToBigInt(amount, tokenSymbol, chainId)\n const requiredRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n inputs.token,\n inputs.chain,\n );\n if (requiredRaw <= BigInt(0)) {\n return baseSelection;\n }\n\n const missingToProceedRaw =\n selectedTotalRaw >= requiredRaw\n ? BigInt(0)\n : requiredRaw - selectedTotalRaw;\n const missingToSafetyRaw = missingToProceedRaw;\n\n const coverageState: SourceCoverageState =\n selectedTotalRaw < requiredRaw ? \"error\" : \"healthy\";\n\n const coverageBasisPoints =\n requiredRaw === BigInt(0)\n ? 10_000\n : selectedTotalRaw >= requiredRaw\n ? 10_000\n : Number((selectedTotalRaw * BigInt(10_000)) / requiredRaw);\n\n return {\n selectedTotal,\n requiredTotal: amount,\n requiredSafetyTotal: amount,\n missingToProceed: formatAmountForDisplay(\n missingToProceedRaw,\n decimals,\n nexusSDK,\n ),\n missingToSafety: formatAmountForDisplay(\n missingToSafetyRaw,\n decimals,\n nexusSDK,\n ),\n coverageState,\n coverageToSafetyPercent: coverageBasisPoints / 100,\n isBelowRequired: coverageState === \"error\",\n isBelowSafetyBuffer: coverageState === \"error\",\n };\n } catch {\n return baseSelection;\n }\n }, [\n type,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n inputs?.chain,\n inputs?.amount,\n inputs?.token,\n intentSourceSpendAmount,\n availableSources,\n effectiveSelectedSourceChains,\n ]);\n\n const stopwatch = useStopwatch({ intervalMs: 100 });\n const setStatus = useCallback(\n (status: TransactionStatus) =>\n dispatch({ type: \"setStatus\", payload: status }),\n [],\n );\n\n const resetInputs = useCallback(() => {\n dispatch({ type: \"resetInputs\" });\n }, []);\n\n const {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n } = useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n });\n\n usePolling(\n Boolean(intent.current) &&\n !isDialogOpen &&\n !isSourceMenuOpen &&\n !hasPendingSourceSelectionChanges,\n async () => {\n await refreshIntent();\n },\n 15000,\n );\n\n const debouncedRefreshMaxForSelection = useDebouncedCallback(\n async (requestId: number) => {\n try {\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (requestId !== maxAmountRequestIdRef.current) return;\n setSelectedSourcesMaxAmount(maxForCurrentSelection ?? \"0\");\n } catch (error) {\n if (requestId !== maxAmountRequestIdRef.current) return;\n console.error(\"Unable to calculate max for selected sources:\", error);\n setSelectedSourcesMaxAmount(\"0\");\n }\n },\n MAX_AMOUNT_DEBOUNCE_MS,\n );\n\n useEffect(() => {\n debouncedRefreshMaxForSelection.cancel();\n if (!nexusSDK || !inputs?.token || !inputs?.chain) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(null);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(\"0\");\n return;\n }\n const requestId = ++maxAmountRequestIdRef.current;\n debouncedRefreshMaxForSelection(requestId);\n }, [\n allAvailableSourceChainIds.length,\n configuredMaxAmount,\n debouncedRefreshMaxForSelection,\n inputs?.recipient,\n sourceSelectionKey,\n inputs?.chain,\n inputs?.token,\n nexusSDK,\n ]);\n\n useEffect(() => {\n if (type !== \"bridge\" || !connectedAddress) return;\n const previousConnectedAddress = previousConnectedAddressRef.current;\n if (!previousConnectedAddress) {\n previousConnectedAddressRef.current = connectedAddress;\n return;\n }\n if (connectedAddress === previousConnectedAddress) return;\n previousConnectedAddressRef.current = connectedAddress;\n if (prefill?.recipient) return;\n if (!inputs?.recipient || inputs.recipient === previousConnectedAddress) {\n dispatch({ type: \"setInputs\", payload: { recipient: connectedAddress } });\n }\n }, [type, connectedAddress, inputs?.recipient, prefill?.recipient]);\n\n useEffect(() => {\n invalidatePendingExecution();\n }, [inputs, invalidatePendingExecution]);\n\n useEffect(() => {\n setSelectedSourceChains(null);\n }, [inputs?.token]);\n\n // Safety-net: stop the stopwatch as soon as status reaches a terminal state.\n // This ensures the timer freezes even if the onEvent closure's stopwatch.stop()\n // didn't fire (e.g. stale closure reference or SDK promise resolved oddly).\n useEffect(() => {\n if (state.status === \"success\" || state.status === \"error\") {\n stopwatch.stop();\n }\n }, [state.status, stopwatch]);\n\n useEffect(() => {\n if (isDialogOpen) return;\n stopwatch.stop();\n stopwatch.reset();\n if (state.status === \"success\" || state.status === \"error\") {\n resetSteps();\n setLastExplorerUrl(\"\");\n setStatus(\"idle\");\n }\n }, [isDialogOpen, resetSteps, setStatus, state.status, stopwatch]);\n\n useEffect(() => {\n if (txError) {\n setTxError(null);\n }\n }, [inputs, txError]);\n\n\n return {\n inputs,\n setInputs,\n timer: stopwatch.seconds,\n setIsDialogOpen,\n setTxError,\n loading,\n refreshing,\n isDialogOpen,\n txError,\n handleTransaction,\n reset,\n filteredBridgableBalance,\n startTransaction,\n commitAmount,\n lastExplorerUrl,\n steps,\n status: state.status,\n availableSources,\n selectedSourceChains: effectiveSelectedSourceChains,\n toggleSourceChain,\n isSourceSelectionInsufficient: sourceSelection.isBelowRequired,\n isSourceSelectionBelowSafetyBuffer: sourceSelection.isBelowSafetyBuffer,\n isSourceSelectionReadyForAccept:\n sourceSelection.coverageState === \"healthy\",\n sourceCoverageState: sourceSelection.coverageState,\n sourceCoveragePercent: sourceSelection.coverageToSafetyPercent,\n missingToProceed: sourceSelection.missingToProceed,\n missingToSafety: sourceSelection.missingToSafety,\n selectedTotal: sourceSelection.selectedTotal,\n requiredTotal: sourceSelection.requiredTotal,\n requiredSafetyTotal: sourceSelection.requiredSafetyTotal,\n maxAvailableAmount: selectedSourcesMaxAmount ?? undefined,\n isInputsValid: areInputsValid,\n };\n}\n\n/** Helper: format a bigint rawAmount with decimals into a readable decimal string. */\nfunction formatToBigIntReadable(raw: bigint, decimals: number): string {\n if (raw <= BigInt(0)) return \"0\";\n const divisor = BigInt(10 ** decimals);\n const whole = raw / divisor;\n const fraction = raw % divisor;\n if (fraction === BigInt(0)) return whole.toString();\n const fractionStr = fraction.toString().padStart(decimals, \"0\").replace(/0+$/, \"\");\n return `${whole}.${fractionStr}`;\n}\n", "type": "registry:component", "target": "components/common/hooks/useTransactionFlow.ts" }, @@ -159,7 +159,7 @@ }, { "path": "registry/nexus-elements/common/tx/steps.ts", - "content": "import type { SwapStepType } from \"@avail-project/nexus-core\";\nimport type { GenericStep } from \"./types\";\nimport { getStepKey } from \"./types\";\n\n/**\n * Predefined expected steps for swaps to seed UI before events arrive.\n * Kept here to avoid duplication across exact-in and exact-out hooks.\n */\nexport const SWAP_EXPECTED_STEPS: SwapStepType[] = [\n { type: \"SWAP_START\", typeID: \"SWAP_START\" } as SwapStepType,\n { type: \"DETERMINING_SWAP\", typeID: \"DETERMINING_SWAP\" } as SwapStepType,\n {\n type: \"CREATE_PERMIT_FOR_SOURCE_SWAP\",\n typeID:\n \"CREATE_PERMIT_FOR_SOURCE_SWAP\" as unknown as SwapStepType[\"typeID\"],\n } as SwapStepType,\n {\n type: \"SOURCE_SWAP_BATCH_TX\",\n typeID: \"SOURCE_SWAP_BATCH_TX\",\n } as SwapStepType,\n {\n type: \"SOURCE_SWAP_HASH\",\n typeID: \"SOURCE_SWAP_HASH\" as unknown as SwapStepType[\"typeID\"],\n } as SwapStepType,\n { type: \"RFF_ID\", typeID: \"RFF_ID\" } as SwapStepType,\n {\n type: \"DESTINATION_SWAP_BATCH_TX\",\n typeID: \"DESTINATION_SWAP_BATCH_TX\",\n } as SwapStepType,\n {\n type: \"DESTINATION_SWAP_HASH\",\n typeID: \"DESTINATION_SWAP_HASH\" as unknown as SwapStepType[\"typeID\"],\n } as SwapStepType,\n { type: \"SWAP_COMPLETE\", typeID: \"SWAP_COMPLETE\" } as SwapStepType,\n];\n\nexport function seedSteps(expected: T[]): Array> {\n return expected.map((st, index) => ({\n id: index,\n completed: false,\n step: st,\n }));\n}\n\nexport function computeAllCompleted(steps: Array>): boolean {\n return steps.length > 0 && steps.every((s) => s.completed);\n}\n\n/**\n * Replace the current list of steps with a new list, preserving completion\n * for any steps that were already marked completed (matched by key).\n */\nexport function mergeStepsList(\n prev: Array>,\n list: T[]\n): Array> {\n const completedKeys = new Set();\n for (const prevStep of prev) {\n if (prevStep.completed) {\n completedKeys.add(getStepKey(prevStep.step));\n }\n }\n const next: Array> = [];\n for (let index = 0; index < list.length; index++) {\n const step = list[index];\n const key = getStepKey(step);\n next.push({\n id: index,\n completed: completedKeys.has(key),\n step,\n });\n }\n return next;\n}\n\n/**\n * Mark a step complete in-place; if the step doesn't yet exist, append it.\n */\nexport function mergeStepComplete(\n prev: Array>,\n step: T\n): Array> {\n const key = getStepKey(step);\n const updated: Array> = [];\n let found = false;\n for (const s of prev) {\n if (getStepKey(s.step) === key) {\n updated.push({ ...s, completed: true, step: { ...s.step, ...step } });\n found = true;\n } else {\n updated.push(s);\n }\n }\n if (!found) {\n updated.push({\n id: updated.length,\n completed: true,\n step,\n });\n }\n return updated;\n}\n", + "content": "// v2: SwapStepType is no longer exported from the SDK — use a local step shape\n// that matches v2 SwapPlanStep discriminator pattern\nexport type SwapStepType = {\n typeID?: string;\n type?: string;\n [key: string]: unknown;\n};\n\nimport type { GenericStep } from \"./types\";\nimport { getStepKey } from \"./types\";\n\n/**\n * Predefined expected steps for swaps to seed UI before events arrive.\n * Uses v2 stepType names that match SwapPlanProgressEvent.stepType discriminators.\n */\nexport const SWAP_EXPECTED_STEPS: SwapStepType[] = [\n { type: \"source_swap\", typeID: \"source_swap\" },\n { type: \"eoa_to_ephemeral_transfer\", typeID: \"eoa_to_ephemeral_transfer\" },\n { type: \"bridge_deposit\", typeID: \"bridge_deposit\" },\n { type: \"bridge_intent_submission\", typeID: \"bridge_intent_submission\" },\n { type: \"bridge_fill\", typeID: \"bridge_fill\" },\n { type: \"destination_swap\", typeID: \"destination_swap\" },\n];\n\nexport function seedSteps(expected: T[]): Array> {\n return expected.map((st, index) => ({\n id: index,\n completed: false,\n step: st,\n }));\n}\n\nexport function computeAllCompleted(steps: Array>): boolean {\n return steps.length > 0 && steps.every((s) => s.completed);\n}\n\n/**\n * Replace the current list of steps with a new list, preserving completion\n * for any steps that were already marked completed (matched by key).\n */\nexport function mergeStepsList(\n prev: Array>,\n list: T[]\n): Array> {\n const completedKeys = new Set();\n for (const prevStep of prev) {\n if (prevStep.completed) {\n completedKeys.add(getStepKey(prevStep.step));\n }\n }\n const next: Array> = [];\n for (let index = 0; index < list.length; index++) {\n const step = list[index];\n const key = getStepKey(step);\n next.push({\n id: index,\n completed: completedKeys.has(key),\n step,\n });\n }\n return next;\n}\n\n/**\n * Mark a step complete in-place; if the step doesn't yet exist, append it.\n */\nexport function mergeStepComplete(\n prev: Array>,\n step: T\n): Array> {\n const key = getStepKey(step);\n const updated: Array> = [];\n let found = false;\n for (const s of prev) {\n if (getStepKey(s.step) === key) {\n updated.push({ ...s, completed: true, step: { ...s.step, ...step } });\n found = true;\n } else {\n updated.push(s);\n }\n }\n if (!found) {\n updated.push({\n id: updated.length,\n completed: true,\n step,\n });\n }\n return updated;\n}\n", "type": "registry:component", "target": "components/common/tx/steps.ts" }, @@ -177,25 +177,25 @@ }, { "path": "registry/nexus-elements/common/types/transaction-flow.ts", - "content": "import {\n type NexusSDK,\n type SUPPORTED_CHAINS_IDS,\n type SUPPORTED_TOKENS,\n} from \"@avail-project/nexus-core\";\nimport { type Address } from \"viem\";\n\nexport type TransactionFlowType = \"bridge\" | \"transfer\";\n\nexport interface TransactionFlowInputs {\n chain: SUPPORTED_CHAINS_IDS;\n token: SUPPORTED_TOKENS;\n amount?: string;\n recipient?: `0x${string}`;\n}\n\nexport interface TransactionFlowPrefill {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n}\n\ntype BridgeOptions = NonNullable[1]>;\n\nexport type TransactionFlowEvent =\n NonNullable extends (event: infer E) => void\n ? E\n : never;\n\nexport type TransactionFlowOnEvent = NonNullable;\n\nexport interface TransactionFlowExecuteParams {\n token: SUPPORTED_TOKENS;\n amount: bigint;\n toChainId: SUPPORTED_CHAINS_IDS;\n recipient: `0x${string}`;\n sourceChains?: number[];\n onEvent: TransactionFlowOnEvent;\n}\n\nexport type TransactionFlowExecutor = (\n params: TransactionFlowExecuteParams,\n) => Promise<{ explorerUrl: string } | null>;\n\nexport type SourceCoverageState = \"healthy\" | \"warning\" | \"error\";\n\nexport interface SourceSelectionValidation {\n coverageState: SourceCoverageState;\n isBelowRequired: boolean;\n missingToProceed: string;\n missingToSafety: string;\n}\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport { type Address } from \"viem\";\n\ntype NexusClient = ReturnType;\n\n// v2 uses string token symbols (toTokenSymbol) with number chain IDs\nexport type TransactionFlowType = \"bridge\" | \"transfer\";\n\nexport interface TransactionFlowInputs {\n chain: number;\n token: string;\n amount?: string;\n recipient?: `0x${string}`;\n}\n\nexport interface TransactionFlowPrefill {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n}\n\n// v2 bridge onEvent uses typed discriminated union, not NEXUS_EVENTS\nexport type TransactionFlowEvent =\n | { type: \"status\"; status: string }\n | { type: \"plan_preview\"; plan: { steps: unknown[] } }\n | { type: \"plan_confirmed\"; plan: { steps: unknown[] } }\n | { type: \"plan_progress\"; stepType: string; state: string; step: unknown };\n\nexport type TransactionFlowOnEvent = (event: TransactionFlowEvent) => void;\n\nexport interface TransactionFlowExecuteParams {\n token: string;\n amount: bigint;\n toChainId: number;\n recipient: `0x${string}`;\n sources?: number[];\n onEvent: TransactionFlowOnEvent;\n}\n\nexport type TransactionFlowExecutor = (\n params: TransactionFlowExecuteParams,\n) => Promise<{ explorerUrl: string } | null>;\n\nexport type SourceCoverageState = \"healthy\" | \"warning\" | \"error\";\n\nexport interface SourceSelectionValidation {\n coverageState: SourceCoverageState;\n isBelowRequired: boolean;\n missingToProceed: string;\n missingToSafety: string;\n}\n", "type": "registry:component", "target": "components/common/types/transaction-flow.ts" }, { "path": "registry/nexus-elements/common/utils/constant.ts", - "content": "import { SUPPORTED_CHAINS } from \"@avail-project/nexus-core\";\nimport { formatUnits, parseUnits } from \"viem\";\n\nexport const SHORT_CHAIN_NAME: Record = {\n [SUPPORTED_CHAINS.ETHEREUM]: \"Ethereum\",\n [SUPPORTED_CHAINS.BASE]: \"Base\",\n [SUPPORTED_CHAINS.ARBITRUM]: \"Arbitrum\",\n [SUPPORTED_CHAINS.OPTIMISM]: \"Optimism\",\n [SUPPORTED_CHAINS.POLYGON]: \"Polygon\",\n [SUPPORTED_CHAINS.AVALANCHE]: \"Avalanche\",\n [SUPPORTED_CHAINS.SCROLL]: \"Scroll\",\n [SUPPORTED_CHAINS.MEGAETH]: \"MegaETH\",\n [SUPPORTED_CHAINS.KAIA]: \"Kaia\",\n [SUPPORTED_CHAINS.BNB]: \"BNB\",\n [SUPPORTED_CHAINS.MONAD]: \"Monad\",\n [SUPPORTED_CHAINS.HYPEREVM]: \"HyperEVM\",\n [SUPPORTED_CHAINS.CITREA]: \"Citrea\",\n // [SUPPORTED_CHAINS.TRON]: \"Tron\",\n [SUPPORTED_CHAINS.SEPOLIA]: \"Sepolia\",\n [SUPPORTED_CHAINS.BASE_SEPOLIA]: \"Base Sepolia\",\n [SUPPORTED_CHAINS.ARBITRUM_SEPOLIA]: \"Arbitrum Sepolia\",\n [SUPPORTED_CHAINS.OPTIMISM_SEPOLIA]: \"Optimism Sepolia\",\n [SUPPORTED_CHAINS.POLYGON_AMOY]: \"Polygon Amoy\",\n [SUPPORTED_CHAINS.MONAD_TESTNET]: \"Monad Testnet\",\n // [SUPPORTED_CHAINS.TRON_SHASTA]: \"Tron Shasta\",\n} as const;\n\nconst DEFAULT_SAFETY_MARGIN = 0.01; // 1%\n\n/**\n * Compute an amount string for fraction buttons (25%, 50%, 75%, 100%).\n *\n * @param balanceStr - user's balance as a human decimal string (e.g. \"12.345\") OR as base-unit integer string if `balanceIsBaseUnits` true\n * @param fraction - fraction e.g. 0.25, 0.5, 0.75, 1\n * @param decimals - token decimals (6 for USDC/USDT, 18 for ETH)\n * @param safetyMargin - 0.01 for 1% default\n * @param balanceIsBaseUnits - if true, balanceStr is already base units integer string (wei / smallest unit)\n * @returns decimal string clipped to token decimals (rounded down)\n */\nexport function computeAmountFromFraction(\n balanceStr: string,\n fraction: number,\n decimals: number,\n safetyMargin = DEFAULT_SAFETY_MARGIN,\n balanceIsBaseUnits = false,\n): string {\n if (!balanceStr) return \"0\";\n\n // parse balance into base units (BigInt)\n const balanceUnits: bigint = balanceIsBaseUnits\n ? BigInt(balanceStr)\n : parseUnits(balanceStr, decimals);\n\n if (balanceUnits === BigInt(0)) return \"0\";\n\n // Use an integer precision multiplier to avoid FP issues\n const PREC = 1_000_000; // 1e6 precision for fraction & safety margin\n const safetyMul = BigInt(Math.max(0, Math.floor((1 - safetyMargin) * PREC))); // (1 - safetyMargin) * PREC\n const fractionMul = BigInt(Math.max(0, Math.floor(fraction * PREC))); // fraction * PREC\n\n // Apply safety margin: floor(balance * (1 - safetyMargin))\n const maxAfterSafety = (balanceUnits * safetyMul) / BigInt(PREC);\n\n // Apply fraction and floor: floor(maxAfterSafety * fraction)\n let desiredUnits = (maxAfterSafety * fractionMul) / BigInt(PREC);\n\n // Extra clamp just in case\n if (desiredUnits > balanceUnits) desiredUnits = balanceUnits;\n if (desiredUnits < BigInt(0)) desiredUnits = BigInt(0);\n\n // format back to human readable decimal string with token decimals (formatUnits truncates/keeps decimals)\n // formatUnits will produce exactly decimals digits if fractional part exists; we'll strip trailing zeros.\n const raw = formatUnits(desiredUnits, decimals);\n // strip trailing zeros and possible trailing dot\n return raw\n .replace(/(\\.\\d*?[1-9])0+$/u, \"$1\")\n .replace(/\\.0+$/u, \"\")\n .replace(/^\\.$/u, \"0\");\n}\n\nexport const usdFormatter = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n});\n\nconst usdFormatterPrecise = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 3,\n maximumFractionDigits: 3,\n});\n\n/**\n * Formats USD values for UI.\n * Values between 0 and 0.001 are shown as \"< $0.001\".\n * Values between 0.001 and 0.01 are shown with 3 decimals.\n */\nexport function formatUsdForDisplay(value: number): string {\n if (!Number.isFinite(value)) return usdFormatter.format(0);\n const absValue = Math.abs(value);\n\n if (absValue === 0) return usdFormatter.format(0);\n if (absValue < 0.001) return \"< $0.001\";\n if (absValue < 0.01) {\n return usdFormatterPrecise.format(value);\n }\n\n return usdFormatter.format(value);\n}\n", + "content": "import { formatUnits, parseUnits } from \"viem\";\n\n// v2: SUPPORTED_CHAINS removed — using literal EVM chain IDs\nexport const SHORT_CHAIN_NAME: Record = {\n 1: \"Ethereum\",\n 8453: \"Base\",\n 42161: \"Arbitrum\",\n 10: \"Optimism\",\n 137: \"Polygon\",\n 43114: \"Avalanche\",\n 534352: \"Scroll\",\n 6342: \"MegaETH\",\n 8217: \"Kaia\",\n 56: \"BNB\",\n 10143: \"Monad\",\n 999: \"HyperEVM\",\n 5115: \"Citrea\",\n 11155111: \"Sepolia\",\n 84532: \"Base Sepolia\",\n 421614: \"Arbitrum Sepolia\",\n 11155420: \"Optimism Sepolia\",\n 80002: \"Polygon Amoy\",\n} as const;\n\nconst DEFAULT_SAFETY_MARGIN = 0.01; // 1%\n\n/**\n * Compute an amount string for fraction buttons (25%, 50%, 75%, 100%).\n *\n * @param balanceStr - user's balance as a human decimal string (e.g. \"12.345\") OR as base-unit integer string if `balanceIsBaseUnits` true\n * @param fraction - fraction e.g. 0.25, 0.5, 0.75, 1\n * @param decimals - token decimals (6 for USDC/USDT, 18 for ETH)\n * @param safetyMargin - 0.01 for 1% default\n * @param balanceIsBaseUnits - if true, balanceStr is already base units integer string (wei / smallest unit)\n * @returns decimal string clipped to token decimals (rounded down)\n */\nexport function computeAmountFromFraction(\n balanceStr: string,\n fraction: number,\n decimals: number,\n safetyMargin = DEFAULT_SAFETY_MARGIN,\n balanceIsBaseUnits = false,\n): string {\n if (!balanceStr) return \"0\";\n\n // parse balance into base units (BigInt)\n const balanceUnits: bigint = balanceIsBaseUnits\n ? BigInt(balanceStr)\n : parseUnits(balanceStr, decimals);\n\n if (balanceUnits === BigInt(0)) return \"0\";\n\n // Use an integer precision multiplier to avoid FP issues\n const PREC = 1_000_000; // 1e6 precision for fraction & safety margin\n const safetyMul = BigInt(Math.max(0, Math.floor((1 - safetyMargin) * PREC))); // (1 - safetyMargin) * PREC\n const fractionMul = BigInt(Math.max(0, Math.floor(fraction * PREC))); // fraction * PREC\n\n // Apply safety margin: floor(balance * (1 - safetyMargin))\n const maxAfterSafety = (balanceUnits * safetyMul) / BigInt(PREC);\n\n // Apply fraction and floor: floor(maxAfterSafety * fraction)\n let desiredUnits = (maxAfterSafety * fractionMul) / BigInt(PREC);\n\n // Extra clamp just in case\n if (desiredUnits > balanceUnits) desiredUnits = balanceUnits;\n if (desiredUnits < BigInt(0)) desiredUnits = BigInt(0);\n\n // format back to human readable decimal string with token decimals (formatUnits truncates/keeps decimals)\n // formatUnits will produce exactly decimals digits if fractional part exists; we'll strip trailing zeros.\n const raw = formatUnits(desiredUnits, decimals);\n // strip trailing zeros and possible trailing dot\n return raw\n .replace(/(\\.\\d*?[1-9])0+$/u, \"$1\")\n .replace(/\\.0+$/u, \"\")\n .replace(/^\\.$/u, \"0\");\n}\n\nexport const usdFormatter = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n});\n\nconst usdFormatterPrecise = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 3,\n maximumFractionDigits: 3,\n});\n\n/**\n * Formats USD values for UI.\n * Values between 0 and 0.001 are shown as \"< $0.001\".\n * Values between 0.001 and 0.01 are shown with 3 decimals.\n */\nexport function formatUsdForDisplay(value: number): string {\n if (!Number.isFinite(value)) return usdFormatter.format(0);\n const absValue = Math.abs(value);\n\n if (absValue === 0) return usdFormatter.format(0);\n if (absValue < 0.001) return \"< $0.001\";\n if (absValue < 0.01) {\n return usdFormatterPrecise.format(value);\n }\n\n return usdFormatter.format(value);\n}\n", "type": "registry:component", "target": "components/common/utils/constant.ts" }, { "path": "registry/nexus-elements/common/utils/token-pricing.ts", - "content": "import type { SupportedChainsAndTokensResult } from \"@avail-project/nexus-core\";\n\nconst COINBASE_SPOT_API_BASE = \"https://api.coinbase.com/v2/prices\";\nconst COINBASE_EXCHANGE_RATES_API_BASE =\n \"https://api.coinbase.com/v2/exchange-rates\";\n\nexport const DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS = 4_000;\nexport const USD_PEGGED_FALLBACK_RATE = 1;\nexport const DEFAULT_USD_PEGGED_TOKEN_SYMBOLS = [\n \"USDT\",\n \"USDC\",\n \"USDS\",\n \"DAI\",\n \"USDM\",\n \"FDUSD\",\n \"BUSD\",\n \"TUSD\",\n \"PYUSD\",\n \"GUSD\",\n \"LUSD\",\n \"USDE\",\n \"USDP\",\n] as const;\n\ntype CoinbaseSpotPriceResponse = {\n data?: {\n amount?: string | number;\n };\n};\n\ntype CoinbaseExchangeRatesResponse = {\n data?: {\n rates?: Record;\n };\n};\n\ntype SupportedTokenMetadata = {\n symbol?: string;\n equivalentCurrency?: string;\n};\n\ntype SupportedChainMetadata = {\n tokens?: SupportedTokenMetadata[];\n};\n\nexport function normalizeTokenSymbol(tokenSymbol: string): string {\n return tokenSymbol.trim().toUpperCase();\n}\n\nexport function toFinitePositiveNumber(value: unknown): number | null {\n const parsed = Number.parseFloat(String(value));\n if (!Number.isFinite(parsed) || parsed <= 0) {\n return null;\n }\n return parsed;\n}\n\nexport function getCoinbaseSymbolCandidates(tokenSymbol: string): string[] {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return [];\n\n const baseSymbol = normalized.split(/[._-]/)[0] ?? normalized;\n const wrappedBase =\n baseSymbol.startsWith(\"W\") && baseSymbol.length > 3\n ? baseSymbol.slice(1)\n : null;\n\n return Array.from(\n new Set(\n [normalized, baseSymbol, wrappedBase].filter(\n (symbol): symbol is string => Boolean(symbol),\n ),\n ),\n );\n}\n\nexport function buildUsdPeggedSymbolSet(\n supportedChains: SupportedChainsAndTokensResult | null,\n baseSymbols: Iterable = DEFAULT_USD_PEGGED_TOKEN_SYMBOLS,\n): Set {\n const symbolSet = new Set(baseSymbols);\n\n for (const chain of (supportedChains ?? []) as SupportedChainMetadata[]) {\n for (const token of chain.tokens ?? []) {\n const symbol = normalizeTokenSymbol(token.symbol ?? \"\");\n const equivalent = normalizeTokenSymbol(token.equivalentCurrency ?? \"\");\n if (!symbol) continue;\n\n if (equivalent && symbolSet.has(equivalent)) {\n symbolSet.add(symbol);\n }\n }\n }\n\n return symbolSet;\n}\n\nasync function fetchJsonWithTimeout(\n url: string,\n requestTimeoutMs: number,\n): Promise {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);\n try {\n const response = await fetch(url, {\n signal: controller.signal,\n });\n if (!response.ok) return null;\n return (await response.json()) as T;\n } catch {\n return null;\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nexport async function fetchCoinbaseUsdRate(\n tokenSymbol: string,\n requestTimeoutMs = DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS,\n): Promise {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return null;\n\n for (const candidate of getCoinbaseSymbolCandidates(normalized)) {\n const spotBody = await fetchJsonWithTimeout(\n `${COINBASE_SPOT_API_BASE}/${encodeURIComponent(candidate)}-USD/spot`,\n requestTimeoutMs,\n );\n const spotAmount = toFinitePositiveNumber(spotBody?.data?.amount);\n if (spotAmount) return spotAmount;\n\n const exchangeRatesBody =\n await fetchJsonWithTimeout(\n `${COINBASE_EXCHANGE_RATES_API_BASE}?currency=${encodeURIComponent(candidate)}`,\n requestTimeoutMs,\n );\n const exchangeRatesAmount = toFinitePositiveNumber(\n exchangeRatesBody?.data?.rates?.USD,\n );\n if (exchangeRatesAmount) return exchangeRatesAmount;\n }\n\n return null;\n}\n", + "content": "// v2: getSupportedChains() return type is inferred directly; define a structural type\ntype SupportedChainsAndTokensResult = readonly {\n tokens?: { symbol?: string; equivalentCurrency?: string }[];\n [key: string]: unknown;\n}[];\n\nconst COINBASE_SPOT_API_BASE = \"https://api.coinbase.com/v2/prices\";\nconst COINBASE_EXCHANGE_RATES_API_BASE =\n \"https://api.coinbase.com/v2/exchange-rates\";\n\nexport const DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS = 4_000;\nexport const USD_PEGGED_FALLBACK_RATE = 1;\nexport const DEFAULT_USD_PEGGED_TOKEN_SYMBOLS = [\n \"USDT\",\n \"USDC\",\n \"USDS\",\n \"DAI\",\n \"USDM\",\n \"FDUSD\",\n \"BUSD\",\n \"TUSD\",\n \"PYUSD\",\n \"GUSD\",\n \"LUSD\",\n \"USDE\",\n \"USDP\",\n] as const;\n\ntype CoinbaseSpotPriceResponse = {\n data?: {\n amount?: string | number;\n };\n};\n\ntype CoinbaseExchangeRatesResponse = {\n data?: {\n rates?: Record;\n };\n};\n\ntype SupportedTokenMetadata = {\n symbol?: string;\n equivalentCurrency?: string;\n};\n\ntype SupportedChainMetadata = {\n tokens?: SupportedTokenMetadata[];\n};\n\nexport function normalizeTokenSymbol(tokenSymbol: string): string {\n return tokenSymbol.trim().toUpperCase();\n}\n\nexport function toFinitePositiveNumber(value: unknown): number | null {\n const parsed = Number.parseFloat(String(value));\n if (!Number.isFinite(parsed) || parsed <= 0) {\n return null;\n }\n return parsed;\n}\n\nexport function getCoinbaseSymbolCandidates(tokenSymbol: string): string[] {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return [];\n\n const baseSymbol = normalized.split(/[._-]/)[0] ?? normalized;\n const wrappedBase =\n baseSymbol.startsWith(\"W\") && baseSymbol.length > 3\n ? baseSymbol.slice(1)\n : null;\n\n return Array.from(\n new Set(\n [normalized, baseSymbol, wrappedBase].filter(\n (symbol): symbol is string => Boolean(symbol),\n ),\n ),\n );\n}\n\nexport function buildUsdPeggedSymbolSet(\n supportedChains: SupportedChainsAndTokensResult | null,\n baseSymbols: Iterable = DEFAULT_USD_PEGGED_TOKEN_SYMBOLS,\n): Set {\n const symbolSet = new Set(baseSymbols);\n\n for (const chain of (supportedChains ?? []) as SupportedChainMetadata[]) {\n for (const token of chain.tokens ?? []) {\n const symbol = normalizeTokenSymbol(token.symbol ?? \"\");\n const equivalent = normalizeTokenSymbol(token.equivalentCurrency ?? \"\");\n if (!symbol) continue;\n\n if (equivalent && symbolSet.has(equivalent)) {\n symbolSet.add(symbol);\n }\n }\n }\n\n return symbolSet;\n}\n\nasync function fetchJsonWithTimeout(\n url: string,\n requestTimeoutMs: number,\n): Promise {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);\n try {\n const response = await fetch(url, {\n signal: controller.signal,\n });\n if (!response.ok) return null;\n return (await response.json()) as T;\n } catch {\n return null;\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nexport async function fetchCoinbaseUsdRate(\n tokenSymbol: string,\n requestTimeoutMs = DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS,\n): Promise {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return null;\n\n for (const candidate of getCoinbaseSymbolCandidates(normalized)) {\n const spotBody = await fetchJsonWithTimeout(\n `${COINBASE_SPOT_API_BASE}/${encodeURIComponent(candidate)}-USD/spot`,\n requestTimeoutMs,\n );\n const spotAmount = toFinitePositiveNumber(spotBody?.data?.amount);\n if (spotAmount) return spotAmount;\n\n const exchangeRatesBody =\n await fetchJsonWithTimeout(\n `${COINBASE_EXCHANGE_RATES_API_BASE}?currency=${encodeURIComponent(candidate)}`,\n requestTimeoutMs,\n );\n const exchangeRatesAmount = toFinitePositiveNumber(\n exchangeRatesBody?.data?.rates?.USD,\n );\n if (exchangeRatesAmount) return exchangeRatesAmount;\n }\n\n return null;\n}\n", "type": "registry:component", "target": "components/common/utils/token-pricing.ts" }, { "path": "registry/nexus-elements/common/utils/transaction-flow.ts", - "content": "import {\n formatUnits,\n type NexusNetwork,\n NexusSDK,\n SUPPORTED_CHAINS,\n type SUPPORTED_CHAINS_IDS,\n type SUPPORTED_TOKENS,\n} from \"@avail-project/nexus-core\";\nimport { type Address } from \"viem\";\n\nconst MAX_AMOUNT_REGEX = /^\\d*\\.?\\d+$/;\n\nexport const MAX_AMOUNT_DEBOUNCE_MS = 300;\n\nexport const normalizeMaxAmount = (\n maxAmount?: string | number,\n): string | undefined => {\n if (maxAmount === undefined || maxAmount === null) return undefined;\n const value = String(maxAmount).trim();\n if (!value || value === \".\" || !MAX_AMOUNT_REGEX.test(value)) {\n return undefined;\n }\n const parsed = Number.parseFloat(value);\n if (!Number.isFinite(parsed) || parsed <= 0) return undefined;\n return value;\n};\n\nexport const clampAmountToMax = ({\n amount,\n maxAmount,\n nexusSDK,\n token,\n chainId,\n}: {\n amount: string;\n maxAmount?: string;\n nexusSDK: NexusSDK;\n token: SUPPORTED_TOKENS;\n chainId: SUPPORTED_CHAINS_IDS;\n}): string => {\n if (!maxAmount) return amount;\n try {\n const amountRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n token,\n chainId,\n );\n const maxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxAmount,\n token,\n chainId,\n );\n return amountRaw > maxRaw ? maxAmount : amount;\n } catch {\n return amount;\n }\n};\n\nexport const formatAmountForDisplay = (\n amount: bigint,\n decimals: number | undefined,\n nexusSDK: NexusSDK,\n): string => {\n if (typeof decimals !== \"number\") return amount.toString();\n const formatted = formatUnits(amount, decimals);\n if (!formatted.includes(\".\")) return formatted;\n const [whole, fraction] = formatted.split(\".\");\n const trimmedFraction = fraction.slice(0, 6).replace(/0+$/, \"\");\n if (!trimmedFraction && whole === \"0\" && amount > BigInt(0)) {\n return \"0.000001\";\n }\n return trimmedFraction ? `${whole}.${trimmedFraction}` : whole;\n};\n\nexport const buildInitialInputs = ({\n type,\n network,\n connectedAddress,\n prefill,\n}: {\n type: \"bridge\" | \"transfer\";\n network: NexusNetwork;\n connectedAddress?: Address;\n prefill?: {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n };\n}) => {\n return {\n chain:\n (prefill?.chainId as SUPPORTED_CHAINS_IDS) ??\n (network === \"testnet\"\n ? SUPPORTED_CHAINS.SEPOLIA\n : SUPPORTED_CHAINS.ETHEREUM),\n token: (prefill?.token as SUPPORTED_TOKENS) ?? \"USDC\",\n amount: prefill?.amount ?? undefined,\n recipient:\n (prefill?.recipient as `0x${string}`) ??\n (type === \"bridge\" ? connectedAddress : undefined),\n };\n};\n\nexport const getCoverageDecimals = ({\n type,\n token,\n chainId,\n fallback,\n}: {\n type: \"bridge\" | \"transfer\";\n token?: SUPPORTED_TOKENS;\n chainId?: SUPPORTED_CHAINS_IDS;\n fallback: number | undefined;\n}) => {\n if (token === \"USDM\") return 18;\n if (\n type === \"bridge\" &&\n token === \"USDC\" &&\n chainId === SUPPORTED_CHAINS.BNB\n ) {\n return 18;\n }\n return fallback;\n};\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type { NexusNetwork } from \"@avail-project/nexus-sdk-v2\";\nimport { formatUnits, type Address } from \"viem\";\n\ntype NexusClient = ReturnType;\n\nconst MAX_AMOUNT_REGEX = /^\\d*\\.?\\d+$/;\n\n// v2 chain IDs for defaults\nconst SEPOLIA_CHAIN_ID = 11155111;\nconst ETHEREUM_CHAIN_ID = 1;\n// v2: BNB chain ID for edge-case decimal override\nconst BNB_CHAIN_ID = 56;\n\nexport const MAX_AMOUNT_DEBOUNCE_MS = 300;\n\nexport const normalizeMaxAmount = (\n maxAmount?: string | number,\n): string | undefined => {\n if (maxAmount === undefined || maxAmount === null) return undefined;\n const value = String(maxAmount).trim();\n if (!value || value === \".\" || !MAX_AMOUNT_REGEX.test(value)) {\n return undefined;\n }\n const parsed = Number.parseFloat(value);\n if (!Number.isFinite(parsed) || parsed <= 0) return undefined;\n return value;\n};\n\nexport const clampAmountToMax = ({\n amount,\n maxAmount,\n nexusSDK,\n token,\n chainId,\n}: {\n amount: string;\n maxAmount?: string;\n nexusSDK: NexusClient;\n token: string;\n chainId: number;\n}): string => {\n if (!maxAmount) return amount;\n try {\n // v2: convertTokenReadableAmountToBigInt(amount, tokenSymbol, chainId)\n const amountRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n token,\n chainId,\n );\n const maxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxAmount,\n token,\n chainId,\n );\n return amountRaw > maxRaw ? maxAmount : amount;\n } catch {\n return amount;\n }\n};\n\nexport const formatAmountForDisplay = (\n amount: bigint,\n decimals: number | undefined,\n // nexusSDK kept for API compatibility but formatUnits is now imported directly\n _nexusSDK: NexusClient,\n): string => {\n if (typeof decimals !== \"number\") return amount.toString();\n const formatted = formatUnits(amount, decimals);\n if (!formatted.includes(\".\")) return formatted;\n const [whole, fraction] = formatted.split(\".\");\n const trimmedFraction = fraction.slice(0, 6).replace(/0+$/, \"\");\n if (!trimmedFraction && whole === \"0\" && amount > BigInt(0)) {\n return \"0.000001\";\n }\n return trimmedFraction ? `${whole}.${trimmedFraction}` : whole;\n};\n\nexport const buildInitialInputs = ({\n type,\n network,\n connectedAddress,\n prefill,\n}: {\n type: \"bridge\" | \"transfer\";\n network: NexusNetwork;\n connectedAddress?: Address;\n prefill?: {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n };\n}) => {\n return {\n // v2 uses plain number chain IDs and string token symbols\n chain:\n prefill?.chainId ??\n (network === \"testnet\" ? SEPOLIA_CHAIN_ID : ETHEREUM_CHAIN_ID),\n token: prefill?.token ?? \"USDC\",\n amount: prefill?.amount ?? undefined,\n recipient:\n (prefill?.recipient as `0x${string}`) ??\n (type === \"bridge\" ? connectedAddress : undefined),\n };\n};\n\nexport const getCoverageDecimals = ({\n type,\n token,\n chainId,\n fallback,\n}: {\n type: \"bridge\" | \"transfer\";\n token?: string;\n chainId?: number;\n fallback: number | undefined;\n}) => {\n if (token === \"USDM\") return 18;\n if (type === \"bridge\" && token === \"USDC\" && chainId === BNB_CHAIN_ID) {\n return 18;\n }\n return fallback;\n};\n", "type": "registry:component", "target": "components/common/utils/transaction-flow.ts" } diff --git a/public/r/nexus-provider.json b/public/r/nexus-provider.json index 530273f..accadbb 100644 --- a/public/r/nexus-provider.json +++ b/public/r/nexus-provider.json @@ -5,13 +5,13 @@ "title": "Nexus Provider", "description": "Shared Nexus SDK provider and types for Nexus Elements", "dependencies": [ - "@avail-project/nexus-core@1.2.0", + "@avail-project/nexus-sdk-v2", "wagmi" ], "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 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: false,\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 sdkRef.current ??= new NexusSDK({\n ...stableConfig,\n });\n const sdk = sdkRef.current;\n\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 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 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 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, rates] = await Promise.allSettled([\n sdk.getBalancesForBridge(),\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 }, [sdk, config?.network, normalizeUserAssetFiatValues]);\n\n const initializeNexus = useCallback(\n async (provider: EthereumProvider) => {\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 if (!nexusSDK) throw new Error(\"Nexus is not initialized\");\n await nexusSDK?.deinit();\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 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.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 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 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 createNexusClient,\n type OnAllowanceHookData,\n type OnIntentHookData,\n type OnSwapIntentHookData,\n type SwapAndExecuteOnIntentHookData,\n type TokenBalance,\n type ChainBalance,\n} from \"@avail-project/nexus-sdk-v2\";\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 fetchCoinbaseUsdRate,\n getCoinbaseSymbolCandidates,\n normalizeTokenSymbol,\n toFinitePositiveNumber,\n} from \"../common/utils/token-pricing\";\n\ntype NexusClient = ReturnType;\ntype SupportedChainsResult = ReturnType;\n\ninterface NexusContextType {\n nexusClient: NexusClient | null;\n /** @deprecated use nexusClient */\n nexusSDK: NexusClient | null;\n bridgableBalance: TokenBalance[] | null;\n swapBalance: TokenBalance[] | null;\n intent: RefObject;\n allowance: RefObject;\n swapIntent: RefObject;\n /** Intent ref for the pure swap widget (swapWithExactIn/Out flows) */\n swapWidgetIntent: RefObject;\n exchangeRate: Record | null;\n supportedChainsAndTokens: SupportedChainsResult | 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: \"testnet\",\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 clientRef = useRef(null);\n if (!clientRef.current) {\n clientRef.current = createNexusClient({ ...stableConfig });\n }\n const client = clientRef.current;\n\n const [nexusClient, setNexusClient] = useState(null);\n const [loading, setLoading] = useState(false);\n const supportedChainsAndTokens = useRef(null);\n const swapSupportedChainsAndTokens = useRef(\n null,\n );\n const [bridgableBalance, setBridgableBalance] = useState<\n TokenBalance[] | null\n >(null);\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 // swapIntent is set by the deposit widget's swapAndExecute onIntent callback\n const swapIntent = useRef(null);\n // swapWidgetIntent is set by useSwaps for the pure swap widget flow\n const swapWidgetIntent = useRef(null);\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: TokenBalance[] | null): TokenBalance[] | null => {\n if (!assets) return assets;\n\n return assets.map((asset) => {\n let computedAssetUsd = 0;\n\n // v2: chainBalances replaces breakdown; value is a string instead of number\n const chainBalances = (asset.chainBalances ?? []).map((entry: ChainBalance) => {\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(String(entry.value ?? \"0\"));\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(asset.symbol);\n if (rate > 0) {\n normalizedUsd = safeBalance * rate;\n }\n }\n\n computedAssetUsd += normalizedUsd;\n return {\n ...entry,\n value: normalizedUsd.toString(),\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(String(asset.value ?? \"0\"));\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 value: normalizedAssetUsd.toString(),\n chainBalances,\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 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 // v2: getSupportedChains() is an instance method that uses configured network\n const list = client.getSupportedChains();\n supportedChainsAndTokens.current = list ?? null;\n usdPeggedSymbols.current = buildUsdPeggedSymbolSet(list ?? null);\n // v2: no separate getSwapSupportedChains — reuse getSupportedChains\n swapSupportedChainsAndTokens.current = list ?? null;\n\n // v2: getBalancesForBridge() is now async and returns array directly\n const [bridgeAbleBalanceResult] = await Promise.allSettled([\n client.getBalancesForBridge(),\n ]);\n\n if (bridgeAbleBalanceResult.status === \"fulfilled\") {\n setBridgableBalance(\n normalizeUserAssetFiatValues(bridgeAbleBalanceResult.value),\n );\n }\n }, [client, normalizeUserAssetFiatValues]);\n\n const initializeNexus = useCallback(\n async (provider: EthereumProvider) => {\n setLoading(true);\n try {\n // v2: two-step init — initialize() fetches deployment, setEVMProvider() connects wallet\n await client.initialize();\n await client.setEVMProvider(provider);\n setNexusClient(client);\n } catch (error) {\n console.error(\"Error initializing Nexus:\", error);\n throw error;\n } finally {\n setLoading(false);\n }\n },\n [client],\n );\n\n const deinitializeNexus = useCallback(async () => {\n try {\n // v2: destroy() is synchronous\n client.destroy();\n setNexusClient(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 }, [client]);\n\n // v2: hooks are now passed per-operation, not set globally.\n // attachEventHooks is kept for API compatibility but is a no-op.\n // Hooks (onIntent, onAllowance) are passed as options to bridge/transfer/swap calls.\n const attachEventHooks = useCallback(() => {\n // No-op in v2: hooks are passed per-operation via options.hooks\n }, []);\n\n const handleInit = useCallback(\n async (provider: EthereumProvider) => {\n if (nexusClient || 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 await setupNexus();\n // attachEventHooks is a no-op in v2\n } catch (error) {\n console.error(\"Error during Nexus setup flow:\", error);\n throw error;\n }\n },\n [nexusClient, loading, initializeNexus, setupNexus],\n );\n\n const fetchBridgableBalance = useCallback(async () => {\n try {\n // v2: returns array directly (no .assets wrapper)\n const updatedBalance = await client.getBalancesForBridge();\n setBridgableBalance(normalizeUserAssetFiatValues(updatedBalance));\n } catch (error) {\n console.error(\"Error fetching bridgable balance:\", error);\n }\n }, [client, normalizeUserAssetFiatValues]);\n\n const fetchSwapBalance = useCallback(async () => {\n try {\n // v2: no filter param\n const updatedBalance = await client.getBalancesForSwap();\n setSwapBalance(normalizeUserAssetFiatValues(updatedBalance));\n } catch (error) {\n console.error(\"Error fetching swap balance:\", error);\n }\n }, [client, 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 nexusClient,\n nexusSDK: nexusClient, // backward-compat alias\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 swapWidgetIntent,\n exchangeRate: exchangeRateState,\n getFiatValue,\n resolveTokenUsdRate,\n }),\n [\n nexusClient,\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", "type": "registry:component", "target": "components/nexus/NexusProvider.tsx" } diff --git a/public/r/swaps.json b/public/r/swaps.json index 14b391c..0eb4a44 100644 --- a/public/r/swaps.json +++ b/public/r/swaps.json @@ -5,7 +5,7 @@ "title": "Swaps", "description": "Swap tokens across chains (Exact In)", "dependencies": [ - "@avail-project/nexus-core@1.2.0", + "@avail-project/nexus-sdk-v2", "lucide-react", "viem" ], @@ -30,25 +30,25 @@ }, { "path": "registry/nexus-elements/swaps/components/destination-asset-select.tsx", - "content": "\"use client\";\nimport { type FC, useMemo, useState } from \"react\";\nimport { Button } from \"../../ui/button\";\nimport {\n type SUPPORTED_CHAINS_IDS,\n CHAIN_METADATA,\n type UserAsset,\n formatTokenBalance,\n} from \"@avail-project/nexus-core\";\nimport { DESTINATION_SWAP_TOKENS } from \"../config/destination\";\nimport { DialogClose } from \"../../ui/dialog\";\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectTrigger,\n} from \"../../ui/select\";\nimport { Link2, Search, X } from \"lucide-react\";\nimport { SHORT_CHAIN_NAME, usdFormatter } from \"../../common\";\nimport { TokenIcon } from \"./token-icon\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { type DestinationTokenInfo } from \"../hooks/useSwaps\";\n\ninterface DestinationAssetSelectProps {\n swapBalance: UserAsset[] | null;\n onSelect: (\n chainId: SUPPORTED_CHAINS_IDS,\n token: DestinationTokenInfo,\n ) => void;\n}\n\nconst DestinationAssetSelect: FC = ({\n swapBalance,\n onSelect,\n}) => {\n const { nexusSDK } = useNexus();\n const [tempChain, setTempChain] = useState(null);\n const [searchQuery, setSearchQuery] = useState(\"\");\n\n // Get all tokens from all chains with their chain info\n const allTokens: DestinationTokenInfo[] = useMemo(() => {\n const tokens: DestinationTokenInfo[] = [];\n for (const [chainId, chainTokens] of DESTINATION_SWAP_TOKENS.entries()) {\n for (const token of chainTokens) {\n tokens.push({\n ...token,\n chainId,\n });\n }\n }\n return tokens.map((token) => {\n const balance = swapBalance\n ?.flatMap((asset) => asset.breakdown ?? [])\n ?.find(\n (chain) =>\n chain.symbol.toUpperCase() === token.symbol.toUpperCase() &&\n chain.chain?.id === token.chainId,\n );\n return {\n ...token,\n balance: formatTokenBalance(balance?.balance ?? \"0\", {\n symbol: balance?.symbol ?? token.symbol,\n decimals: balance?.decimals ?? 0,\n }),\n balanceInFiat: usdFormatter.format(balance?.balanceInFiat ?? 0),\n };\n });\n }, [swapBalance]);\n\n // Only show chains that have tokens\n const chainsWithTokens = useMemo(() => {\n return Array.from(DESTINATION_SWAP_TOKENS.keys());\n }, []);\n\n // Filter tokens by selected chain and search query\n const displayedTokens: DestinationTokenInfo[] = useMemo(() => {\n let filtered = allTokens;\n\n // Filter by chain\n if (tempChain) {\n filtered = filtered.filter((t) => t.chainId === tempChain);\n }\n\n // Filter by search query\n if (searchQuery.trim()) {\n const query = searchQuery.toLowerCase().trim();\n filtered = filtered.filter(\n (t) =>\n t.symbol.toLowerCase().includes(query) ||\n t.name.toLowerCase().includes(query) ||\n t.tokenAddress.toLowerCase().includes(query),\n );\n }\n\n return filtered;\n }, [tempChain, allTokens, searchQuery]);\n\n const handlePick = (tok: DestinationTokenInfo) => {\n const chainId = tempChain ?? tok.chainId;\n if (!chainId) return;\n onSelect(chainId as SUPPORTED_CHAINS_IDS, tok);\n };\n\n return (\n
\n
\n {\n const matchedChain = chainsWithTokens.find(\n (chain) => String(chain) === value,\n );\n if (matchedChain) {\n setTempChain(matchedChain);\n }\n }}\n >\n
\n
\n \n setSearchQuery(e.target.value)}\n className=\"bg-transparent w-full text-foreground text-base font-medium outline-none transition-all duration-150 placeholder-muted-foreground proportional-nums disabled:opacity-80\"\n />\n {searchQuery && (\n setSearchQuery(\"\")}\n className=\"p-0.5 hover:bg-muted rounded-full transition-colors\"\n >\n \n \n )}\n
\n \n {tempChain ? (\n \n ) : (\n
\n \n
\n )}\n
\n
\n \n \n {chainsWithTokens.map((c) => (\n \n
\n \n {CHAIN_METADATA[c].name}\n
\n
\n ))}\n
\n
\n \n

\n {tempChain\n ? `Tokens on ${SHORT_CHAIN_NAME[tempChain]}`\n : \"All Tokens\"}\n

\n
\n
\n {displayedTokens.length > 0 ? (\n displayedTokens.map((t) => (\n \n handlePick(t)}\n className=\"flex items-center justify-between gap-x-2 p-2 rounded w-full h-max\"\n >\n
\n {t.symbol ? (\n
\n \n
\n ) : null}\n
\n
\n

{t.balance}

\n

\n {t.balanceInFiat}\n

\n
\n \n
\n ))\n ) : (\n

No Tokens Found

\n )}\n
\n
\n
\n
\n );\n};\n\nexport default DestinationAssetSelect;\n", + "content": "\"use client\";\nimport { type FC, useMemo, useState } from \"react\";\nimport { Button } from \"../../ui/button\";\nimport { type TokenBalance } from \"@avail-project/nexus-sdk-v2\";\nimport { formatTokenBalance } from \"@avail-project/nexus-sdk-v2/utils\";\nimport { DESTINATION_SWAP_TOKENS } from \"../config/destination\";\nimport { DialogClose } from \"../../ui/dialog\";\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectTrigger,\n} from \"../../ui/select\";\nimport { Link2, Search, X } from \"lucide-react\";\nimport { SHORT_CHAIN_NAME, usdFormatter } from \"../../common\";\nimport { TokenIcon } from \"./token-icon\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { type DestinationTokenInfo } from \"../hooks/useSwaps\";\n\ninterface DestinationAssetSelectProps {\n swapBalance: TokenBalance[] | null;\n onSelect: (\n chainId: number,\n token: DestinationTokenInfo,\n ) => void;\n}\n\nconst DestinationAssetSelect: FC = ({\n swapBalance,\n onSelect,\n}) => {\n const { nexusSDK, supportedChainsAndTokens } = useNexus();\n const [tempChain, setTempChain] = useState(null);\n const [searchQuery, setSearchQuery] = useState(\"\");\n\n // v2: look up chain name/logo from supportedChainsAndTokens instead of CHAIN_METADATA\n const chainMetaById = useMemo(() => {\n const map = new Map();\n (supportedChainsAndTokens ?? []).forEach((chain) => {\n map.set(chain.id, { name: chain.name, logo: chain.logo ?? \"\" });\n });\n return map;\n }, [supportedChainsAndTokens]);\n const getChainMeta = (id?: number | null) =>\n id != null ? chainMetaById.get(id) ?? { name: \"Chain \"+id, logo: \"\" } : { name: \"\", logo: \"\" };\n\n // Get all tokens from all chains with their chain info\n const allTokens: DestinationTokenInfo[] = useMemo(() => {\n const tokens: DestinationTokenInfo[] = [];\n for (const [chainId, chainTokens] of DESTINATION_SWAP_TOKENS.entries()) {\n for (const token of chainTokens) {\n tokens.push({\n ...token,\n chainId,\n });\n }\n }\n return tokens.map((token) => {\n // v2: chainBalances replaces breakdown; value is a string USD amount\n const balance = swapBalance\n ?.flatMap((asset) => asset.chainBalances ?? [])\n ?.find(\n (chain) =>\n chain.symbol.toUpperCase() === token.symbol.toUpperCase() &&\n chain.chain?.id === token.chainId,\n );\n return {\n ...token,\n balance: formatTokenBalance(balance?.balance ?? \"0\", {\n symbol: balance?.symbol ?? token.symbol,\n decimals: balance?.decimals ?? 0,\n }),\n balanceInFiat: usdFormatter.format(\n Number.parseFloat(balance?.value ?? \"0\"),\n ),\n };\n });\n }, [swapBalance]);\n\n // Only show chains that have tokens\n const chainsWithTokens = useMemo(() => {\n return Array.from(DESTINATION_SWAP_TOKENS.keys());\n }, []);\n\n // Filter tokens by selected chain and search query\n const displayedTokens: DestinationTokenInfo[] = useMemo(() => {\n let filtered = allTokens;\n\n // Filter by chain\n if (tempChain) {\n filtered = filtered.filter((t) => t.chainId === tempChain);\n }\n\n // Filter by search query\n if (searchQuery.trim()) {\n const query = searchQuery.toLowerCase().trim();\n filtered = filtered.filter(\n (t) =>\n t.symbol.toLowerCase().includes(query) ||\n t.name.toLowerCase().includes(query) ||\n t.tokenAddress.toLowerCase().includes(query),\n );\n }\n\n return filtered;\n }, [tempChain, allTokens, searchQuery]);\n\n const handlePick = (tok: DestinationTokenInfo) => {\n const chainId = tempChain ?? tok.chainId;\n if (!chainId) return;\n onSelect(chainId, tok);\n };\n\n return (\n
\n
\n {\n const matchedChain = chainsWithTokens.find(\n (chain: any) => String((chain as any).id) === value,\n );\n if (matchedChain) {\n setTempChain(matchedChain);\n }\n }}\n >\n
\n
\n \n setSearchQuery(e.target.value)}\n className=\"bg-transparent w-full text-foreground text-base font-medium outline-none transition-all duration-150 placeholder-muted-foreground proportional-nums disabled:opacity-80\"\n />\n {searchQuery && (\n setSearchQuery(\"\")}\n className=\"p-0.5 hover:bg-muted rounded-full transition-colors\"\n >\n \n \n )}\n
\n \n {tempChain ? (\n \n ) : (\n
\n \n
\n )}\n
\n
\n \n \n {chainsWithTokens.map((c) => (\n \n
\n \n {getChainMeta(c).name}\n
\n
\n ))}\n
\n
\n \n

\n {tempChain\n ? `Tokens on ${SHORT_CHAIN_NAME[tempChain]}`\n : \"All Tokens\"}\n

\n
\n
\n {displayedTokens.length > 0 ? (\n displayedTokens.map((t) => (\n \n handlePick(t)}\n className=\"flex items-center justify-between gap-x-2 p-2 rounded w-full h-max\"\n >\n
\n {t.symbol ? (\n
\n \n
\n ) : null}\n
\n
\n

{t.balance}

\n

\n {t.balanceInFiat}\n

\n
\n \n
\n ))\n ) : (\n

No Tokens Found

\n )}\n
\n
\n
\n
\n );\n};\n\nexport default DestinationAssetSelect;\n", "type": "registry:component", "target": "components/swaps/components/destination-asset-select.tsx" }, { "path": "registry/nexus-elements/swaps/components/destination-container.tsx", - "content": "import React, { type RefObject, useMemo } from \"react\";\nimport { Label } from \"../../ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport {\n CHAIN_METADATA,\n type OnSwapIntentHookData,\n type SUPPORTED_CHAINS_IDS,\n type UserAsset,\n} from \"@avail-project/nexus-core\";\nimport {\n type SwapInputs,\n type SwapMode,\n type TransactionStatus,\n} from \"../hooks/useSwaps\";\nimport { Button } from \"../../ui/button\";\nimport { TokenIcon } from \"./token-icon\";\nimport AmountInput from \"./amount-input\";\nimport { usdFormatter } from \"../../common\";\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from \"../../ui/dialog\";\nimport { ChevronDown } from \"lucide-react\";\nimport DestinationAssetSelect from \"./destination-asset-select\";\nimport { TOKEN_IMAGES } from \"../config/destination\";\n\ninterface DestinationContainerProps {\n destinationHovered: boolean;\n inputs: SwapInputs;\n swapIntent: RefObject;\n destinationBalance?: UserAsset[\"breakdown\"][0];\n swapBalance: UserAsset[] | null;\n availableStables: UserAsset[];\n swapMode: SwapMode;\n status: TransactionStatus;\n setInputs: (inputs: Partial) => void;\n setSwapMode: (mode: SwapMode) => void;\n getFiatValue: (amount: number, token: string) => number;\n formatBalance: (\n balance?: string | number,\n symbol?: string,\n decimals?: number\n ) => string | undefined;\n}\n\ntype AssetBreakdownWithOptionalIcon = UserAsset[\"breakdown\"][number] & {\n icon?: string;\n};\n\nconst DestinationContainer: React.FC = ({\n destinationHovered,\n inputs,\n swapIntent,\n destinationBalance,\n swapBalance,\n availableStables,\n swapMode,\n status,\n setInputs,\n setSwapMode,\n getFiatValue,\n formatBalance,\n}) => {\n // In exactOut mode, show user's input; in exactIn mode, show calculated destination\n const displayedAmount =\n swapMode === \"exactOut\"\n ? inputs.toAmount ?? \"\"\n : formatBalance(\n swapIntent?.current?.intent?.destination?.amount,\n swapIntent?.current?.intent?.destination?.token?.symbol,\n swapIntent?.current?.intent?.destination?.token?.decimals\n ) ?? \"\";\n\n const quickPickTokens = useMemo(\n () =>\n availableStables\n .map((token) => {\n const breakdown =\n token.breakdown?.find(\n (entry) => Number.parseFloat(entry.balance ?? \"0\") > 0,\n ) ?? token.breakdown?.[0];\n if (!breakdown) return null;\n return { token, breakdown };\n })\n .filter(\n (\n item,\n ): item is {\n token: UserAsset;\n breakdown: UserAsset[\"breakdown\"][number];\n } => item !== null,\n ),\n [availableStables],\n );\n\n return (\n
\n
\n \n {(!inputs?.toToken || !inputs?.toChainID) && (\n \n {quickPickTokens.map(({ token, breakdown }) => (\n {\n const normalizedSymbol = breakdown.symbol.toUpperCase();\n const breakdownIcon = (\n breakdown as AssetBreakdownWithOptionalIcon\n ).icon;\n const tokenLogo =\n breakdownIcon ||\n TOKEN_IMAGES[breakdown.symbol] ||\n TOKEN_IMAGES[normalizedSymbol] ||\n token.icon ||\n \"\";\n setInputs({\n ...inputs,\n toToken: {\n tokenAddress: breakdown.contractAddress,\n decimals: breakdown.decimals ?? token.decimals,\n logo: tokenLogo,\n name: breakdown.symbol,\n symbol: breakdown.symbol,\n },\n toChainID: breakdown.chain.id as SUPPORTED_CHAINS_IDS,\n });\n }}\n className=\"bg-transparent rounded-full hover:-translate-y-1 hover:object-scale-down\"\n >\n \n \n ))}\n
\n )}\n
\n
\n {\n setSwapMode(\"exactOut\");\n setInputs({ toAmount: val, fromAmount: undefined });\n }}\n disabled={status === \"simulating\" || status === \"swapping\"}\n />\n \n \n
\n \n {inputs?.toToken?.symbol}\n \n
\n
\n \n \n Select Destination\n \n \n setInputs({ ...inputs, toChainID, toToken })\n }\n />\n \n
\n
\n
\n {swapIntent?.current?.intent?.destination?.amount && inputs?.toToken ? (\n \n {usdFormatter.format(\n getFiatValue(\n Number.parseFloat(\n swapIntent?.current?.intent?.destination?.amount\n ),\n inputs.toToken?.symbol\n )\n )}\n \n ) : (\n \n )}\n {inputs?.toToken ? (\n \n {formatBalance(\n destinationBalance?.balance,\n inputs?.toToken?.symbol,\n destinationBalance?.decimals\n ) ?? \"\"}\n \n ) : (\n \n )}\n
\n \n );\n};\n\nexport default DestinationContainer;\n", + "content": "import React, { type RefObject, useMemo } from \"react\";\nimport { Label } from \"../../ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport {\n type OnSwapIntentHookData,\n type TokenBalance,\n type ChainBalance,\n} from \"@avail-project/nexus-sdk-v2\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport {\n type SwapInputs,\n type SwapMode,\n type TransactionStatus,\n} from \"../hooks/useSwaps\";\nimport { Button } from \"../../ui/button\";\nimport { TokenIcon } from \"./token-icon\";\nimport AmountInput from \"./amount-input\";\nimport { usdFormatter } from \"../../common\";\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from \"../../ui/dialog\";\nimport { ChevronDown } from \"lucide-react\";\nimport DestinationAssetSelect from \"./destination-asset-select\";\nimport { TOKEN_IMAGES } from \"../config/destination\";\n\ninterface DestinationContainerProps {\n destinationHovered: boolean;\n inputs: SwapInputs;\n swapIntent: RefObject;\n destinationBalance?: ChainBalance; // v2: was UserAsset[\"breakdown\"][0]\n swapBalance: TokenBalance[] | null;\n availableStables: TokenBalance[];\n swapMode: SwapMode;\n status: TransactionStatus;\n setInputs: (inputs: Partial) => void;\n setSwapMode: (mode: SwapMode) => void;\n getFiatValue: (amount: number, token: string) => number;\n formatBalance: (\n balance?: string | number,\n symbol?: string,\n decimals?: number\n ) => string | undefined;\n}\n\n// v2: ChainBalance replaces UserAsset[\"breakdown\"][number]\ntype AssetBreakdownWithOptionalIcon = ChainBalance & {\n icon?: string;\n};\n\nconst DestinationContainer: React.FC = ({\n destinationHovered,\n inputs,\n swapIntent,\n destinationBalance,\n swapBalance,\n availableStables,\n swapMode,\n status,\n setInputs,\n setSwapMode,\n getFiatValue,\n formatBalance,\n}) => {\n // In exactOut mode, show user's input; in exactIn mode, show calculated destination\n const displayedAmount =\n swapMode === \"exactOut\"\n ? inputs.toAmount ?? \"\"\n : formatBalance(\n swapIntent?.current?.intent?.destination?.amount,\n swapIntent?.current?.intent?.destination?.token?.symbol,\n swapIntent?.current?.intent?.destination?.token?.decimals\n ) ?? \"\";\n\n const { swapSupportedChainsAndTokens } = useNexus();\n const getChainMeta = (id?: number) =>\n swapSupportedChainsAndTokens?.find((c) => c.id === id) ?? { id: id ?? 0, name: \"\", logo: \"\" };\n\n // v2: quick-pick tokens from chainBalances (replaces breakdown)\n const quickPickTokens = useMemo(\n () =>\n (availableStables ?? [])\n .map((token) => {\n const breakdown =\n token.chainBalances?.find(\n (b) => b.chain.id === inputs?.toChainID,\n ) ?? token.chainBalances?.[0];\n if (!breakdown) return null;\n return { token, breakdown };\n })\n .filter(Boolean) as {\n token: TokenBalance;\n breakdown: ChainBalance;\n }[],\n [availableStables, inputs?.toChainID],\n );\n\n return (\n
\n
\n \n {(!inputs?.toToken || !inputs?.toChainID) && (\n \n {quickPickTokens.map(({ token, breakdown }) => (\n {\n const normalizedSymbol = breakdown.symbol.toUpperCase();\n // v2: ChainBalanceWithIcon uses icon, not logo directly\n const breakdownIcon = (\n breakdown as AssetBreakdownWithOptionalIcon\n ).icon;\n const tokenLogo =\n breakdownIcon ||\n TOKEN_IMAGES[breakdown.symbol] ||\n TOKEN_IMAGES[normalizedSymbol] ||\n token.logo ||\n \"\";\n setInputs({\n ...inputs,\n toToken: {\n tokenAddress: breakdown.contractAddress,\n decimals: breakdown.decimals ?? token.decimals,\n logo: tokenLogo,\n name: breakdown.symbol,\n symbol: breakdown.symbol,\n },\n toChainID: breakdown.chain.id,\n });\n }}\n className=\"bg-transparent rounded-full hover:-translate-y-1 hover:object-scale-down\"\n >\n \n \n ))}\n
\n )}\n
\n
\n {\n setSwapMode(\"exactOut\");\n setInputs({ toAmount: val, fromAmount: undefined });\n }}\n disabled={status === \"simulating\" || status === \"swapping\"}\n />\n \n \n
\n \n {inputs?.toToken?.symbol}\n \n
\n
\n \n \n Select Destination\n \n \n setInputs({ ...inputs, toChainID, toToken })\n }\n />\n \n
\n
\n
\n {swapIntent?.current?.intent?.destination?.amount && inputs?.toToken ? (\n \n {usdFormatter.format(\n getFiatValue(\n Number.parseFloat(\n swapIntent?.current?.intent?.destination?.amount\n ),\n inputs.toToken?.symbol\n )\n )}\n \n ) : (\n \n )}\n {inputs?.toToken ? (\n \n {formatBalance(\n destinationBalance?.balance,\n inputs?.toToken?.symbol,\n destinationBalance?.decimals\n ) ?? \"\"}\n \n ) : (\n \n )}\n
\n \n );\n};\n\nexport default DestinationContainer;\n", "type": "registry:component", "target": "components/swaps/components/destination-container.tsx" }, { "path": "registry/nexus-elements/swaps/components/source-asset-select.tsx", - "content": "\"use client\";\nimport { type FC, useMemo, useState } from \"react\";\nimport { Button } from \"../../ui/button\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport {\n type UserAsset,\n type SUPPORTED_CHAINS_IDS,\n CHAIN_METADATA,\n formatTokenBalance,\n} from \"@avail-project/nexus-core\";\nimport { TOKEN_IMAGES } from \"../config/destination\";\nimport { Link2, Loader2, Search, X } from \"lucide-react\";\nimport { DialogClose } from \"../../ui/dialog\";\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectTrigger,\n} from \"../../ui/select\";\nimport { TokenIcon } from \"./token-icon\";\nimport { SHORT_CHAIN_NAME } from \"../../common\";\nimport { type SourceTokenInfo } from \"../hooks/useSwaps\";\n\ninterface SourceAssetSelectProps {\n onSelect: (chainId: SUPPORTED_CHAINS_IDS, token: SourceTokenInfo) => void;\n swapBalance: UserAsset[] | null;\n}\n\ntype AssetBreakdownWithOptionalIcon = UserAsset[\"breakdown\"][number] & {\n icon?: string;\n};\n\nconst SourceAssetSelect: FC = ({\n onSelect,\n swapBalance,\n}) => {\n const { swapSupportedChainsAndTokens, nexusSDK } = useNexus();\n const [tempChain, setTempChain] = useState<{\n id: number;\n logo: string;\n name: string;\n } | null>(null);\n const [searchQuery, setSearchQuery] = useState(\"\");\n\n // Get all tokens from swapBalance with their chain info\n const allTokens: SourceTokenInfo[] = useMemo(() => {\n if (!swapBalance) return [];\n const tokens: SourceTokenInfo[] = [];\n\n for (const asset of swapBalance) {\n if (!asset?.breakdown?.length) continue;\n for (const breakdown of asset.breakdown) {\n if (Number.parseFloat(breakdown.balance) <= 0) continue;\n const tokenSymbol = breakdown.symbol;\n const normalizedTokenSymbol = tokenSymbol.toUpperCase();\n const breakdownIcon = (breakdown as AssetBreakdownWithOptionalIcon).icon;\n const tokenLogo =\n breakdownIcon ||\n TOKEN_IMAGES[tokenSymbol] ||\n TOKEN_IMAGES[normalizedTokenSymbol] ||\n asset.icon ||\n \"\";\n\n tokens.push({\n contractAddress: breakdown.contractAddress,\n decimals: breakdown.decimals ?? asset.decimals,\n logo: tokenLogo,\n name: tokenSymbol,\n symbol: tokenSymbol,\n balance: formatTokenBalance(breakdown?.balance, {\n symbol: tokenSymbol,\n decimals: breakdown.decimals ?? asset.decimals,\n }),\n balanceInFiat: `$${breakdown.balanceInFiat}`,\n chainId: breakdown.chain?.id,\n });\n }\n }\n\n // Dedupe by contractAddress + chainId\n const unique = new Map();\n for (const t of tokens) {\n const key = `${t.contractAddress.toLowerCase()}-${t.chainId}`;\n unique.set(key, t);\n }\n return Array.from(unique.values());\n }, [swapBalance, nexusSDK]);\n\n // Only show chains that have tokens with balance\n const chainsWithTokens = useMemo(() => {\n if (!swapSupportedChainsAndTokens || !allTokens.length) return [];\n const chainIdsWithTokens = new Set(allTokens.map((t) => t.chainId));\n return swapSupportedChainsAndTokens.filter((c) =>\n chainIdsWithTokens.has(c.id),\n );\n }, [swapSupportedChainsAndTokens, allTokens]);\n\n // Filter tokens by selected chain and search query\n const displayedTokens: SourceTokenInfo[] = useMemo(() => {\n let filtered = allTokens;\n\n // Filter by chain\n if (tempChain) {\n filtered = filtered.filter((t) => t.chainId === tempChain.id);\n }\n\n // Filter by search query\n if (searchQuery.trim()) {\n const query = searchQuery.toLowerCase().trim();\n filtered = filtered.filter(\n (t) =>\n t.symbol.toLowerCase().includes(query) ||\n t.name.toLowerCase().includes(query) ||\n t.contractAddress.toLowerCase().includes(query),\n );\n }\n\n return filtered;\n }, [tempChain, allTokens, searchQuery]);\n\n const handlePick = (tok: SourceTokenInfo) => {\n const chainId = tempChain?.id ?? tok.chainId;\n if (!chainId) return;\n onSelect(chainId as SUPPORTED_CHAINS_IDS, tok);\n };\n\n if (!swapBalance)\n return (\n
\n

\n Fetching swappable assets\n

\n \n
\n );\n\n return (\n
\n {\n const matchedChain = chainsWithTokens.find(\n (chain) => chain.name === value,\n );\n if (matchedChain) {\n setTempChain(matchedChain);\n }\n }}\n >\n
\n
\n \n setSearchQuery(e.target.value)}\n className=\"bg-transparent w-full text-foreground text-base font-medium outline-none transition-all duration-150 placeholder-muted-foreground proportional-nums disabled:opacity-80\"\n />\n {searchQuery && (\n setSearchQuery(\"\")}\n className=\"p-0.5 hover:bg-muted rounded-full transition-colors\"\n >\n \n \n )}\n
\n \n {tempChain ? (\n \n ) : (\n
\n \n
\n )}\n
\n
\n \n \n {chainsWithTokens.map((c) => (\n \n
\n \n {c.name}\n
\n
\n ))}\n
\n
\n \n

\n {tempChain?.id\n ? `Tokens on ${SHORT_CHAIN_NAME[tempChain.id]}`\n : \"All Tokens\"}\n

\n
\n
\n {displayedTokens.length > 0 ? (\n displayedTokens.map((t) => (\n \n handlePick(t)}\n className=\"flex items-center justify-between gap-x-2 p-2 rounded w-full h-max\"\n >\n
\n {t.symbol ? (\n
\n \n
\n ) : null}\n
\n
\n

{t.balance}

\n

\n {t.balanceInFiat}\n

\n
\n \n
\n ))\n ) : (\n

No Tokens Found

\n )}\n
\n
\n
\n );\n};\n\nexport default SourceAssetSelect;\n", + "content": "\"use client\";\nimport { type FC, useMemo, useState } from \"react\";\nimport { Button } from \"../../ui/button\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { type TokenBalance, type ChainBalance } from \"@avail-project/nexus-sdk-v2\";\nimport { formatTokenBalance } from \"@avail-project/nexus-sdk-v2/utils\";\nimport { TOKEN_IMAGES } from \"../config/destination\";\nimport { Link2, Loader2, Search, X } from \"lucide-react\";\nimport { DialogClose } from \"../../ui/dialog\";\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectTrigger,\n} from \"../../ui/select\";\nimport { TokenIcon } from \"./token-icon\";\nimport { SHORT_CHAIN_NAME } from \"../../common\";\nimport { type SourceTokenInfo } from \"../hooks/useSwaps\";\n\ninterface SourceAssetSelectProps {\n onSelect: (chainId: number, token: SourceTokenInfo) => void;\n swapBalance: TokenBalance[] | null;\n}\n\n// v2: ChainBalance replaces UserAsset[\"breakdown\"]\ntype ChainBalanceWithOptionalIcon = ChainBalance & {\n icon?: string;\n};\n\nconst SourceAssetSelect: FC = ({\n onSelect,\n swapBalance,\n}) => {\n const { swapSupportedChainsAndTokens, nexusSDK } = useNexus();\n const [tempChain, setTempChain] = useState<{\n id: number;\n logo: string;\n name: string;\n } | null>(null);\n const [searchQuery, setSearchQuery] = useState(\"\");\n\n // Get all tokens from swapBalance with their chain info\n const allTokens: SourceTokenInfo[] = useMemo(() => {\n if (!swapBalance) return [];\n const tokens: SourceTokenInfo[] = [];\n\n for (const asset of swapBalance) {\n // v2: chainBalances replaces breakdown\n if (!asset?.chainBalances?.length) continue;\n for (const chainBal of asset.chainBalances) {\n if (Number.parseFloat(chainBal.balance) <= 0) continue;\n const tokenSymbol = chainBal.symbol;\n const normalizedTokenSymbol = tokenSymbol.toUpperCase();\n // v2: logo is on chain.logo for ChainBalance; contractAddress is the token address\n const tokenLogo =\n (chainBal as ChainBalanceWithOptionalIcon).icon ||\n chainBal.chain.logo ||\n TOKEN_IMAGES[tokenSymbol] ||\n TOKEN_IMAGES[normalizedTokenSymbol] ||\n asset.logo ||\n \"\";\n\n tokens.push({\n contractAddress: chainBal.contractAddress,\n decimals: chainBal.decimals ?? asset.decimals,\n logo: tokenLogo,\n name: tokenSymbol,\n symbol: tokenSymbol,\n balance: formatTokenBalance(chainBal?.balance, {\n symbol: tokenSymbol,\n decimals: chainBal.decimals ?? asset.decimals,\n }),\n // v2: value is a string USD amount per ChainBalance\n balanceInFiat: `$${Number.parseFloat(chainBal.value ?? \"0\").toFixed(2)}`,\n chainId: chainBal.chain?.id,\n });\n }\n }\n\n // Dedupe by contractAddress + chainId\n const unique = new Map();\n for (const t of tokens) {\n const key = `${t.contractAddress.toLowerCase()}-${t.chainId}`;\n unique.set(key, t);\n }\n return Array.from(unique.values());\n }, [swapBalance, nexusSDK]);\n\n // Only show chains that have tokens with balance\n const chainsWithTokens = useMemo(() => {\n if (!swapSupportedChainsAndTokens || !allTokens.length) return [];\n const chainIdsWithTokens = new Set(allTokens.map((t) => t.chainId));\n return swapSupportedChainsAndTokens.filter((c) =>\n chainIdsWithTokens.has(c.id),\n );\n }, [swapSupportedChainsAndTokens, allTokens]);\n\n // Filter tokens by selected chain and search query\n const displayedTokens: SourceTokenInfo[] = useMemo(() => {\n let filtered = allTokens;\n\n // Filter by chain\n if (tempChain) {\n filtered = filtered.filter((t) => t.chainId === tempChain.id);\n }\n\n // Filter by search query\n if (searchQuery.trim()) {\n const query = searchQuery.toLowerCase().trim();\n filtered = filtered.filter(\n (t) =>\n t.symbol.toLowerCase().includes(query) ||\n t.name.toLowerCase().includes(query) ||\n t.contractAddress.toLowerCase().includes(query),\n );\n }\n\n return filtered;\n }, [tempChain, allTokens, searchQuery]);\n\n const handlePick = (tok: SourceTokenInfo) => {\n const chainId = tempChain?.id ?? tok.chainId;\n if (!chainId) return;\n onSelect(chainId, tok);\n };\n\n if (!swapBalance)\n return (\n
\n

\n Fetching swappable assets\n

\n \n
\n );\n\n return (\n
\n {\n const matchedChain = chainsWithTokens.find(\n (chain) => chain.name === value,\n );\n if (matchedChain) {\n setTempChain(matchedChain);\n }\n }}\n >\n
\n
\n \n setSearchQuery(e.target.value)}\n className=\"bg-transparent w-full text-foreground text-base font-medium outline-none transition-all duration-150 placeholder-muted-foreground proportional-nums disabled:opacity-80\"\n />\n {searchQuery && (\n setSearchQuery(\"\")}\n className=\"p-0.5 hover:bg-muted rounded-full transition-colors\"\n >\n \n \n )}\n
\n \n {tempChain ? (\n \n ) : (\n
\n \n
\n )}\n
\n
\n \n \n {chainsWithTokens.map((c) => (\n \n
\n \n {c.name}\n
\n
\n ))}\n
\n
\n \n

\n {tempChain?.id\n ? `Tokens on ${SHORT_CHAIN_NAME[tempChain.id]}`\n : \"All Tokens\"}\n

\n
\n
\n {displayedTokens.length > 0 ? (\n displayedTokens.map((t) => (\n \n handlePick(t)}\n className=\"flex items-center justify-between gap-x-2 p-2 rounded w-full h-max\"\n >\n
\n {t.symbol ? (\n
\n c.id === (t.chainId ?? 1))?.logo || undefined}\n className=\"border border-border rounded-full\"\n />\n
\n ) : null}\n
\n
\n

{t.balance}

\n

\n {t.balanceInFiat}\n

\n
\n \n
\n ))\n ) : (\n

No Tokens Found

\n )}\n
\n
\n
\n );\n};\n\nexport default SourceAssetSelect;\n", "type": "registry:component", "target": "components/swaps/components/source-asset-select.tsx" }, { "path": "registry/nexus-elements/swaps/components/source-container.tsx", - "content": "import React, { type RefObject } from \"react\";\nimport { Label } from \"../../ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"../../ui/button\";\nimport {\n type TransactionStatus,\n type SwapInputs,\n type SwapMode,\n} from \"../hooks/useSwaps\";\nimport { computeAmountFromFraction, usdFormatter } from \"../../common\";\nimport {\n CHAIN_METADATA,\n type UserAsset,\n type OnSwapIntentHookData,\n} from \"@avail-project/nexus-core\";\nimport AmountInput from \"./amount-input\";\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from \"../../ui/dialog\";\nimport { TokenIcon } from \"./token-icon\";\nimport { ChevronDown } from \"lucide-react\";\nimport SourceAssetSelect from \"./source-asset-select\";\n\nconst RANGE_OPTIONS = [\n {\n label: \"25%\",\n value: 0.25,\n },\n {\n label: \"50%\",\n value: 0.5,\n },\n {\n label: \"75%\",\n value: 0.75,\n },\n {\n label: \"MAX\",\n value: 1,\n },\n];\n\nconst SAFETY_MARGIN = 0.05;\n\ninterface SourceContainerProps {\n status: TransactionStatus;\n sourceHovered: boolean;\n inputs: SwapInputs;\n availableBalance?: UserAsset[\"breakdown\"][0];\n swapBalance: UserAsset[] | null;\n swapMode: SwapMode;\n swapIntent: RefObject;\n setInputs: (inputs: Partial) => void;\n setSwapMode: (mode: SwapMode) => void;\n setTxError: (error: string | null) => void;\n getFiatValue: (amount: number, token: string) => number;\n formatBalance: (\n balance?: string | number,\n symbol?: string,\n decimals?: number\n ) => string | undefined;\n}\n\nconst SourceContainer: React.FC = ({\n status,\n sourceHovered,\n inputs,\n availableBalance,\n swapBalance,\n swapMode,\n swapIntent,\n setInputs,\n setSwapMode,\n setTxError,\n getFiatValue,\n formatBalance,\n}) => {\n const isExactOut = swapMode === \"exactOut\";\n\n // In exactIn mode, show user's input; in exactOut mode, show calculated source from intent\n const displayedAmount =\n swapMode === \"exactIn\"\n ? inputs.fromAmount ?? \"\"\n : formatBalance(\n swapIntent?.current?.intent?.sources?.[0]?.amount,\n swapIntent?.current?.intent?.sources?.[0]?.token?.symbol,\n swapIntent?.current?.intent?.sources?.[0]?.token?.decimals\n ) ?? \"\";\n\n const isDisabled =\n isExactOut || status === \"simulating\" || status === \"swapping\";\n\n // Render exact-out read-only view\n if (isExactOut) {\n return (\n
\n
\n \n
\n
\n

\n Enter destination token, chain and amount.\n
\n We'll calculate the best sources for you.\n

\n
\n
\n );\n }\n\n return (\n
\n
\n \n \n {RANGE_OPTIONS.map((option) => (\n {\n if (!inputs.fromToken) return 0;\n setSwapMode(\"exactIn\");\n const amount = computeAmountFromFraction(\n availableBalance?.balance ?? \"0\",\n option.value,\n inputs?.fromToken?.decimals,\n SAFETY_MARGIN\n );\n setInputs({ fromAmount: amount, toAmount: undefined });\n }}\n className=\"px-5 py-1.5 rounded-full hover:-translate-y-1 hover:object-scale-down\"\n >\n

{option.label}

\n \n ))}\n
\n
\n
\n {\n if (availableBalance?.balance) {\n const parsedAvailableBalance = Number.parseFloat(\n availableBalance?.balance\n );\n const parsedVal = Number.parseFloat(val);\n if (parsedVal > parsedAvailableBalance) {\n setTxError(\"Insufficient Balance\");\n return;\n }\n }\n setSwapMode(\"exactIn\");\n setInputs({ fromAmount: val, toAmount: undefined });\n }}\n disabled={isDisabled}\n />\n\n \n \n \n \n {inputs?.fromToken?.symbol}\n \n
\n \n \n \n Select a Token\n \n \n setInputs({ ...inputs, fromChainID, fromToken })\n }\n swapBalance={swapBalance}\n />\n \n \n \n
\n {inputs.fromAmount && inputs?.fromToken ? (\n \n {usdFormatter.format(\n getFiatValue(\n Number.parseFloat(inputs.fromAmount),\n inputs.fromToken?.symbol\n )\n )}\n \n ) : (\n \n )}\n\n \n {formatBalance(\n availableBalance?.balance ?? \"0\",\n inputs?.fromToken?.symbol,\n availableBalance?.decimals\n )}\n \n
\n \n );\n};\n\nexport default SourceContainer;\n", + "content": "import React, { type RefObject } from \"react\";\nimport { Label } from \"../../ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"../../ui/button\";\nimport {\n type TransactionStatus,\n type SwapInputs,\n type SwapMode,\n} from \"../hooks/useSwaps\";\nimport { computeAmountFromFraction, usdFormatter } from \"../../common\";\nimport {\n type TokenBalance,\n type ChainBalance,\n type OnSwapIntentHookData,\n} from \"@avail-project/nexus-sdk-v2\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport AmountInput from \"./amount-input\";\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from \"../../ui/dialog\";\nimport { TokenIcon } from \"./token-icon\";\nimport { ChevronDown } from \"lucide-react\";\nimport SourceAssetSelect from \"./source-asset-select\";\n\nconst RANGE_OPTIONS = [\n {\n label: \"25%\",\n value: 0.25,\n },\n {\n label: \"50%\",\n value: 0.5,\n },\n {\n label: \"75%\",\n value: 0.75,\n },\n {\n label: \"MAX\",\n value: 1,\n },\n];\n\nconst SAFETY_MARGIN = 0.05;\n\ninterface SourceContainerProps {\n status: TransactionStatus;\n sourceHovered: boolean;\n inputs: SwapInputs;\n availableBalance?: ChainBalance; // v2: was UserAsset[\"breakdown\"][0]\n swapBalance: TokenBalance[] | null;\n swapMode: SwapMode;\n swapIntent: RefObject;\n setInputs: (inputs: Partial) => void;\n setSwapMode: (mode: SwapMode) => void;\n setTxError: (error: string | null) => void;\n getFiatValue: (amount: number, token: string) => number;\n formatBalance: (\n balance?: string | number,\n symbol?: string,\n decimals?: number\n ) => string | undefined;\n}\n\nconst SourceContainer: React.FC = ({\n status,\n sourceHovered,\n inputs,\n availableBalance,\n swapBalance,\n swapMode,\n swapIntent,\n setInputs,\n setSwapMode,\n setTxError,\n getFiatValue,\n formatBalance,\n}) => {\n const { swapSupportedChainsAndTokens } = useNexus();\n const fromChainLogo = swapSupportedChainsAndTokens?.find(\n (c) => c.id === inputs?.fromChainID\n )?.logo || undefined;\n\n const isExactOut = swapMode === \"exactOut\";\n\n // In exactIn mode, show user's input; in exactOut mode, show calculated source from intent\n const displayedAmount =\n swapMode === \"exactIn\"\n ? inputs.fromAmount ?? \"\"\n : formatBalance(\n swapIntent?.current?.intent?.sources?.[0]?.amount,\n swapIntent?.current?.intent?.sources?.[0]?.token?.symbol,\n swapIntent?.current?.intent?.sources?.[0]?.token?.decimals\n ) ?? \"\";\n\n const isDisabled =\n isExactOut || status === \"simulating\" || status === \"swapping\";\n\n // Render exact-out read-only view\n if (isExactOut) {\n return (\n
\n
\n \n
\n
\n

\n Enter destination token, chain and amount.\n
\n We'll calculate the best sources for you.\n

\n
\n
\n );\n }\n\n return (\n
\n
\n \n \n {RANGE_OPTIONS.map((option) => (\n {\n if (!inputs.fromToken) return 0;\n setSwapMode(\"exactIn\");\n const amount = computeAmountFromFraction(\n availableBalance?.balance ?? \"0\",\n option.value,\n inputs?.fromToken?.decimals,\n SAFETY_MARGIN\n );\n setInputs({ fromAmount: amount, toAmount: undefined });\n }}\n className=\"px-5 py-1.5 rounded-full hover:-translate-y-1 hover:object-scale-down\"\n >\n

{option.label}

\n \n ))}\n
\n
\n
\n {\n if (availableBalance?.balance) {\n const parsedAvailableBalance = Number.parseFloat(\n availableBalance?.balance\n );\n const parsedVal = Number.parseFloat(val);\n if (parsedVal > parsedAvailableBalance) {\n setTxError(\"Insufficient Balance\");\n return;\n }\n }\n setSwapMode(\"exactIn\");\n setInputs({ fromAmount: val, toAmount: undefined });\n }}\n disabled={isDisabled}\n />\n\n \n \n \n \n {inputs?.fromToken?.symbol}\n \n
\n \n \n \n Select a Token\n \n \n setInputs({ ...inputs, fromChainID, fromToken })\n }\n swapBalance={swapBalance}\n />\n \n \n \n
\n {inputs.fromAmount && inputs?.fromToken ? (\n \n {usdFormatter.format(\n getFiatValue(\n Number.parseFloat(inputs.fromAmount),\n inputs.fromToken?.symbol\n )\n )}\n \n ) : (\n \n )}\n\n \n {formatBalance(\n availableBalance?.balance ?? \"0\",\n inputs?.fromToken?.symbol,\n availableBalance?.decimals\n )}\n \n
\n \n );\n};\n\nexport default SourceContainer;\n", "type": "registry:component", "target": "components/swaps/components/source-container.tsx" }, @@ -72,19 +72,19 @@ }, { "path": "registry/nexus-elements/swaps/components/transaction-progress.tsx", - "content": "import { type FC, useMemo } from \"react\";\nimport {\n type BridgeStepType,\n type SwapStepType,\n} from \"@avail-project/nexus-core\";\nimport { StepFlow } from \"./step-flow\";\n\nexport type DisplayStep = { id: string; label: string; completed: boolean };\ntype ProgressStep = BridgeStepType | SwapStepType;\n\ninterface TokenSource {\n tokenLogo: string;\n chainLogo: string;\n symbol: string;\n}\n\ninterface TransactionProgressProps {\n steps: Array<{ id: number; completed: boolean; step: ProgressStep }>;\n explorerUrls: {\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n };\n sourceSymbol: string;\n destinationSymbol: string;\n sourceLogos: {\n token: string;\n chain: string;\n };\n destinationLogos: {\n token: string;\n chain: string;\n };\n hasMultipleSources?: boolean;\n sources?: TokenSource[];\n}\n\nconst STEP_TYPES = {\n INTENT_VERIFICATION: [\"CREATE_PERMIT_FOR_SOURCE_SWAP\"],\n SOURCE_STEP_TYPES: [\n \"CREATE_PERMIT_EOA_TO_EPHEMERAL\",\n \"CREATE_PERMIT_FOR_SOURCE_SWAP\",\n \"SOURCE_SWAP_BATCH_TX\",\n \"SOURCE_SWAP_HASH\",\n ],\n SOURCE_TRANSACTION: [\"SOURCE_SWAP_HASH\", \"SOURCE_SWAP_BATCH_TX\"],\n DESTINATION_STEP_TYPES: [\n \"DESTINATION_SWAP_BATCH_TX\",\n \"DESTINATION_SWAP_HASH\",\n \"SWAP_COMPLETE\",\n ],\n TRANSACTION_COMPLETE: [\"SWAP_COMPLETE\"],\n};\n\nconst TransactionProgress: FC = ({\n steps,\n explorerUrls,\n sourceSymbol,\n destinationSymbol,\n sourceLogos,\n destinationLogos,\n hasMultipleSources,\n sources,\n}) => {\n const { effectiveSteps, currentIndex, allCompleted } = useMemo(() => {\n const completedTypes = new Set(\n steps?.filter((s) => s?.completed).map((s) => s?.step?.type)\n );\n // Consider only steps that were actually emitted by the SDK (ignore pre-seeded placeholders)\n const eventfulTypes = new Set(\n steps\n ?.filter((s) => {\n const st = s?.step ?? {};\n return (\n \"explorerURL\" in st || \"chain\" in st || \"completed\" in st // present when event args were merged into step\n );\n })\n .map((s) => s?.step?.type)\n );\n const hasAny = (types: string[]) =>\n types.some((t) => completedTypes.has(t));\n const sawAny = (types: string[]) => types.some((t) => eventfulTypes.has(t));\n\n const intentVerified = hasAny([\"DETERMINING_SWAP\", \"SWAP_START\"]);\n\n // If the flow does not include SOURCE_* steps, consider it implicitly collected\n\n const collectedOnSources =\n (sawAny(STEP_TYPES.SOURCE_STEP_TYPES) &&\n hasAny(STEP_TYPES.SOURCE_TRANSACTION)) ||\n (!sawAny(STEP_TYPES.SOURCE_STEP_TYPES) &&\n hasAny(STEP_TYPES.DESTINATION_STEP_TYPES));\n\n const filledOnDestination = hasAny(STEP_TYPES.DESTINATION_STEP_TYPES);\n\n const displaySteps: DisplayStep[] = [\n { id: \"intent\", label: \"Intent verified\", completed: intentVerified },\n {\n id: \"collected\",\n label: \"Collected on sources\",\n completed: collectedOnSources,\n },\n {\n id: \"filled\",\n label: \"Filled on destination\",\n completed: filledOnDestination,\n },\n ];\n\n // Mark overall completion ONLY when the SDK reports SWAP_COMPLETE\n const done = hasAny(STEP_TYPES.TRANSACTION_COMPLETE);\n const current = displaySteps.findIndex((st) => !st.completed);\n return {\n effectiveSteps: displaySteps,\n currentIndex: current,\n allCompleted: done,\n };\n }, [steps]);\n\n return (\n
\n \n
\n );\n};\n\nexport default TransactionProgress;\n", + "content": "import { type FC, useMemo } from \"react\";\n// v2: BridgeStepType/SwapStepType removed — use generic record step shape\ntype ProgressStep = { type?: string; typeID?: string; [key: string]: unknown };\n\nimport { StepFlow } from \"./step-flow\";\n\nexport type DisplayStep = { id: string; label: string; completed: boolean };\n\ninterface TokenSource {\n tokenLogo: string;\n chainLogo: string;\n symbol: string;\n}\n\ninterface TransactionProgressProps {\n steps: Array<{ id: number; completed: boolean; step: ProgressStep }>;\n explorerUrls: {\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n };\n sourceSymbol: string;\n destinationSymbol: string;\n sourceLogos: {\n token: string;\n chain: string;\n };\n destinationLogos: {\n token: string;\n chain: string;\n };\n hasMultipleSources?: boolean;\n sources?: TokenSource[];\n}\n\nconst STEP_TYPES = {\n // v2 step type strings (snake_case)\n INTENT_VERIFICATION: [\"bridge_intent_submission\", \"request_signing\"],\n SOURCE_STEP_TYPES: [\n \"eoa_to_ephemeral_transfer\",\n \"source_swap\",\n \"bridge_deposit\",\n \"bridge_intent_submission\",\n ],\n SOURCE_TRANSACTION: [\"source_swap\", \"bridge_deposit\"],\n DESTINATION_STEP_TYPES: [\n \"bridge_fill\",\n \"destination_swap\",\n ],\n TRANSACTION_COMPLETE: [\"bridge_fill\", \"destination_swap\"],\n};\n\nconst TransactionProgress: FC = ({\n steps,\n explorerUrls,\n sourceSymbol,\n destinationSymbol,\n sourceLogos,\n destinationLogos,\n hasMultipleSources,\n sources,\n}) => {\n const { effectiveSteps, currentIndex, allCompleted } = useMemo(() => {\n const completedTypes = new Set(\n steps?.filter((s) => s?.completed).map((s) => s?.step?.type)\n );\n // Consider only steps that were actually emitted by the SDK (ignore pre-seeded placeholders)\n const eventfulTypes = new Set(\n steps\n ?.filter((s) => {\n const st = s?.step ?? {};\n // v2: emitted steps have chain, id, or other properties merged in\n return (\n \"chain\" in st || \"id\" in st || \"explorerURL\" in st || \"completed\" in st\n );\n })\n .map((s) => s?.step?.type)\n );\n const hasAny = (types: string[]) =>\n types.some((t) => completedTypes.has(t));\n const sawAny = (types: string[]) => types.some((t) => eventfulTypes.has(t));\n\n // v2: intent is verified once the bridge_intent_submission step completes,\n // OR implicitly if any source/destination step is already done\n const intentVerified =\n hasAny(STEP_TYPES.INTENT_VERIFICATION) ||\n hasAny(STEP_TYPES.SOURCE_STEP_TYPES) ||\n hasAny(STEP_TYPES.DESTINATION_STEP_TYPES);\n\n // If the flow does not include SOURCE_* steps, consider it implicitly collected\n const collectedOnSources =\n (sawAny(STEP_TYPES.SOURCE_STEP_TYPES) &&\n hasAny(STEP_TYPES.SOURCE_TRANSACTION)) ||\n (!sawAny(STEP_TYPES.SOURCE_STEP_TYPES) &&\n hasAny(STEP_TYPES.DESTINATION_STEP_TYPES));\n\n const filledOnDestination = hasAny(STEP_TYPES.DESTINATION_STEP_TYPES);\n\n const displaySteps: DisplayStep[] = [\n { id: \"intent\", label: \"Intent verified\", completed: intentVerified },\n {\n id: \"collected\",\n label: \"Collected on sources\",\n completed: collectedOnSources,\n },\n {\n id: \"filled\",\n label: \"Filled on destination\",\n completed: filledOnDestination,\n },\n ];\n\n // Mark overall completion ONLY when the SDK reports the final destination step\n const done = hasAny(STEP_TYPES.TRANSACTION_COMPLETE);\n const current = displaySteps.findIndex((st) => !st.completed);\n return {\n effectiveSteps: displaySteps,\n currentIndex: current,\n allCompleted: done,\n };\n }, [steps]);\n\n return (\n
\n \n
\n );\n};\n\nexport default TransactionProgress;\n", "type": "registry:component", "target": "components/swaps/components/transaction-progress.tsx" }, { "path": "registry/nexus-elements/swaps/components/view-transaction.tsx", - "content": "import React, { FC, type RefObject, useMemo, useState } from \"react\";\nimport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogHeader,\n} from \"../../ui/dialog\";\nimport {\n type SwapStepType,\n type OnSwapIntentHookData,\n formatTokenBalance,\n} from \"@avail-project/nexus-core\";\nimport { ChevronDown, ChevronUp, Info, MoveDown, XIcon } from \"lucide-react\";\nimport { TokenIcon } from \"./token-icon\";\nimport { StackedTokenIcons } from \"./stacked-token-icons\";\nimport {\n type GenericStep,\n formatUsdForDisplay,\n usdFormatter,\n} from \"../../common\";\nimport { TOKEN_IMAGES } from \"../config/destination\";\nimport { Button } from \"../../ui/button\";\nimport {\n type ExactOutSourceOption,\n type SwapMode,\n type TransactionStatus,\n} from \"../hooks/useSwaps\";\nimport { getIntentMatchedOptionKeys } from \"../utils/source-matching\";\nimport TransactionProgress from \"./transaction-progress\";\nimport { Separator } from \"../../ui/separator\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"../../ui/accordion\";\nimport { Checkbox } from \"../../ui/checkbox\";\nimport { cn } from \"@/lib/utils\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../../ui/tooltip\";\n\nfunction parseNonNegativeNumber(value: unknown): number {\n const parsed = Number.parseFloat(String(value));\n if (!Number.isFinite(parsed) || parsed < 0) return 0;\n return parsed;\n}\n\nfunction formatFeeUsd(amountUsd: number): string {\n if (amountUsd > 0 && amountUsd < 0.001) {\n return \"< $0.001\";\n }\n return formatUsdForDisplay(amountUsd);\n}\n\nfunction formatSignedUsd(value: number): string {\n if (!Number.isFinite(value) || value === 0) return \"$0.00\";\n const sign = value < 0 ? \"-\" : \"+\";\n const absolute = Math.abs(value);\n const absoluteLabel =\n absolute < 0.001 ? \"< $0.001\" : formatUsdForDisplay(absolute);\n return `${sign}${absoluteLabel}`;\n}\n\nfunction formatImpactPercent(value: number): string {\n if (!Number.isFinite(value) || value === 0) return \"0%\";\n const absolute = Math.abs(value);\n if (absolute < 0.01) {\n return \"< 0.01%\";\n }\n const fixed = absolute.toFixed(2);\n return `${fixed.replace(/\\.?0+$/, \"\")}%`;\n}\n\ninterface ViewTransactionProps {\n steps: GenericStep[];\n status: TransactionStatus;\n swapMode: SwapMode;\n swapIntent: RefObject;\n getFiatValue: (amount: number, token: string) => number;\n continueSwap: () => void | Promise;\n exactOutSourceOptions: ExactOutSourceOption[];\n exactOutSelectedKeys: string[];\n toggleExactOutSource: (key: string) => void;\n isExactOutSourceSelectionDirty: boolean;\n updatingExactOutSources: boolean;\n explorerUrls: {\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n };\n reset: () => void;\n txError: string | null;\n}\n\ninterface TokenBreakdownProps\n extends Omit<\n ViewTransactionProps,\n | \"swapIntent\"\n | \"continueSwap\"\n | \"status\"\n | \"explorerUrls\"\n | \"steps\"\n | \"reset\"\n | \"txError\"\n | \"swapMode\"\n | \"nexusSDK\"\n | \"exactOutSourceOptions\"\n | \"exactOutSelectedKeys\"\n | \"toggleExactOutSource\"\n | \"isExactOutSourceSelectionDirty\"\n | \"updatingExactOutSources\"\n > {\n tokenLogo: string;\n chainLogo: string;\n symbol: string;\n amount: number;\n decimals: number;\n}\n\nconst TokenBreakdown = ({\n getFiatValue,\n tokenLogo,\n chainLogo,\n symbol,\n amount,\n decimals,\n}: TokenBreakdownProps) => {\n return (\n
\n
\n

\n {formatTokenBalance(amount, {\n symbol: symbol,\n decimals: decimals,\n })}\n

\n

\n {usdFormatter.format(getFiatValue(amount, symbol))}\n

\n
\n \n
\n );\n};\n\ninterface MultiSourceBreakdownProps {\n getFiatValue: (amount: number, token: string) => number;\n sources: NonNullable[\"sources\"];\n}\n\nconst MultiSourceBreakdown = ({\n getFiatValue,\n sources,\n}: MultiSourceBreakdownProps) => {\n // Calculate summed USD value across all sources\n const totalUsdValue = useMemo(() => {\n return sources.reduce((sum, source) => {\n const amount = Number.parseFloat(source.amount);\n const fiatValue = getFiatValue(amount, source.token.symbol);\n return sum + fiatValue;\n }, 0);\n }, [sources, getFiatValue]);\n\n // Prepare sources for stacked icons\n const stackedSources = useMemo(() => {\n return sources.map((source) => ({\n tokenLogo: TOKEN_IMAGES[source.token.symbol] ?? \"\",\n chainLogo: source.chain.logo,\n symbol: source.token.symbol,\n }));\n }, [sources]);\n\n return (\n
\n
\n

\n {sources.length} source{sources.length > 1 ? \"s\" : \"\"}\n

\n

\n {usdFormatter.format(totalUsdValue)}\n

\n
\n \n
\n );\n};\n\nconst ViewTransaction: FC = ({\n steps,\n status,\n swapMode,\n swapIntent,\n getFiatValue,\n continueSwap,\n exactOutSourceOptions,\n exactOutSelectedKeys,\n toggleExactOutSource,\n isExactOutSourceSelectionDirty,\n updatingExactOutSources,\n explorerUrls,\n reset,\n txError,\n}) => {\n const transactionIntent = swapIntent.current?.intent;\n const [showFeeDetails, setShowFeeDetails] = useState(false);\n const [showPriceImpactDetails, setShowPriceImpactDetails] = useState(false);\n const sources = useMemo(\n () => transactionIntent?.sources ?? [],\n [transactionIntent?.sources],\n );\n const hasSources = sources.length > 0;\n const hasMultipleSources = sources.length > 1;\n const usedSourceKeys = useMemo(\n () => getIntentMatchedOptionKeys(sources, exactOutSourceOptions),\n [sources, exactOutSourceOptions],\n );\n const usedSourceKeySet = useMemo(\n () => new Set(usedSourceKeys),\n [usedSourceKeys],\n );\n const { usedSourceOptions, otherSourceOptions } = useMemo(() => {\n const usedOrder = new Map(\n usedSourceKeys.map((key, index) => [key, index] as const),\n );\n const used: ExactOutSourceOption[] = [];\n const other: ExactOutSourceOption[] = [];\n for (const opt of exactOutSourceOptions) {\n if (usedSourceKeySet.has(opt.key)) {\n used.push(opt);\n } else {\n other.push(opt);\n }\n }\n used.sort((a, b) => {\n const aOrder = usedOrder.get(a.key) ?? Number.MAX_SAFE_INTEGER;\n const bOrder = usedOrder.get(b.key) ?? Number.MAX_SAFE_INTEGER;\n return aOrder - bOrder;\n });\n return { usedSourceOptions: used, otherSourceOptions: other };\n }, [exactOutSourceOptions, usedSourceKeySet, usedSourceKeys]);\n\n // Prepare source info for TransactionProgress\n const sourceInfo = useMemo(() => {\n if (!hasSources || sources.length === 0) {\n return {\n symbol: \"Multiple assets\",\n logos: { token: \"\", chain: \"\" },\n };\n }\n if (hasMultipleSources) {\n return {\n symbol: `${sources.length} sources`,\n logos: {\n token: TOKEN_IMAGES[sources[0].token.symbol] ?? \"\",\n chain: sources[0].chain.logo,\n },\n };\n }\n return {\n symbol: sources[0].token.symbol,\n logos: {\n token: TOKEN_IMAGES[sources[0].token.symbol] ?? \"\",\n chain: sources[0].chain.logo,\n },\n };\n }, [sources, hasSources, hasMultipleSources]);\n\n const shouldShowExactOutSourceSelection =\n status === \"simulating\" && swapMode === \"exactOut\";\n\n const feeBreakdown = useMemo(() => {\n const feesAndBuffer = transactionIntent?.feesAndBuffer;\n const bridgeRaw = feesAndBuffer?.bridge;\n const caGasUsd = parseNonNegativeNumber(bridgeRaw?.caGas);\n const collectionUsd = parseNonNegativeNumber(\n (bridgeRaw as Record | undefined)?.collection,\n );\n const fulfilmentUsd = parseNonNegativeNumber(\n (bridgeRaw as Record | undefined)?.fulfilment,\n );\n const gasSuppliedUsd = parseNonNegativeNumber(\n (bridgeRaw as Record | undefined)\n ?.gasSupplied,\n );\n const protocolFeeUsd = parseNonNegativeNumber(bridgeRaw?.protocol);\n const solverFeeUsd = parseNonNegativeNumber(bridgeRaw?.solver);\n\n const hasBridgeBreakdown = Boolean(bridgeRaw);\n const executionBridgeUsd = collectionUsd + fulfilmentUsd + gasSuppliedUsd;\n const gasSponsorshipUsd = hasBridgeBreakdown ? caGasUsd : 0;\n\n const gasAmount = parseNonNegativeNumber(\n transactionIntent?.destination?.gas?.amount,\n );\n const gasSymbol = transactionIntent?.destination?.gas?.token?.symbol;\n const destinationGasUsd =\n gasAmount > 0 && gasSymbol\n ? parseNonNegativeNumber(getFiatValue(gasAmount, gasSymbol))\n : 0;\n const executionGasFeeUsd = hasBridgeBreakdown\n ? executionBridgeUsd\n : destinationGasUsd;\n\n const bridgeComponentTotal = Object.entries(bridgeRaw ?? {})\n .filter(([key]) => key !== \"total\")\n .reduce((sum, [, value]) => sum + parseNonNegativeNumber(value), 0);\n const bridgeExplicitTotal = parseNonNegativeNumber(bridgeRaw?.total);\n const bridgeUsd =\n bridgeExplicitTotal > 0 ? bridgeExplicitTotal : bridgeComponentTotal;\n const knownBridgeRowsUsd =\n gasSponsorshipUsd +\n executionGasFeeUsd +\n protocolFeeUsd +\n solverFeeUsd;\n const otherBridgeFeeUsd = Math.max(0, bridgeUsd - knownBridgeRowsUsd);\n\n const bufferUsd = parseNonNegativeNumber(feesAndBuffer?.buffer);\n const totalFeeUsd =\n gasSponsorshipUsd +\n executionGasFeeUsd +\n protocolFeeUsd +\n solverFeeUsd +\n otherBridgeFeeUsd;\n const intentSpendUsd = sources.reduce((sum, source) => {\n const amount = parseNonNegativeNumber(source.amount);\n const fiatValue = getFiatValue(amount, source.token.symbol);\n return sum + parseNonNegativeNumber(fiatValue);\n }, 0);\n const destinationAmount = parseNonNegativeNumber(\n transactionIntent?.destination?.amount,\n );\n const destinationSymbol = transactionIntent?.destination?.token?.symbol;\n const destinationValueUsd =\n destinationAmount > 0 && destinationSymbol\n ? parseNonNegativeNumber(\n getFiatValue(destinationAmount, destinationSymbol),\n )\n : 0;\n const swapImpactUsd =\n destinationValueUsd - intentSpendUsd - totalFeeUsd - bufferUsd;\n const maxPriceImpactUsd = swapImpactUsd + bufferUsd;\n const spendBaseUsd = intentSpendUsd - totalFeeUsd - bufferUsd;\n const swapImpactPercent =\n spendBaseUsd > 0 ? (swapImpactUsd / spendBaseUsd) * 100 : 0;\n const maxPriceImpactPercent =\n spendBaseUsd > 0 ? (maxPriceImpactUsd / spendBaseUsd) * 100 : 0;\n\n return {\n totalFeeUsd,\n gasSponsorshipUsd,\n executionGasFeeUsd,\n protocolFeeUsd,\n solverFeeUsd,\n otherBridgeFeeUsd,\n bridgeUsd,\n bufferUsd,\n swapImpactUsd,\n swapImpactPercent,\n maxPriceImpactUsd,\n maxPriceImpactPercent,\n };\n }, [transactionIntent, getFiatValue, sources]);\n\n const feeDetailRows = useMemo(\n () =>\n [\n { label: \"Gas sponsorship\", amountUsd: feeBreakdown.gasSponsorshipUsd },\n {\n label: \"Execution Gas fee\",\n amountUsd:\n feeBreakdown.executionGasFeeUsd + feeBreakdown.otherBridgeFeeUsd,\n },\n { label: \"Protocol fee\", amountUsd: feeBreakdown.protocolFeeUsd },\n { label: \"Solver fee\", amountUsd: feeBreakdown.solverFeeUsd },\n ],\n [feeBreakdown],\n );\n\n const showFeeBreakdown = feeDetailRows.length > 0;\n const showPriceImpactBreakdown =\n Math.abs(feeBreakdown.maxPriceImpactUsd) > 0 ||\n Math.abs(feeBreakdown.swapImpactUsd) > 0 ||\n feeBreakdown.bufferUsd > 0;\n\n const exactOutSelectedTotalUsd = useMemo(() => {\n if (!shouldShowExactOutSourceSelection) return 0;\n if (!exactOutSourceOptions.length || !exactOutSelectedKeys.length) return 0;\n\n const selectedSet = new Set(exactOutSelectedKeys);\n return exactOutSourceOptions.reduce((sum, opt) => {\n if (!selectedSet.has(opt.key)) return sum;\n const balance = Number.parseFloat(opt.balance);\n if (!Number.isFinite(balance) || balance <= 0) return sum;\n const fiatValue = getFiatValue(balance, opt.tokenSymbol);\n if (!Number.isFinite(fiatValue) || fiatValue <= 0) return sum;\n return sum + fiatValue;\n }, 0);\n }, [\n shouldShowExactOutSourceSelection,\n exactOutSourceOptions,\n exactOutSelectedKeys,\n getFiatValue,\n ]);\n\n const exactOutRequiredUsd = useMemo(() => {\n if (!shouldShowExactOutSourceSelection) return 0;\n const amount = Number.parseFloat(\n transactionIntent?.destination?.amount ?? \"0\",\n );\n if (!Number.isFinite(amount) || amount <= 0) return 0;\n const symbol = transactionIntent?.destination?.token?.symbol;\n if (!symbol) return 0;\n const base = getFiatValue(amount, symbol);\n if (!Number.isFinite(base) || base <= 0) return 0;\n return base;\n }, [shouldShowExactOutSourceSelection, transactionIntent, getFiatValue]);\n\n const isExactOutSourceSelectionInsufficient = useMemo(() => {\n if (!shouldShowExactOutSourceSelection) return false;\n if (exactOutRequiredUsd <= 0) return false;\n return exactOutSelectedTotalUsd < exactOutRequiredUsd;\n }, [\n shouldShowExactOutSourceSelection,\n exactOutRequiredUsd,\n exactOutSelectedTotalUsd,\n ]);\n\n const continueLabel = !hasSources\n ? \"Waiting for sources...\"\n : updatingExactOutSources\n ? \"Updating sources...\"\n : shouldShowExactOutSourceSelection && isExactOutSourceSelectionDirty\n ? \"Update sources\"\n : \"Continue\";\n\n if (!transactionIntent) return null;\n\n return (\n {\n if (!open) {\n reset();\n }\n }}\n >\n \n \n

\n You're Swapping\n

\n \n \n \n
\n
\n {/* Source section - handle empty, single, and multiple sources */}\n {!hasSources ? (\n
\n

\n Calculating sources...\n

\n
\n ) : hasMultipleSources ? (\n \n ) : (\n \n )}\n \n \n
\n {(showFeeBreakdown || showPriceImpactBreakdown) && (\n
\n {showFeeBreakdown && (\n
\n
\n

Total fees

\n

\n {formatUsdForDisplay(feeBreakdown.totalFeeUsd)}\n

\n
\n
\n setShowFeeDetails(!showFeeDetails)}\n >\n View details\n {showFeeDetails ? (\n \n ) : (\n \n )}\n \n
\n {showFeeDetails && (\n
\n {feeDetailRows.map((row) => (\n \n

{row.label}

\n

\n {formatFeeUsd(row.amountUsd)}\n

\n
\n ))}\n
\n )}\n
\n )}\n\n {showPriceImpactBreakdown && (\n
\n
\n
\n

Max price impact

\n \n \n \n \n \n \n \n Includes a small buffer to ensure your swaps succeed.\n Excess funds are refunded after deducting swap fees and\n price impact.\n \n \n
\n

\n {formatSignedUsd(feeBreakdown.maxPriceImpactUsd)} (\n {formatImpactPercent(feeBreakdown.maxPriceImpactPercent)})\n

\n
\n
\n

\n Includes a buffer to ensure swaps succeed. Excess funds are\n refunded after deducting fees and impact.\n

\n \n setShowPriceImpactDetails(!showPriceImpactDetails)\n }\n >\n View details\n {showPriceImpactDetails ? (\n \n ) : (\n \n )}\n \n
\n {showPriceImpactDetails && (\n
\n
\n

Swap impact

\n

\n {formatSignedUsd(feeBreakdown.swapImpactUsd)} (\n {formatImpactPercent(feeBreakdown.swapImpactPercent)})\n

\n
\n
\n

Swap buffer

\n

\n {formatFeeUsd(feeBreakdown.bufferUsd)}\n

\n
\n
\n )}\n
\n )}\n \n )}\n {status === \"error\" && (\n

{txError}

\n )}\n {shouldShowExactOutSourceSelection &&\n exactOutSourceOptions.length > 0 && (\n \n \n \n
\n

Choose sources

\n

\n {exactOutSelectedKeys.length} selected\n

\n
\n
\n \n {isExactOutSourceSelectionInsufficient && (\n
\n Insufficient selected sources balance. Selected{\" \"}\n \n {usdFormatter.format(exactOutSelectedTotalUsd)}\n \n , need at least{\" \"}\n \n {usdFormatter.format(exactOutRequiredUsd)}\n {\" \"}\n (required for {transactionIntent?.destination?.amount}{\" \"}\n {transactionIntent?.destination?.token.symbol}).\n
\n )}\n

\n {updatingExactOutSources\n ? \"Updating sources…\"\n : isExactOutSourceSelectionDirty\n ? \"Changes apply when you press Update sources.\"\n : \"Press Continue to proceed with these sources.\"}\n

\n
\n {usedSourceOptions.map((opt) => {\n const isSelected = exactOutSelectedKeys.includes(opt.key);\n const isLastSelected =\n isSelected && exactOutSelectedKeys.length === 1;\n const isUsed = usedSourceKeySet.has(opt.key);\n const tokenLogo =\n opt.tokenLogo || TOKEN_IMAGES[opt.tokenSymbol] || \"\";\n const formattedBalance =\n formatTokenBalance(opt.balance, {\n symbol: opt.tokenSymbol,\n decimals: opt.decimals,\n }) ?? `${opt.balance} ${opt.tokenSymbol}`;\n\n return (\n {\n if (isLastSelected || updatingExactOutSources) {\n return;\n }\n toggleExactOutSource(opt.key);\n }}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (isLastSelected || updatingExactOutSources) {\n return;\n }\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n toggleExactOutSource(opt.key);\n }\n }}\n >\n
\n {\n if (isLastSelected || updatingExactOutSources) {\n return;\n }\n toggleExactOutSource(opt.key);\n }}\n onClick={(e) => e.stopPropagation()}\n aria-label={`Select ${opt.tokenSymbol} on ${opt.chainName} as a source`}\n />\n \n
\n

\n {opt.tokenSymbol}\n

\n

\n {opt.chainName}\n

\n
\n
\n\n
\n

\n {formattedBalance}\n

\n {isUsed && (\n

\n Currently used\n

\n )}\n
\n
\n );\n })}\n {otherSourceOptions.length > 0 &&\n usedSourceOptions.length > 0 && (\n \n )}\n {otherSourceOptions.map((opt) => {\n const isSelected = exactOutSelectedKeys.includes(opt.key);\n const isLastSelected =\n isSelected && exactOutSelectedKeys.length === 1;\n const isUsed = usedSourceKeySet.has(opt.key);\n const tokenLogo =\n opt.tokenLogo || TOKEN_IMAGES[opt.tokenSymbol] || \"\";\n const formattedBalance =\n formatTokenBalance(opt.balance, {\n symbol: opt.tokenSymbol,\n decimals: opt.decimals,\n }) ?? `${opt.balance} ${opt.tokenSymbol}`;\n\n return (\n {\n if (isLastSelected || updatingExactOutSources) {\n return;\n }\n toggleExactOutSource(opt.key);\n }}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (isLastSelected || updatingExactOutSources) {\n return;\n }\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n toggleExactOutSource(opt.key);\n }\n }}\n >\n
\n {\n if (isLastSelected || updatingExactOutSources) {\n return;\n }\n toggleExactOutSource(opt.key);\n }}\n onClick={(e) => e.stopPropagation()}\n aria-label={`Select ${opt.tokenSymbol} on ${opt.chainName} as a source`}\n />\n \n
\n

\n {opt.tokenSymbol}\n

\n

\n {opt.chainName}\n

\n
\n
\n\n
\n

\n {formattedBalance}\n

\n {isUsed && (\n

\n Currently used\n

\n )}\n
\n \n );\n })}\n \n

\n Select at least 1 source.\n

\n
\n
\n
\n )}\n {status === \"simulating\" && (\n void continueSwap()}\n disabled={\n !hasSources ||\n updatingExactOutSources ||\n (shouldShowExactOutSourceSelection &&\n isExactOutSourceSelectionInsufficient)\n }\n >\n {continueLabel}\n \n )}\n\n {(status === \"swapping\" || status === \"success\") && (\n <>\n \n ({\n tokenLogo: TOKEN_IMAGES[s.token.symbol] ?? \"\",\n chainLogo: s.chain.logo,\n symbol: s.token.symbol,\n }))\n : undefined\n }\n />\n \n )}\n
\n \n );\n};\n\nexport default ViewTransaction;\n", + "content": "import React, { FC, type RefObject, useMemo, useState } from \"react\";\nimport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogHeader,\n} from \"../../ui/dialog\";\nimport { type OnSwapIntentHookData } from \"@avail-project/nexus-sdk-v2\";\nimport { formatTokenBalance } from \"@avail-project/nexus-sdk-v2/utils\";\nimport { ChevronDown, ChevronUp, Info, MoveDown, XIcon } from \"lucide-react\";\nimport { TokenIcon } from \"./token-icon\";\nimport { StackedTokenIcons } from \"./stacked-token-icons\";\nimport {\n type GenericStep,\n formatUsdForDisplay,\n usdFormatter,\n} from \"../../common\";\nimport { TOKEN_IMAGES } from \"../config/destination\";\nimport { Button } from \"../../ui/button\";\nimport {\n type ExactOutSourceOption,\n type SwapMode,\n type TransactionStatus,\n} from \"../hooks/useSwaps\";\nimport { getIntentMatchedOptionKeys } from \"../utils/source-matching\";\nimport TransactionProgress from \"./transaction-progress\";\nimport { Separator } from \"../../ui/separator\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"../../ui/accordion\";\nimport { Checkbox } from \"../../ui/checkbox\";\nimport { cn } from \"@/lib/utils\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../../ui/tooltip\";\n\nfunction parseNonNegativeNumber(value: unknown): number {\n const parsed = Number.parseFloat(String(value));\n if (!Number.isFinite(parsed) || parsed < 0) return 0;\n return parsed;\n}\n\nfunction formatFeeUsd(amountUsd: number): string {\n if (amountUsd > 0 && amountUsd < 0.001) {\n return \"< $0.001\";\n }\n return formatUsdForDisplay(amountUsd);\n}\n\nfunction formatSignedUsd(value: number): string {\n if (!Number.isFinite(value) || value === 0) return \"$0.00\";\n const sign = value < 0 ? \"-\" : \"+\";\n const absolute = Math.abs(value);\n const absoluteLabel =\n absolute < 0.001 ? \"< $0.001\" : formatUsdForDisplay(absolute);\n return `${sign}${absoluteLabel}`;\n}\n\nfunction formatImpactPercent(value: number): string {\n if (!Number.isFinite(value) || value === 0) return \"0%\";\n const absolute = Math.abs(value);\n if (absolute < 0.01) {\n return \"< 0.01%\";\n }\n const fixed = absolute.toFixed(2);\n return `${fixed.replace(/\\.?0+$/, \"\")}%`;\n}\n\ninterface ViewTransactionProps {\n // v2: SwapStepType renamed to SwapPlanStep; use generic record for GenericStep\n steps: GenericStep<{ type?: string; [key: string]: unknown }>[];\n status: TransactionStatus;\n swapMode: SwapMode;\n swapIntent: RefObject;\n getFiatValue: (amount: number, token: string) => number;\n continueSwap: () => void | Promise;\n exactOutSourceOptions: ExactOutSourceOption[];\n exactOutSelectedKeys: string[];\n toggleExactOutSource: (key: string) => void;\n isExactOutSourceSelectionDirty: boolean;\n updatingExactOutSources: boolean;\n explorerUrls: {\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n };\n reset: () => void;\n txError: string | null;\n}\n\ninterface TokenBreakdownProps extends Omit<\n ViewTransactionProps,\n | \"swapIntent\"\n | \"continueSwap\"\n | \"status\"\n | \"explorerUrls\"\n | \"steps\"\n | \"reset\"\n | \"txError\"\n | \"swapMode\"\n | \"nexusSDK\"\n | \"exactOutSourceOptions\"\n | \"exactOutSelectedKeys\"\n | \"toggleExactOutSource\"\n | \"isExactOutSourceSelectionDirty\"\n | \"updatingExactOutSources\"\n> {\n tokenLogo: string;\n chainLogo: string;\n symbol: string;\n amount: number;\n decimals: number;\n}\n\nconst TokenBreakdown = ({\n getFiatValue,\n tokenLogo,\n chainLogo,\n symbol,\n amount,\n decimals,\n}: TokenBreakdownProps) => {\n return (\n
\n
\n

\n {formatTokenBalance(amount, {\n symbol: symbol,\n decimals: decimals,\n })}\n

\n

\n {usdFormatter.format(getFiatValue(amount, symbol))}\n

\n
\n \n
\n );\n};\n\ninterface MultiSourceBreakdownProps {\n getFiatValue: (amount: number, token: string) => number;\n sources: NonNullable[\"sources\"];\n}\n\nconst MultiSourceBreakdown = ({\n getFiatValue,\n sources,\n}: MultiSourceBreakdownProps) => {\n // Calculate summed USD value across all sources\n const totalUsdValue = useMemo(() => {\n return sources.reduce((sum, source) => {\n const amount = Number.parseFloat(source.amount);\n const fiatValue = getFiatValue(amount, source.token.symbol);\n return sum + fiatValue;\n }, 0);\n }, [sources, getFiatValue]);\n\n // Prepare sources for stacked icons\n const stackedSources = useMemo(() => {\n return sources.map((source) => ({\n tokenLogo: TOKEN_IMAGES[source.token.symbol] ?? \"\",\n chainLogo: source.chain.logo,\n symbol: source.token.symbol,\n }));\n }, [sources]);\n\n return (\n
\n
\n

\n {sources.length} source{sources.length > 1 ? \"s\" : \"\"}\n

\n

\n {usdFormatter.format(totalUsdValue)}\n

\n
\n \n
\n );\n};\n\nconst ViewTransaction: FC = ({\n steps,\n status,\n swapMode,\n swapIntent,\n getFiatValue,\n continueSwap,\n exactOutSourceOptions,\n exactOutSelectedKeys,\n toggleExactOutSource,\n isExactOutSourceSelectionDirty,\n updatingExactOutSources,\n explorerUrls,\n reset,\n txError,\n}) => {\n const transactionIntent = swapIntent.current?.intent;\n const [showFeeDetails, setShowFeeDetails] = useState(false);\n const [showPriceImpactDetails, setShowPriceImpactDetails] = useState(false);\n const sources = useMemo(\n () => transactionIntent?.sources ?? [],\n [transactionIntent?.sources],\n );\n const hasSources = sources.length > 0;\n const hasMultipleSources = sources.length > 1;\n const usedSourceKeys = useMemo(\n () => getIntentMatchedOptionKeys(sources, exactOutSourceOptions),\n [sources, exactOutSourceOptions],\n );\n const usedSourceKeySet = useMemo(\n () => new Set(usedSourceKeys),\n [usedSourceKeys],\n );\n const { usedSourceOptions, otherSourceOptions } = useMemo(() => {\n const usedOrder = new Map(\n usedSourceKeys.map((key, index) => [key, index] as const),\n );\n const used: ExactOutSourceOption[] = [];\n const other: ExactOutSourceOption[] = [];\n for (const opt of exactOutSourceOptions) {\n if (usedSourceKeySet.has(opt.key)) {\n used.push(opt);\n } else {\n other.push(opt);\n }\n }\n used.sort((a, b) => {\n const aOrder = usedOrder.get(a.key) ?? Number.MAX_SAFE_INTEGER;\n const bOrder = usedOrder.get(b.key) ?? Number.MAX_SAFE_INTEGER;\n return aOrder - bOrder;\n });\n return { usedSourceOptions: used, otherSourceOptions: other };\n }, [exactOutSourceOptions, usedSourceKeySet, usedSourceKeys]);\n\n // Prepare source info for TransactionProgress\n const sourceInfo = useMemo(() => {\n if (!hasSources || sources.length === 0) {\n return {\n symbol: \"Multiple assets\",\n logos: { token: \"\", chain: \"\" },\n };\n }\n if (hasMultipleSources) {\n return {\n symbol: `${sources.length} sources`,\n logos: {\n token: TOKEN_IMAGES[sources[0].token.symbol] ?? \"\",\n chain: sources[0].chain.logo,\n },\n };\n }\n return {\n symbol: sources[0].token.symbol,\n logos: {\n token: TOKEN_IMAGES[sources[0].token.symbol] ?? \"\",\n chain: sources[0].chain.logo,\n },\n };\n }, [sources, hasSources, hasMultipleSources]);\n\n const shouldShowExactOutSourceSelection =\n status === \"simulating\" && swapMode === \"exactOut\";\n\n const feeBreakdown = useMemo(() => {\n const feesAndBuffer = transactionIntent?.feesAndBuffer;\n const bridgeRaw = feesAndBuffer?.bridge;\n const caGasUsd = parseNonNegativeNumber(bridgeRaw?.caGas);\n const collectionUsd = parseNonNegativeNumber(\n (bridgeRaw as Record | undefined)?.collection,\n );\n const fulfilmentUsd = parseNonNegativeNumber(\n (bridgeRaw as Record | undefined)?.fulfilment,\n );\n const gasSuppliedUsd = parseNonNegativeNumber(\n (bridgeRaw as Record | undefined)\n ?.gasSupplied,\n );\n const protocolFeeUsd = parseNonNegativeNumber(bridgeRaw?.protocol);\n const solverFeeUsd = parseNonNegativeNumber(bridgeRaw?.solver);\n\n const hasBridgeBreakdown = Boolean(bridgeRaw);\n const executionBridgeUsd = collectionUsd + fulfilmentUsd + gasSuppliedUsd;\n const gasSponsorshipUsd = hasBridgeBreakdown ? caGasUsd : 0;\n\n const gasAmount = parseNonNegativeNumber(\n transactionIntent?.destination?.gas?.amount,\n );\n const gasSymbol = transactionIntent?.destination?.gas?.token?.symbol;\n const destinationGasUsd =\n gasAmount > 0 && gasSymbol\n ? parseNonNegativeNumber(getFiatValue(gasAmount, gasSymbol))\n : 0;\n const executionGasFeeUsd = hasBridgeBreakdown\n ? executionBridgeUsd\n : destinationGasUsd;\n\n const bridgeComponentTotal = Object.entries(bridgeRaw ?? {})\n .filter(([key]) => key !== \"total\")\n .reduce((sum, [, value]) => sum + parseNonNegativeNumber(value), 0);\n const bridgeExplicitTotal = parseNonNegativeNumber(bridgeRaw?.total);\n const bridgeUsd =\n bridgeExplicitTotal > 0 ? bridgeExplicitTotal : bridgeComponentTotal;\n const knownBridgeRowsUsd =\n gasSponsorshipUsd + executionGasFeeUsd + protocolFeeUsd + solverFeeUsd;\n const otherBridgeFeeUsd = Math.max(0, bridgeUsd - knownBridgeRowsUsd);\n\n const bufferUsd = parseNonNegativeNumber(feesAndBuffer?.buffer);\n const totalFeeUsd =\n gasSponsorshipUsd +\n executionGasFeeUsd +\n protocolFeeUsd +\n solverFeeUsd +\n otherBridgeFeeUsd;\n const intentSpendUsd = sources.reduce((sum, source) => {\n const amount = parseNonNegativeNumber(source.amount);\n const fiatValue = getFiatValue(amount, source.token.symbol);\n return sum + parseNonNegativeNumber(fiatValue);\n }, 0);\n const destinationAmount = parseNonNegativeNumber(\n transactionIntent?.destination?.amount,\n );\n const destinationSymbol = transactionIntent?.destination?.token?.symbol;\n const destinationValueUsd =\n destinationAmount > 0 && destinationSymbol\n ? parseNonNegativeNumber(\n getFiatValue(destinationAmount, destinationSymbol),\n )\n : 0;\n const swapImpactUsd =\n destinationValueUsd - intentSpendUsd - totalFeeUsd - bufferUsd;\n const maxPriceImpactUsd = swapImpactUsd + bufferUsd;\n const spendBaseUsd = intentSpendUsd - totalFeeUsd - bufferUsd;\n const swapImpactPercent =\n spendBaseUsd > 0 ? (swapImpactUsd / spendBaseUsd) * 100 : 0;\n const maxPriceImpactPercent =\n spendBaseUsd > 0 ? (maxPriceImpactUsd / spendBaseUsd) * 100 : 0;\n\n return {\n totalFeeUsd,\n gasSponsorshipUsd,\n executionGasFeeUsd,\n protocolFeeUsd,\n solverFeeUsd,\n otherBridgeFeeUsd,\n bridgeUsd,\n bufferUsd,\n swapImpactUsd,\n swapImpactPercent,\n maxPriceImpactUsd,\n maxPriceImpactPercent,\n };\n }, [transactionIntent, getFiatValue, sources]);\n\n const feeDetailRows = useMemo(\n () => [\n { label: \"Gas sponsorship\", amountUsd: feeBreakdown.gasSponsorshipUsd },\n {\n label: \"Execution Gas fee\",\n amountUsd:\n feeBreakdown.executionGasFeeUsd + feeBreakdown.otherBridgeFeeUsd,\n },\n { label: \"Protocol fee\", amountUsd: feeBreakdown.protocolFeeUsd },\n { label: \"Solver fee\", amountUsd: feeBreakdown.solverFeeUsd },\n ],\n [feeBreakdown],\n );\n\n const showFeeBreakdown = feeDetailRows.length > 0;\n const showPriceImpactBreakdown =\n Math.abs(feeBreakdown.maxPriceImpactUsd) > 0 ||\n Math.abs(feeBreakdown.swapImpactUsd) > 0 ||\n feeBreakdown.bufferUsd > 0;\n\n const exactOutSelectedTotalUsd = useMemo(() => {\n if (!shouldShowExactOutSourceSelection) return 0;\n if (!exactOutSourceOptions.length || !exactOutSelectedKeys.length) return 0;\n\n const selectedSet = new Set(exactOutSelectedKeys);\n return exactOutSourceOptions.reduce((sum, opt) => {\n if (!selectedSet.has(opt.key)) return sum;\n const balance = Number.parseFloat(opt.balance);\n if (!Number.isFinite(balance) || balance <= 0) return sum;\n const fiatValue = getFiatValue(balance, opt.tokenSymbol);\n if (!Number.isFinite(fiatValue) || fiatValue <= 0) return sum;\n return sum + fiatValue;\n }, 0);\n }, [\n shouldShowExactOutSourceSelection,\n exactOutSourceOptions,\n exactOutSelectedKeys,\n getFiatValue,\n ]);\n\n const exactOutRequiredUsd = useMemo(() => {\n if (!shouldShowExactOutSourceSelection) return 0;\n const amount = Number.parseFloat(\n transactionIntent?.destination?.amount ?? \"0\",\n );\n if (!Number.isFinite(amount) || amount <= 0) return 0;\n const symbol = transactionIntent?.destination?.token?.symbol;\n if (!symbol) return 0;\n const base = getFiatValue(amount, symbol);\n if (!Number.isFinite(base) || base <= 0) return 0;\n return base;\n }, [shouldShowExactOutSourceSelection, transactionIntent, getFiatValue]);\n\n const isExactOutSourceSelectionInsufficient = useMemo(() => {\n if (!shouldShowExactOutSourceSelection) return false;\n if (exactOutRequiredUsd <= 0) return false;\n return exactOutSelectedTotalUsd < exactOutRequiredUsd;\n }, [\n shouldShowExactOutSourceSelection,\n exactOutRequiredUsd,\n exactOutSelectedTotalUsd,\n ]);\n\n const continueLabel = !hasSources\n ? \"Waiting for sources...\"\n : updatingExactOutSources\n ? \"Updating sources...\"\n : shouldShowExactOutSourceSelection && isExactOutSourceSelectionDirty\n ? \"Update sources\"\n : \"Continue\";\n\n if (!transactionIntent) return null;\n\n return (\n {\n if (!open) {\n reset();\n }\n }}\n >\n \n \n

\n You're Swapping\n

\n \n \n \n
\n
\n {/* Source section - handle empty, single, and multiple sources */}\n {!hasSources ? (\n
\n

\n Calculating sources...\n

\n
\n ) : hasMultipleSources ? (\n \n ) : (\n \n )}\n \n \n
\n {(showFeeBreakdown || showPriceImpactBreakdown) && (\n
\n {showFeeBreakdown && (\n
\n
\n

Total fees

\n

\n {formatUsdForDisplay(feeBreakdown.totalFeeUsd)}\n

\n
\n
\n setShowFeeDetails(!showFeeDetails)}\n >\n View details\n {showFeeDetails ? (\n \n ) : (\n \n )}\n \n
\n {showFeeDetails && (\n
\n {feeDetailRows.map((row) => (\n \n

{row.label}

\n

\n {formatFeeUsd(row.amountUsd)}\n

\n
\n ))}\n
\n )}\n
\n )}\n\n {showPriceImpactBreakdown && (\n
\n
\n
\n

Max price impact

\n \n \n \n \n \n \n \n Includes a small buffer to ensure your swaps succeed.\n Excess funds are refunded after deducting swap fees and\n price impact.\n \n \n
\n

\n {formatSignedUsd(feeBreakdown.maxPriceImpactUsd)} (\n {formatImpactPercent(feeBreakdown.maxPriceImpactPercent)})\n

\n
\n
\n

\n Includes a buffer to ensure swaps succeed. Excess funds are\n refunded after deducting fees and impact.\n

\n \n setShowPriceImpactDetails(!showPriceImpactDetails)\n }\n >\n View details\n {showPriceImpactDetails ? (\n \n ) : (\n \n )}\n \n
\n {showPriceImpactDetails && (\n
\n
\n

Swap impact

\n

\n {formatSignedUsd(feeBreakdown.swapImpactUsd)} (\n {formatImpactPercent(feeBreakdown.swapImpactPercent)})\n

\n
\n
\n

Swap buffer

\n

\n {formatFeeUsd(feeBreakdown.bufferUsd)}\n

\n
\n
\n )}\n
\n )}\n \n )}\n {status === \"error\" && (\n

{txError}

\n )}\n {shouldShowExactOutSourceSelection &&\n exactOutSourceOptions.length > 0 && (\n \n \n \n
\n

Choose sources

\n

\n {exactOutSelectedKeys.length} selected\n

\n
\n
\n \n {isExactOutSourceSelectionInsufficient && (\n
\n Insufficient selected sources balance. Selected{\" \"}\n \n {usdFormatter.format(exactOutSelectedTotalUsd)}\n \n , need at least{\" \"}\n \n {usdFormatter.format(exactOutRequiredUsd)}\n {\" \"}\n (required for {transactionIntent?.destination?.amount}{\" \"}\n {transactionIntent?.destination?.token.symbol}).\n
\n )}\n

\n {updatingExactOutSources\n ? \"Updating sources…\"\n : isExactOutSourceSelectionDirty\n ? \"Changes apply when you press Update sources.\"\n : \"Press Continue to proceed with these sources.\"}\n

\n
\n {usedSourceOptions.map((opt) => {\n const isSelected = exactOutSelectedKeys.includes(opt.key);\n const isLastSelected =\n isSelected && exactOutSelectedKeys.length === 1;\n const isUsed = usedSourceKeySet.has(opt.key);\n const tokenLogo =\n opt.tokenLogo || TOKEN_IMAGES[opt.tokenSymbol] || \"\";\n const formattedBalance =\n formatTokenBalance(opt.balance, {\n symbol: opt.tokenSymbol,\n decimals: opt.decimals,\n }) ?? `${opt.balance} ${opt.tokenSymbol}`;\n\n return (\n {\n if (isLastSelected || updatingExactOutSources) {\n return;\n }\n toggleExactOutSource(opt.key);\n }}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (isLastSelected || updatingExactOutSources) {\n return;\n }\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n toggleExactOutSource(opt.key);\n }\n }}\n >\n
\n {\n if (isLastSelected || updatingExactOutSources) {\n return;\n }\n toggleExactOutSource(opt.key);\n }}\n onClick={(e) => e.stopPropagation()}\n aria-label={`Select ${opt.tokenSymbol} on ${opt.chainName} as a source`}\n />\n \n
\n

\n {opt.tokenSymbol}\n

\n

\n {opt.chainName}\n

\n
\n
\n\n
\n

\n {formattedBalance}\n

\n {isUsed && (\n

\n Currently used\n

\n )}\n
\n
\n );\n })}\n {otherSourceOptions.length > 0 &&\n usedSourceOptions.length > 0 && (\n \n )}\n {otherSourceOptions.map((opt) => {\n const isSelected = exactOutSelectedKeys.includes(opt.key);\n const isLastSelected =\n isSelected && exactOutSelectedKeys.length === 1;\n const isUsed = usedSourceKeySet.has(opt.key);\n const tokenLogo =\n opt.tokenLogo || TOKEN_IMAGES[opt.tokenSymbol] || \"\";\n const formattedBalance =\n formatTokenBalance(opt.balance, {\n symbol: opt.tokenSymbol,\n decimals: opt.decimals,\n }) ?? `${opt.balance} ${opt.tokenSymbol}`;\n\n return (\n {\n if (isLastSelected || updatingExactOutSources) {\n return;\n }\n toggleExactOutSource(opt.key);\n }}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (isLastSelected || updatingExactOutSources) {\n return;\n }\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n toggleExactOutSource(opt.key);\n }\n }}\n >\n
\n {\n if (isLastSelected || updatingExactOutSources) {\n return;\n }\n toggleExactOutSource(opt.key);\n }}\n onClick={(e) => e.stopPropagation()}\n aria-label={`Select ${opt.tokenSymbol} on ${opt.chainName} as a source`}\n />\n \n
\n

\n {opt.tokenSymbol}\n

\n

\n {opt.chainName}\n

\n
\n
\n\n
\n

\n {formattedBalance}\n

\n {isUsed && (\n

\n Currently used\n

\n )}\n
\n \n );\n })}\n \n

\n Select at least 1 source.\n

\n
\n
\n
\n )}\n {status === \"simulating\" && (\n void continueSwap()}\n disabled={\n !hasSources ||\n updatingExactOutSources ||\n (shouldShowExactOutSourceSelection &&\n isExactOutSourceSelectionInsufficient)\n }\n >\n {continueLabel}\n \n )}\n\n {(status === \"swapping\" || status === \"success\") && (\n <>\n \n ({\n tokenLogo: TOKEN_IMAGES[s.token.symbol] ?? \"\",\n chainLogo: s.chain.logo,\n symbol: s.token.symbol,\n }))\n : undefined\n }\n />\n \n )}\n
\n \n );\n};\n\nexport default ViewTransaction;\n", "type": "registry:component", "target": "components/swaps/components/view-transaction.tsx" }, { "path": "registry/nexus-elements/swaps/config/destination.ts", - "content": "import { SUPPORTED_CHAINS } from \"@avail-project/nexus-core\";\n\nexport const DESTINATION_SWAP_TOKENS = new Map<\n number,\n {\n decimals: number;\n logo: string;\n name: string;\n symbol: string;\n tokenAddress: `0x${string}`;\n }[]\n>([\n [\n SUPPORTED_CHAINS.OPTIMISM,\n [\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628\",\n name: \"Ether\",\n symbol: \"ETH\",\n tokenAddress: \"0x0000000000000000000000000000000000000000\",\n },\n {\n decimals: 6,\n logo: \"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694\",\n name: \"USD Coin\",\n symbol: \"USDC\",\n tokenAddress: \"0x0b2c639c533813f4aa9d7837caf62653d097ff85\",\n },\n {\n decimals: 6,\n logo: \"https://coin-images.coingecko.com/coins/images/35023/large/USDT.png\",\n name: \"USDT Coin\",\n symbol: \"USDT\",\n tokenAddress: \"0x94b008aa00579c1307b0ef2c499ad98a8ce58e58\",\n },\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/25244/large/Optimism.png?1696524385\",\n name: \"Optimism\",\n symbol: \"OP\",\n tokenAddress: \"0x4200000000000000000000000000000000000042\",\n },\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/12645/large/AAVE.png?1696512452\",\n name: \"Aave Token\",\n symbol: \"AAVE\",\n tokenAddress: \"0x76fb31fb4af56892a25e32cfc43de717950c9278\",\n },\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/12504/large/uni.jpg?1696512319\",\n name: \"Uniswap\",\n symbol: \"UNI\",\n tokenAddress: \"0x6fd9d7ad17242c41f7131d257212c54a0e816691\",\n },\n ],\n ],\n [\n SUPPORTED_CHAINS.ETHEREUM,\n [\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628\",\n name: \"Ether\",\n symbol: \"ETH\",\n tokenAddress: \"0x0000000000000000000000000000000000000000\",\n },\n {\n decimals: 6,\n logo: \"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694\",\n name: \"USD Coin\",\n symbol: \"USDC\",\n tokenAddress: \"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48\",\n },\n ],\n ],\n [\n SUPPORTED_CHAINS.MONAD,\n [\n {\n decimals: 18,\n logo: \"https://raw.githubusercontent.com/availproject/nexus-assets/main/chains/monad/logo.png\",\n name: \"Monad\",\n symbol: \"MON\",\n tokenAddress: \"0x0000000000000000000000000000000000000000\",\n },\n {\n decimals: 6,\n logo: \"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694\",\n name: \"USD Coin\",\n symbol: \"USDC\",\n tokenAddress: \"0x754704Bc059F8C67012fEd69BC8A327a5aafb603\",\n },\n ],\n ],\n [\n SUPPORTED_CHAINS.MEGAETH,\n [\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628\",\n name: \"Ether\",\n symbol: \"ETH\",\n tokenAddress: \"0x0000000000000000000000000000000000000000\",\n },\n {\n decimals: 18,\n logo: \"https://raw.githubusercontent.com/availproject/nexus-assets/main/tokens/usdm/logo.png\",\n name: \"USDm Coin\",\n symbol: \"USDM\",\n tokenAddress: \"0xFAfDdbb3FC7688494971a79cc65DCa3EF82079E7\",\n },\n ],\n ],\n [\n SUPPORTED_CHAINS.ARBITRUM,\n [\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628\",\n name: \"Ether\",\n symbol: \"ETH\",\n tokenAddress: \"0x0000000000000000000000000000000000000000\",\n },\n {\n decimals: 6,\n logo: \"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694\",\n name: \"USD Coin\",\n symbol: \"USDC\",\n tokenAddress: \"0xaf88d065e77c8cc2239327c5edb3a432268e5831\",\n },\n {\n decimals: 6,\n logo: \"https://coin-images.coingecko.com/coins/images/35023/large/USDT.png\",\n name: \"USDT Coin\",\n symbol: \"USDT\",\n tokenAddress: \"0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9\",\n },\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/29850/large/pepe-token.jpeg?1696528776\",\n name: \"Pepe\",\n symbol: \"PEPE\",\n tokenAddress: \"0x25d887ce7a35172c62febfd67a1856f20faebb00\",\n },\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/13573/large/Lido_DAO.png?1696513326\",\n name: \"Lido DAO Token\",\n symbol: \"LDO\",\n tokenAddress: \"0x13ad51ed4f1b7e9dc168d8a00cb3f4ddd85efa60\",\n },\n ],\n ],\n [\n SUPPORTED_CHAINS.SCROLL,\n [\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628\",\n name: \"Ether\",\n symbol: \"ETH\",\n tokenAddress: \"0x0000000000000000000000000000000000000000\",\n },\n {\n decimals: 6,\n logo: \"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694\",\n name: \"USD Coin\",\n symbol: \"USDC\",\n tokenAddress: \"0x06efdbff2a14a7c8e15944d1f4a48f9f95f663a4\",\n },\n {\n decimals: 6,\n logo: \"https://coin-images.coingecko.com/coins/images/35023/large/USDT.png\",\n name: \"USDT Coin\",\n symbol: \"USDT\",\n tokenAddress: \"0xf55bec9cafdbe8730f096aa55dad6d22d44099df\",\n },\n ],\n ],\n [\n SUPPORTED_CHAINS.BASE,\n [\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628\",\n name: \"Ether\",\n symbol: \"ETH\",\n tokenAddress: \"0x0000000000000000000000000000000000000000\",\n },\n {\n decimals: 6,\n logo: \"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694\",\n name: \"USD Coin\",\n symbol: \"USDC\",\n tokenAddress: \"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913\",\n },\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/9956/large/Badge_Dai.png?1696509996\",\n name: \"Dai Stablecoin\",\n symbol: \"DAI\",\n tokenAddress: \"0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb\",\n },\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/28206/large/ftxG9_TJ_400x400.jpeg?1696527208\",\n name: \"LayerZero\",\n symbol: \"ZRO\",\n tokenAddress: \"0x6985884c4392d348587b19cb9eaaf157f13271cd\",\n },\n {\n decimals: 18,\n logo: \"https://assets.coingecko.com/coins/images/12151/standard/OM_Token.png?1696511991\",\n name: \"MANTRA\",\n symbol: \"OM\",\n tokenAddress: \"0x3992b27da26848c2b19cea6fd25ad5568b68ab98\",\n },\n {\n decimals: 18,\n logo: \"https://assets.coingecko.com/coins/images/54411/standard/Qm4DW488_400x400.jpg\",\n name: \"KAITO\",\n symbol: \"KAITO\",\n tokenAddress: \"0x98d0baa52b2d063e780de12f615f963fe8537553\",\n },\n ],\n ],\n [\n SUPPORTED_CHAINS.BNB,\n [\n {\n decimals: 18,\n logo: \"https://assets.coingecko.com/coins/images/825/large/bnb-icon2_2x.png\",\n name: \"BNB\",\n symbol: \"BNB\",\n tokenAddress: \"0x0000000000000000000000000000000000000000\",\n },\n ],\n ],\n]);\n\nexport const TOKEN_IMAGES: Record = {\n USDC: \"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png\",\n USDT: \"https://coin-images.coingecko.com/coins/images/35023/large/USDT.png\",\n USDM: \"https://raw.githubusercontent.com/availproject/nexus-assets/main/tokens/usdm/logo.png\",\n \"USD₮0\":\n \"https://coin-images.coingecko.com/coins/images/35023/large/USDT.png\",\n WETH: \"https://assets.coingecko.com/coins/images/279/large/ethereum.png?1595348880\",\n USDS: \"https://assets.coingecko.com/coins/images/39926/standard/usds.webp?1726666683\",\n SOPH: \"https://assets.coingecko.com/coins/images/38680/large/sophon_logo_200.png\",\n KAIA: \"https://assets.coingecko.com/asset_platforms/images/9672/large/kaia.png\",\n BNB: \"https://assets.coingecko.com/coins/images/825/large/bnb-icon2_2x.png\",\n // Add ETH as fallback for any ETH-related tokens\n ETH: \"https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628\",\n // Add common token fallbacks\n POL: \"https://coin-images.coingecko.com/coins/images/32440/standard/polygon.png\",\n AVAX: \"https://assets.coingecko.com/coins/images/12559/standard/Avalanche_Circle_RedWhite_Trans.png\",\n FUEL: \"https://coin-images.coingecko.com/coins/images/279/large/ethereum.png\",\n HYPE: \"https://assets.coingecko.com/asset_platforms/images/243/large/hyperliquid.png\",\n // Popular swap tokens\n DAI: \"https://coin-images.coingecko.com/coins/images/9956/large/Badge_Dai.png?1696509996\",\n UNI: \"https://coin-images.coingecko.com/coins/images/12504/large/uni.jpg?1696512319\",\n AAVE: \"https://coin-images.coingecko.com/coins/images/12645/large/AAVE.png?1696512452\",\n LDO: \"https://coin-images.coingecko.com/coins/images/13573/large/Lido_DAO.png?1696513326\",\n PEPE: \"https://coin-images.coingecko.com/coins/images/29850/large/pepe-token.jpeg?1696528776\",\n OP: \"https://coin-images.coingecko.com/coins/images/25244/large/Optimism.png?1696524385\",\n ZRO: \"https://coin-images.coingecko.com/coins/images/28206/large/ftxG9_TJ_400x400.jpeg?1696527208\",\n OM: \"https://assets.coingecko.com/coins/images/12151/standard/OM_Token.png?1696511991\",\n KAITO:\n \"https://assets.coingecko.com/coins/images/54411/standard/Qm4DW488_400x400.jpg\",\n};\n", + "content": "const SUPPORTED_CHAINS = { OPTIMISM: 10, ETHEREUM: 1, MONAD: 143, MEGAETH: 4326, ARBITRUM: 42161, SCROLL: 534352, BASE: 8453, BNB: 56 } as const;\n\nexport const DESTINATION_SWAP_TOKENS = new Map<\n number,\n {\n decimals: number;\n logo: string;\n name: string;\n symbol: string;\n tokenAddress: `0x${string}`;\n }[]\n>([\n [\n SUPPORTED_CHAINS.OPTIMISM,\n [\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628\",\n name: \"Ether\",\n symbol: \"ETH\",\n tokenAddress: \"0x0000000000000000000000000000000000000000\",\n },\n {\n decimals: 6,\n logo: \"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694\",\n name: \"USD Coin\",\n symbol: \"USDC\",\n tokenAddress: \"0x0b2c639c533813f4aa9d7837caf62653d097ff85\",\n },\n {\n decimals: 6,\n logo: \"https://coin-images.coingecko.com/coins/images/35023/large/USDT.png\",\n name: \"USDT Coin\",\n symbol: \"USDT\",\n tokenAddress: \"0x94b008aa00579c1307b0ef2c499ad98a8ce58e58\",\n },\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/25244/large/Optimism.png?1696524385\",\n name: \"Optimism\",\n symbol: \"OP\",\n tokenAddress: \"0x4200000000000000000000000000000000000042\",\n },\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/12645/large/AAVE.png?1696512452\",\n name: \"Aave Token\",\n symbol: \"AAVE\",\n tokenAddress: \"0x76fb31fb4af56892a25e32cfc43de717950c9278\",\n },\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/12504/large/uni.jpg?1696512319\",\n name: \"Uniswap\",\n symbol: \"UNI\",\n tokenAddress: \"0x6fd9d7ad17242c41f7131d257212c54a0e816691\",\n },\n ],\n ],\n [\n SUPPORTED_CHAINS.ETHEREUM,\n [\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628\",\n name: \"Ether\",\n symbol: \"ETH\",\n tokenAddress: \"0x0000000000000000000000000000000000000000\",\n },\n {\n decimals: 6,\n logo: \"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694\",\n name: \"USD Coin\",\n symbol: \"USDC\",\n tokenAddress: \"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48\",\n },\n ],\n ],\n [\n SUPPORTED_CHAINS.MONAD,\n [\n {\n decimals: 18,\n logo: \"https://raw.githubusercontent.com/availproject/nexus-assets/main/chains/monad/logo.png\",\n name: \"Monad\",\n symbol: \"MON\",\n tokenAddress: \"0x0000000000000000000000000000000000000000\",\n },\n {\n decimals: 6,\n logo: \"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694\",\n name: \"USD Coin\",\n symbol: \"USDC\",\n tokenAddress: \"0x754704Bc059F8C67012fEd69BC8A327a5aafb603\",\n },\n ],\n ],\n [\n SUPPORTED_CHAINS.MEGAETH,\n [\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628\",\n name: \"Ether\",\n symbol: \"ETH\",\n tokenAddress: \"0x0000000000000000000000000000000000000000\",\n },\n {\n decimals: 18,\n logo: \"https://raw.githubusercontent.com/availproject/nexus-assets/main/tokens/usdm/logo.png\",\n name: \"USDm Coin\",\n symbol: \"USDM\",\n tokenAddress: \"0xFAfDdbb3FC7688494971a79cc65DCa3EF82079E7\",\n },\n ],\n ],\n [\n SUPPORTED_CHAINS.ARBITRUM,\n [\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628\",\n name: \"Ether\",\n symbol: \"ETH\",\n tokenAddress: \"0x0000000000000000000000000000000000000000\",\n },\n {\n decimals: 6,\n logo: \"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694\",\n name: \"USD Coin\",\n symbol: \"USDC\",\n tokenAddress: \"0xaf88d065e77c8cc2239327c5edb3a432268e5831\",\n },\n {\n decimals: 6,\n logo: \"https://coin-images.coingecko.com/coins/images/35023/large/USDT.png\",\n name: \"USDT Coin\",\n symbol: \"USDT\",\n tokenAddress: \"0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9\",\n },\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/29850/large/pepe-token.jpeg?1696528776\",\n name: \"Pepe\",\n symbol: \"PEPE\",\n tokenAddress: \"0x25d887ce7a35172c62febfd67a1856f20faebb00\",\n },\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/13573/large/Lido_DAO.png?1696513326\",\n name: \"Lido DAO Token\",\n symbol: \"LDO\",\n tokenAddress: \"0x13ad51ed4f1b7e9dc168d8a00cb3f4ddd85efa60\",\n },\n ],\n ],\n [\n SUPPORTED_CHAINS.SCROLL,\n [\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628\",\n name: \"Ether\",\n symbol: \"ETH\",\n tokenAddress: \"0x0000000000000000000000000000000000000000\",\n },\n {\n decimals: 6,\n logo: \"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694\",\n name: \"USD Coin\",\n symbol: \"USDC\",\n tokenAddress: \"0x06efdbff2a14a7c8e15944d1f4a48f9f95f663a4\",\n },\n {\n decimals: 6,\n logo: \"https://coin-images.coingecko.com/coins/images/35023/large/USDT.png\",\n name: \"USDT Coin\",\n symbol: \"USDT\",\n tokenAddress: \"0xf55bec9cafdbe8730f096aa55dad6d22d44099df\",\n },\n ],\n ],\n [\n SUPPORTED_CHAINS.BASE,\n [\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628\",\n name: \"Ether\",\n symbol: \"ETH\",\n tokenAddress: \"0x0000000000000000000000000000000000000000\",\n },\n {\n decimals: 6,\n logo: \"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694\",\n name: \"USD Coin\",\n symbol: \"USDC\",\n tokenAddress: \"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913\",\n },\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/9956/large/Badge_Dai.png?1696509996\",\n name: \"Dai Stablecoin\",\n symbol: \"DAI\",\n tokenAddress: \"0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb\",\n },\n {\n decimals: 18,\n logo: \"https://coin-images.coingecko.com/coins/images/28206/large/ftxG9_TJ_400x400.jpeg?1696527208\",\n name: \"LayerZero\",\n symbol: \"ZRO\",\n tokenAddress: \"0x6985884c4392d348587b19cb9eaaf157f13271cd\",\n },\n {\n decimals: 18,\n logo: \"https://assets.coingecko.com/coins/images/12151/standard/OM_Token.png?1696511991\",\n name: \"MANTRA\",\n symbol: \"OM\",\n tokenAddress: \"0x3992b27da26848c2b19cea6fd25ad5568b68ab98\",\n },\n {\n decimals: 18,\n logo: \"https://assets.coingecko.com/coins/images/54411/standard/Qm4DW488_400x400.jpg\",\n name: \"KAITO\",\n symbol: \"KAITO\",\n tokenAddress: \"0x98d0baa52b2d063e780de12f615f963fe8537553\",\n },\n ],\n ],\n [\n SUPPORTED_CHAINS.BNB,\n [\n {\n decimals: 18,\n logo: \"https://assets.coingecko.com/coins/images/825/large/bnb-icon2_2x.png\",\n name: \"BNB\",\n symbol: \"BNB\",\n tokenAddress: \"0x0000000000000000000000000000000000000000\",\n },\n ],\n ],\n]);\n\nexport const TOKEN_IMAGES: Record = {\n USDC: \"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png\",\n USDT: \"https://coin-images.coingecko.com/coins/images/35023/large/USDT.png\",\n USDM: \"https://raw.githubusercontent.com/availproject/nexus-assets/main/tokens/usdm/logo.png\",\n \"USD₮0\":\n \"https://coin-images.coingecko.com/coins/images/35023/large/USDT.png\",\n WETH: \"https://assets.coingecko.com/coins/images/279/large/ethereum.png?1595348880\",\n USDS: \"https://assets.coingecko.com/coins/images/39926/standard/usds.webp?1726666683\",\n SOPH: \"https://assets.coingecko.com/coins/images/38680/large/sophon_logo_200.png\",\n KAIA: \"https://assets.coingecko.com/asset_platforms/images/9672/large/kaia.png\",\n BNB: \"https://assets.coingecko.com/coins/images/825/large/bnb-icon2_2x.png\",\n // Add ETH as fallback for any ETH-related tokens\n ETH: \"https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628\",\n // Add common token fallbacks\n POL: \"https://coin-images.coingecko.com/coins/images/32440/standard/polygon.png\",\n AVAX: \"https://assets.coingecko.com/coins/images/12559/standard/Avalanche_Circle_RedWhite_Trans.png\",\n FUEL: \"https://coin-images.coingecko.com/coins/images/279/large/ethereum.png\",\n HYPE: \"https://assets.coingecko.com/asset_platforms/images/243/large/hyperliquid.png\",\n // Popular swap tokens\n DAI: \"https://coin-images.coingecko.com/coins/images/9956/large/Badge_Dai.png?1696509996\",\n UNI: \"https://coin-images.coingecko.com/coins/images/12504/large/uni.jpg?1696512319\",\n AAVE: \"https://coin-images.coingecko.com/coins/images/12645/large/AAVE.png?1696512452\",\n LDO: \"https://coin-images.coingecko.com/coins/images/13573/large/Lido_DAO.png?1696513326\",\n PEPE: \"https://coin-images.coingecko.com/coins/images/29850/large/pepe-token.jpeg?1696528776\",\n OP: \"https://coin-images.coingecko.com/coins/images/25244/large/Optimism.png?1696524385\",\n ZRO: \"https://coin-images.coingecko.com/coins/images/28206/large/ftxG9_TJ_400x400.jpeg?1696527208\",\n OM: \"https://assets.coingecko.com/coins/images/12151/standard/OM_Token.png?1696511991\",\n KAITO:\n \"https://assets.coingecko.com/coins/images/54411/standard/Qm4DW488_400x400.jpg\",\n};\n", "type": "registry:component", "target": "components/swaps/config/destination.ts" }, @@ -96,13 +96,13 @@ }, { "path": "registry/nexus-elements/swaps/hooks/useSwaps.ts", - "content": "import {\n type RefObject,\n useCallback,\n useEffect,\n useMemo,\n useReducer,\n useRef,\n useState,\n} from \"react\";\nimport {\n NexusSDK,\n type SUPPORTED_CHAINS_IDS,\n type ExactInSwapInput,\n type ExactOutSwapInput,\n NEXUS_EVENTS,\n type SwapStepType,\n type OnSwapIntentHookData,\n type Source as SwapSource,\n type UserAsset,\n sortSourcesByPriority,\n parseUnits,\n formatTokenBalance,\n} from \"@avail-project/nexus-core\";\nimport { padHex, type Hex } from \"viem\";\nimport {\n useTransactionSteps,\n SWAP_EXPECTED_STEPS,\n useNexusError,\n useDebouncedCallback,\n usePolling,\n} from \"../../common\";\nimport {\n buildSourceOptionKey,\n getIntentMatchedOptionKeys,\n getIntentSourcesSignature,\n} from \"../utils/source-matching\";\n\nconst ZERO_ADDRESS = \"0x0000000000000000000000000000000000000000\";\nconst EVM_NATIVE_PLACEHOLDER = \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\";\n\nfunction normalizeAddress(address: string): string {\n return address.toLowerCase();\n}\n\nfunction toComparableSdkAddress(address: string): string {\n const normalized = normalizeAddress(address);\n const effectiveAddress =\n normalized === ZERO_ADDRESS ? EVM_NATIVE_PLACEHOLDER : normalized;\n\n try {\n return padHex(effectiveAddress as Hex, { size: 32 }).toLowerCase();\n } catch {\n return effectiveAddress;\n }\n}\n\ntype AssetBreakdownWithOptionalIcon = UserAsset[\"breakdown\"][number] & {\n icon?: string;\n};\n\nfunction getBreakdownTokenIcon(\n breakdown: UserAsset[\"breakdown\"][number],\n): string {\n const icon = (breakdown as AssetBreakdownWithOptionalIcon).icon;\n return typeof icon === \"string\" && icon.length > 0 ? icon : \"\";\n}\n\nexport type SourceTokenInfo = {\n contractAddress: `0x${string}`;\n decimals: number;\n logo: string;\n name: string;\n symbol: string;\n balance?: string;\n balanceInFiat?: string;\n chainId?: number;\n};\n\nexport type DestinationTokenInfo = {\n tokenAddress: `0x${string}`;\n decimals: number;\n logo: string;\n name: string;\n symbol: string;\n chainId?: number;\n balance?: string;\n balanceInFiat?: string;\n};\n\nexport type ExactOutSourceOption = {\n key: string;\n chainId: number;\n chainName: string;\n chainLogo: string;\n tokenAddress: `0x${string}`;\n tokenSymbol: string;\n tokenLogo: string;\n balance: string;\n decimals: number;\n};\n\nexport type TransactionStatus =\n | \"idle\"\n | \"simulating\"\n | \"swapping\"\n | \"success\"\n | \"error\";\n\nexport type SwapMode = \"exactIn\" | \"exactOut\";\n\nexport interface SwapInputs {\n fromChainID?: SUPPORTED_CHAINS_IDS;\n fromToken?: SourceTokenInfo;\n fromAmount?: string;\n toChainID?: SUPPORTED_CHAINS_IDS;\n toToken?: DestinationTokenInfo;\n toAmount?: string;\n}\n\nexport type SwapState = {\n inputs: SwapInputs;\n swapMode: SwapMode;\n status: TransactionStatus;\n error: string | null;\n explorerUrls: {\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n };\n};\n\ntype Action =\n | { type: \"setInputs\"; payload: Partial }\n | { type: \"setStatus\"; payload: TransactionStatus }\n | { type: \"setError\"; payload: string | null }\n | { type: \"setSwapMode\"; payload: SwapMode }\n | {\n type: \"setExplorerUrls\";\n payload: Partial;\n }\n | { type: \"reset\" };\n\nconst initialState: SwapState = {\n inputs: {\n fromToken: undefined,\n toToken: undefined,\n fromAmount: undefined,\n toAmount: undefined,\n fromChainID: undefined,\n toChainID: undefined,\n },\n swapMode: \"exactIn\",\n status: \"idle\",\n error: null,\n explorerUrls: {\n sourceExplorerUrl: null,\n destinationExplorerUrl: null,\n },\n};\n\nfunction reducer(state: SwapState, action: Action): SwapState {\n switch (action.type) {\n case \"setInputs\": {\n return {\n ...state,\n inputs: {\n ...state.inputs,\n ...action.payload,\n },\n };\n }\n case \"setStatus\":\n return { ...state, status: action.payload };\n case \"setError\":\n return { ...state, error: action.payload };\n case \"setSwapMode\":\n return { ...state, swapMode: action.payload };\n case \"setExplorerUrls\":\n return {\n ...state,\n explorerUrls: { ...state.explorerUrls, ...action.payload },\n };\n case \"reset\":\n return { ...initialState };\n default:\n return state;\n }\n}\n\ninterface UseSwapsProps {\n nexusSDK: NexusSDK | null;\n swapIntent: RefObject;\n swapBalance: UserAsset[] | null;\n fetchBalance: () => Promise;\n onComplete?: (amount?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n}\n\nconst useSwaps = ({\n nexusSDK,\n swapIntent,\n swapBalance,\n fetchBalance,\n onComplete,\n onStart,\n onError,\n}: UseSwapsProps) => {\n const [state, dispatch] = useReducer(reducer, initialState);\n const {\n steps,\n seed,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n const swapRunIdRef = useRef(0);\n const lastSyncedIntentSourcesSignatureRef = useRef(\"\");\n const lastSyncedIntentSelectionKeyRef = useRef(\"\");\n\n const currentIntentSources = swapIntent.current?.intent?.sources ?? [];\n const currentIntentSourcesSignature = useMemo(\n () => getIntentSourcesSignature(currentIntentSources),\n [currentIntentSources],\n );\n\n const exactOutSourceOptions = useMemo(() => {\n const optionsByKey = new Map();\n const excludedDestinationChainId = state.inputs.toChainID;\n\n const upsertOption = (option: ExactOutSourceOption) => {\n optionsByKey.set(option.key, option);\n };\n\n for (const asset of swapBalance ?? []) {\n for (const entry of asset.breakdown ?? []) {\n const balance = entry.balance ?? \"0\";\n const parsed = Number.parseFloat(balance);\n if (!Number.isFinite(parsed) || parsed <= 0) continue;\n\n const tokenAddress = entry.contractAddress as `0x${string}`;\n const chainId = entry.chain.id;\n if (\n typeof excludedDestinationChainId === \"number\" &&\n chainId === excludedDestinationChainId\n ) {\n continue;\n }\n upsertOption({\n key: buildSourceOptionKey(chainId, tokenAddress),\n chainId,\n chainName: entry.chain.name,\n chainLogo: entry.chain.logo,\n tokenAddress,\n tokenSymbol: entry.symbol,\n tokenLogo: getBreakdownTokenIcon(entry),\n balance,\n decimals: entry.decimals ?? asset.decimals,\n });\n }\n }\n\n for (const source of currentIntentSources) {\n const chainId = source.chain.id;\n if (\n typeof excludedDestinationChainId === \"number\" &&\n chainId === excludedDestinationChainId\n ) {\n continue;\n }\n const tokenAddress = source.token.contractAddress as `0x${string}`;\n const key = buildSourceOptionKey(chainId, tokenAddress);\n if (optionsByKey.has(key)) continue;\n\n upsertOption({\n key,\n chainId,\n chainName: source.chain.name,\n chainLogo: source.chain.logo,\n tokenAddress,\n tokenSymbol: source.token.symbol,\n tokenLogo: \"\",\n balance: source.amount ?? \"0\",\n decimals: source.token.decimals,\n });\n }\n\n const options = [...optionsByKey.values()];\n\n const destinationChainId = state.inputs.toChainID;\n const destinationToken = state.inputs.toToken;\n if (!destinationChainId || !destinationToken || !swapBalance?.length) {\n return options.sort((a, b) => {\n if (a.tokenSymbol === b.tokenSymbol) {\n return a.chainName.localeCompare(b.chainName);\n }\n return a.tokenSymbol.localeCompare(b.tokenSymbol);\n });\n }\n\n const priorityByOptionKey = new Map();\n const sortedSources = sortSourcesByPriority(swapBalance, {\n chainID: destinationChainId,\n tokenAddress: destinationToken.tokenAddress,\n symbol: destinationToken.symbol,\n });\n\n sortedSources.forEach((source, index) => {\n const sourceComparableAddress = toComparableSdkAddress(\n source.tokenAddress,\n );\n\n for (const option of options) {\n if (option.chainId !== source.chainID) continue;\n const optionComparableAddress = toComparableSdkAddress(\n option.tokenAddress,\n );\n if (optionComparableAddress !== sourceComparableAddress) continue;\n if (!priorityByOptionKey.has(option.key)) {\n priorityByOptionKey.set(option.key, index);\n }\n }\n });\n\n return options.sort((a, b) => {\n const aPriority =\n priorityByOptionKey.get(a.key) ?? Number.MAX_SAFE_INTEGER;\n const bPriority =\n priorityByOptionKey.get(b.key) ?? Number.MAX_SAFE_INTEGER;\n if (aPriority !== bPriority) {\n return aPriority - bPriority;\n }\n\n const aBalance = Number.parseFloat(a.balance);\n const bBalance = Number.parseFloat(b.balance);\n if (Number.isFinite(aBalance) && Number.isFinite(bBalance)) {\n if (aBalance !== bBalance) {\n return bBalance - aBalance;\n }\n }\n\n if (a.tokenSymbol === b.tokenSymbol) {\n return a.chainName.localeCompare(b.chainName);\n }\n return a.tokenSymbol.localeCompare(b.tokenSymbol);\n });\n }, [\n currentIntentSources,\n currentIntentSourcesSignature,\n state.inputs.toToken,\n state.inputs.toChainID,\n swapBalance,\n ]);\n\n const exactOutAllSourceKeys = useMemo(\n () => exactOutSourceOptions.map((opt) => opt.key),\n [exactOutSourceOptions],\n );\n\n const [exactOutSelectedKeys, setExactOutSelectedKeys] = useState<\n string[] | null\n >(null);\n const [appliedExactOutSelectionKey, setAppliedExactOutSelectionKey] =\n useState(\"ALL\");\n\n const effectiveExactOutSelectedKeys = useMemo(() => {\n const allKeys = exactOutAllSourceKeys;\n if (allKeys.length === 0) return [];\n\n const selectedKeys = exactOutSelectedKeys ?? allKeys;\n const selectedSet = new Set(selectedKeys);\n const filtered = allKeys.filter((key) => selectedSet.has(key));\n return filtered.length > 0 ? filtered : allKeys;\n }, [exactOutSelectedKeys, exactOutAllSourceKeys]);\n\n const isExactOutAllSelected = useMemo(() => {\n if (exactOutAllSourceKeys.length === 0) return true;\n return (\n effectiveExactOutSelectedKeys.length === exactOutAllSourceKeys.length\n );\n }, [exactOutAllSourceKeys, effectiveExactOutSelectedKeys]);\n\n const toggleExactOutSource = useCallback(\n (key: string) => {\n setExactOutSelectedKeys((prev) => {\n const allKeys = exactOutAllSourceKeys;\n if (allKeys.length === 0) return prev;\n\n const current = prev ?? allKeys;\n const set = new Set(current);\n if (set.has(key)) {\n set.delete(key);\n } else {\n set.add(key);\n }\n\n const next = allKeys.filter((k) => set.has(k));\n if (next.length === 0) return prev ?? allKeys; // keep at least 1\n if (next.length === allKeys.length) return null; // back to default \"all\"\n return next;\n });\n },\n [exactOutAllSourceKeys],\n );\n\n const applyExactOutSelectionKeys = useCallback(\n (keys: string[]) => {\n const allKeys = exactOutAllSourceKeys;\n if (allKeys.length === 0) return;\n\n const selectedSet = new Set(keys);\n const filtered = allKeys.filter((k) => selectedSet.has(k));\n const unique = [...new Set(filtered)];\n if (unique.length === 0) return;\n\n const isAllSelected = unique.length === allKeys.length;\n const selectionKey = isAllSelected ? \"ALL\" : [...unique].sort().join(\"|\");\n\n setExactOutSelectedKeys(isAllSelected ? null : unique);\n setAppliedExactOutSelectionKey(selectionKey);\n },\n [exactOutAllSourceKeys],\n );\n\n const exactOutSelectionKey = useMemo(() => {\n if (isExactOutAllSelected) return \"ALL\";\n return [...effectiveExactOutSelectedKeys].sort().join(\"|\");\n }, [effectiveExactOutSelectedKeys, isExactOutAllSelected]);\n\n const syncExactOutSelectionFromIntent = useCallback(\n (\n intentSources: NonNullable[\"sources\"],\n force = false,\n ) => {\n if (intentSources.length === 0 || exactOutSourceOptions.length === 0) {\n return false;\n }\n\n const signature = getIntentSourcesSignature(intentSources);\n const usedKeys = getIntentMatchedOptionKeys(\n intentSources,\n exactOutSourceOptions,\n );\n if (usedKeys.length === 0) return false;\n const usedSelectionKey = [...new Set(usedKeys)].sort().join(\"|\");\n if (\n !force &&\n signature === lastSyncedIntentSourcesSignatureRef.current &&\n usedSelectionKey === lastSyncedIntentSelectionKeyRef.current\n ) {\n return false;\n }\n\n applyExactOutSelectionKeys(usedKeys);\n lastSyncedIntentSourcesSignatureRef.current = signature;\n lastSyncedIntentSelectionKeyRef.current = usedSelectionKey;\n return true;\n },\n [applyExactOutSelectionKeys, exactOutSourceOptions],\n );\n\n const exactOutFromSources = useMemo(() => {\n if (state.swapMode !== \"exactOut\") return undefined;\n if (exactOutSourceOptions.length === 0) return undefined;\n\n const selectedSet = new Set(effectiveExactOutSelectedKeys);\n const sources: SwapSource[] = [];\n const seen = new Set();\n\n for (const opt of exactOutSourceOptions) {\n if (!selectedSet.has(opt.key)) continue;\n if (seen.has(opt.key)) continue;\n seen.add(opt.key);\n sources.push({ chainId: opt.chainId, tokenAddress: opt.tokenAddress });\n }\n\n return sources.length > 0 ? sources : undefined;\n }, [state.swapMode, effectiveExactOutSelectedKeys, exactOutSourceOptions]);\n const isExactOutSourceSelectionDirty = useMemo(() => {\n return (\n state.swapMode === \"exactOut\" &&\n exactOutSelectionKey !== appliedExactOutSelectionKey\n );\n }, [state.swapMode, exactOutSelectionKey, appliedExactOutSelectionKey]);\n\n const [updatingExactOutSources, setUpdatingExactOutSources] = useState(false);\n\n // Validation for exact-in mode\n const areExactInInputsValid = useMemo(() => {\n return (\n state?.inputs?.fromChainID !== undefined &&\n state?.inputs?.toChainID !== undefined &&\n state?.inputs?.fromToken &&\n state?.inputs?.toToken &&\n state?.inputs?.fromAmount &&\n Number(state.inputs.fromAmount) > 0\n );\n }, [state.inputs]);\n\n // Validation for exact-out mode\n const areExactOutInputsValid = useMemo(() => {\n return (\n state?.inputs?.toChainID !== undefined &&\n state?.inputs?.toToken &&\n state?.inputs?.toAmount &&\n Number(state.inputs.toAmount) > 0\n );\n }, [state.inputs]);\n\n // Combined validation based on current mode\n const areInputsValid = useMemo(() => {\n return state.swapMode === \"exactIn\"\n ? areExactInInputsValid\n : areExactOutInputsValid;\n }, [state.swapMode, areExactInInputsValid, areExactOutInputsValid]);\n\n const handleNexusError = useNexusError();\n\n // Event handler shared between exact-in and exact-out\n const handleSwapEvent = (event: { name: string; args: SwapStepType }) => {\n if (event.name === NEXUS_EVENTS.SWAP_STEP_COMPLETE) {\n const step = event.args;\n if (step?.type === \"SOURCE_SWAP_HASH\" && step.explorerURL) {\n dispatch({\n type: \"setExplorerUrls\",\n payload: { sourceExplorerUrl: step.explorerURL },\n });\n }\n if (step?.type === \"DESTINATION_SWAP_HASH\" && step.explorerURL) {\n dispatch({\n type: \"setExplorerUrls\",\n payload: { destinationExplorerUrl: step.explorerURL },\n });\n }\n onStepComplete(step);\n }\n };\n\n const handleExactInSwap = async (runId: number) => {\n const fromToken = state.inputs.fromToken;\n const toToken = state.inputs.toToken;\n const fromAmount = state.inputs.fromAmount;\n const toChainID = state.inputs.toChainID;\n const fromChainID = state.inputs.fromChainID;\n\n if (\n !nexusSDK ||\n !areExactInInputsValid ||\n !fromToken ||\n !toToken ||\n !fromAmount ||\n !toChainID ||\n !fromChainID\n )\n return;\n\n const sourceBalance = swapBalance\n ?.flatMap((token) => token.breakdown ?? [])\n ?.find(\n (chain) =>\n chain.chain?.id === fromChainID &&\n normalizeAddress(chain.contractAddress) ===\n normalizeAddress(fromToken.contractAddress),\n );\n if (\n !sourceBalance ||\n Number.parseFloat(sourceBalance.balance ?? \"0\") <= 0\n ) {\n throw new Error(\n \"No balance found for this wallet on supported source chains.\",\n );\n }\n\n const amountBigInt = parseUnits(fromAmount, fromToken.decimals);\n const swapInput: ExactInSwapInput = {\n from: [\n {\n chainId: fromChainID,\n amount: amountBigInt,\n tokenAddress: fromToken.contractAddress,\n },\n ],\n toChainId: toChainID,\n toTokenAddress: toToken.tokenAddress,\n };\n\n const result = await nexusSDK.swapWithExactIn(swapInput, {\n onEvent: (event) => {\n if (swapRunIdRef.current !== runId) return;\n handleSwapEvent(event as { name: string; args: SwapStepType });\n },\n });\n\n if (!result?.success) {\n throw new Error(result?.error || \"Swap failed\");\n }\n };\n\n const handleExactOutSwap = async (runId: number) => {\n const toToken = state.inputs.toToken;\n const toAmount = state.inputs.toAmount;\n const toChainID = state.inputs.toChainID;\n\n if (\n !nexusSDK ||\n !areExactOutInputsValid ||\n !toToken ||\n !toAmount ||\n !toChainID\n )\n return;\n if (swapBalance && exactOutSourceOptions.length === 0) {\n throw new Error(\n \"No balance found for this wallet on supported source chains.\",\n );\n }\n if (!exactOutFromSources || exactOutFromSources.length === 0) {\n throw new Error(\"Select at least one source with available balance.\");\n }\n\n const amountBigInt = parseUnits(toAmount, toToken.decimals);\n const swapInput: ExactOutSwapInput = {\n toAmount: amountBigInt,\n toChainId: toChainID,\n toTokenAddress: toToken.tokenAddress,\n ...(exactOutFromSources ? { fromSources: exactOutFromSources } : {}),\n };\n\n const result = await nexusSDK.swapWithExactOut(swapInput, {\n onEvent: (event) => {\n if (swapRunIdRef.current !== runId) return;\n handleSwapEvent(event as { name: string; args: SwapStepType });\n },\n });\n if (!result?.success) {\n throw new Error(result?.error || \"Swap failed\");\n }\n };\n\n const runSwap = async (runId: number) => {\n if (!nexusSDK || !areInputsValid || !swapBalance) return;\n\n try {\n onStart?.();\n dispatch({ type: \"setStatus\", payload: \"simulating\" });\n dispatch({ type: \"setError\", payload: null });\n seed(SWAP_EXPECTED_STEPS);\n\n if (state.swapMode === \"exactOut\") {\n setAppliedExactOutSelectionKey(exactOutSelectionKey);\n } else {\n setAppliedExactOutSelectionKey(\"ALL\");\n }\n\n if (state.swapMode === \"exactIn\") {\n await handleExactInSwap(runId);\n } else {\n await handleExactOutSwap(runId);\n }\n\n if (swapRunIdRef.current !== runId) return;\n dispatch({ type: \"setStatus\", payload: \"success\" });\n onComplete?.(swapIntent.current?.intent?.destination?.amount);\n await fetchBalance();\n } catch (error) {\n if (swapRunIdRef.current !== runId) return;\n const { message } = handleNexusError(error);\n dispatch({ type: \"setStatus\", payload: \"error\" });\n dispatch({ type: \"setError\", payload: message });\n onError?.(message);\n swapIntent.current?.deny();\n swapIntent.current = null;\n setExactOutSelectedKeys(null);\n setAppliedExactOutSelectionKey(\"ALL\");\n setUpdatingExactOutSources(false);\n lastSyncedIntentSourcesSignatureRef.current = \"\";\n lastSyncedIntentSelectionKeyRef.current = \"\";\n void fetchBalance();\n }\n };\n\n const startSwap = () => {\n swapRunIdRef.current += 1;\n const runId = swapRunIdRef.current;\n void runSwap(runId);\n return runId;\n };\n\n const debouncedSwapStart = useDebouncedCallback(startSwap, 1200);\n\n const reset = () => {\n // invalidate any in-flight swap run\n swapRunIdRef.current += 1;\n dispatch({ type: \"reset\" });\n resetSteps();\n swapIntent.current?.deny();\n swapIntent.current = null;\n setExactOutSelectedKeys(null);\n setAppliedExactOutSelectionKey(\"ALL\");\n setUpdatingExactOutSources(false);\n lastSyncedIntentSourcesSignatureRef.current = \"\";\n lastSyncedIntentSelectionKeyRef.current = \"\";\n };\n\n useEffect(() => {\n if (state.swapMode !== \"exactOut\") return;\n if (state.status !== \"simulating\") return;\n if (exactOutSourceOptions.length === 0) return;\n\n const runId = swapRunIdRef.current;\n let cancelled = false;\n\n void (async () => {\n const start = Date.now();\n while (!cancelled && Date.now() - start < 10000) {\n if (swapRunIdRef.current !== runId) return;\n\n const intentSources = swapIntent.current?.intent?.sources ?? [];\n if (intentSources.length > 0) {\n syncExactOutSelectionFromIntent(intentSources);\n return;\n }\n\n // eslint-disable-next-line no-await-in-loop\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [\n currentIntentSourcesSignature,\n exactOutSourceOptions,\n state.status,\n state.swapMode,\n syncExactOutSelectionFromIntent,\n swapIntent,\n ]);\n\n const availableBalance = useMemo(() => {\n if (\n !nexusSDK ||\n !swapBalance ||\n !state.inputs?.fromToken ||\n !state.inputs?.fromChainID\n )\n return undefined;\n return (\n swapBalance\n ?.flatMap((token) => token.breakdown ?? [])\n ?.find(\n (chain) =>\n chain.chain?.id === state.inputs?.fromChainID &&\n normalizeAddress(chain.contractAddress) ===\n normalizeAddress(state.inputs?.fromToken?.contractAddress ?? \"\"),\n ) ?? undefined\n );\n }, [\n state.inputs?.fromToken,\n state.inputs?.fromChainID,\n swapBalance,\n nexusSDK,\n ]);\n\n const destinationBalance = useMemo(() => {\n if (\n !nexusSDK ||\n !swapBalance ||\n !state.inputs?.toToken ||\n !state.inputs?.toChainID\n )\n return undefined;\n return (\n swapBalance\n ?.flatMap((token) => token.breakdown ?? [])\n ?.find(\n (chain) =>\n chain.chain?.id === state?.inputs?.toChainID &&\n normalizeAddress(chain.contractAddress) ===\n normalizeAddress(state?.inputs?.toToken?.tokenAddress ?? \"\"),\n ) ?? undefined\n );\n }, [state?.inputs?.toToken, state?.inputs?.toChainID, swapBalance, nexusSDK]);\n\n const availableStables = useMemo(() => {\n if (!nexusSDK || !swapBalance) return [];\n const stableSymbols = new Set([\"USDT\", \"USDC\", \"ETH\", \"DAI\", \"WBTC\"]);\n const filteredToken = swapBalance.filter((token) =>\n (token.breakdown ?? []).some((entry) =>\n stableSymbols.has(entry.symbol.toUpperCase()),\n ),\n );\n return filteredToken ?? [];\n }, [swapBalance, nexusSDK]);\n\n const formatBalance = (\n balance?: string | number,\n symbol?: string,\n decimals?: number,\n ) => {\n if (!balance || !symbol || !decimals) return undefined;\n return formatTokenBalance(balance, {\n symbol: symbol,\n decimals: decimals,\n });\n };\n\n useEffect(() => {\n if (!swapBalance) {\n fetchBalance();\n }\n }, [swapBalance]);\n\n useEffect(() => {\n // Check validity based on current swap mode\n const isValidForCurrentMode =\n state.swapMode === \"exactIn\"\n ? areExactInInputsValid &&\n state?.inputs?.fromAmount &&\n state?.inputs?.fromChainID &&\n state?.inputs?.fromToken &&\n state?.inputs?.toChainID &&\n state?.inputs?.toToken\n : areExactOutInputsValid &&\n state?.inputs?.toAmount &&\n state?.inputs?.toChainID &&\n state?.inputs?.toToken;\n\n if (!isValidForCurrentMode) {\n swapIntent.current?.deny();\n swapIntent.current = null;\n lastSyncedIntentSourcesSignatureRef.current = \"\";\n lastSyncedIntentSelectionKeyRef.current = \"\";\n return;\n }\n if (state.status === \"idle\") {\n debouncedSwapStart();\n }\n }, [\n state.inputs,\n state.swapMode,\n areExactInInputsValid,\n areExactOutInputsValid,\n state.status,\n ]);\n\n const refreshSimulation = async () => {\n try {\n const updated = await swapIntent.current?.refresh();\n if (updated) {\n swapIntent.current!.intent = updated;\n }\n } catch (e) {\n console.error(e);\n }\n };\n\n usePolling(\n state.status === \"simulating\" && Boolean(swapIntent.current),\n async () => {\n await refreshSimulation();\n },\n 15000,\n );\n\n const continueSwap = useCallback(async () => {\n if (state.status !== \"simulating\") return;\n\n if (state.swapMode !== \"exactOut\" || !isExactOutSourceSelectionDirty) {\n dispatch({ type: \"setStatus\", payload: \"swapping\" });\n swapIntent.current?.allow();\n return;\n }\n\n if (!nexusSDK || !areInputsValid) return;\n\n setUpdatingExactOutSources(true);\n try {\n const previousIntent = swapIntent.current;\n swapRunIdRef.current += 1;\n const runId = swapRunIdRef.current;\n\n previousIntent?.deny();\n\n void runSwap(runId);\n const start = Date.now();\n while (Date.now() - start < 10000) {\n if (swapRunIdRef.current !== runId) return;\n const nextIntent = swapIntent.current;\n const sourcesReady =\n nextIntent &&\n nextIntent !== previousIntent &&\n (nextIntent.intent.sources?.length ?? 0) > 0;\n if (sourcesReady) break;\n // eslint-disable-next-line no-await-in-loop\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n\n if (swapRunIdRef.current !== runId) return;\n const nextIntent = swapIntent.current;\n if (!nextIntent || nextIntent === previousIntent) return;\n if ((nextIntent.intent.sources?.length ?? 0) === 0) return;\n syncExactOutSelectionFromIntent(nextIntent.intent.sources, true);\n // Updated sources are now reflected in the intent. Wait for explicit user\n // confirmation before proceeding.\n return;\n } finally {\n setUpdatingExactOutSources(false);\n }\n }, [\n areInputsValid,\n isExactOutSourceSelectionDirty,\n nexusSDK,\n runSwap,\n syncExactOutSelectionFromIntent,\n state.status,\n state.swapMode,\n swapIntent,\n ]);\n\n return {\n status: state.status,\n inputs: state.inputs,\n swapMode: state.swapMode,\n setSwapMode: (mode: SwapMode) =>\n dispatch({ type: \"setSwapMode\", payload: mode }),\n setStatus: (status: TransactionStatus) =>\n dispatch({ type: \"setStatus\", payload: status }),\n setInputs: (inputs: Partial) => {\n if (state.status === \"error\") {\n dispatch({ type: \"setError\", payload: null });\n dispatch({ type: \"setStatus\", payload: \"idle\" });\n }\n dispatch({ type: \"setInputs\", payload: inputs });\n },\n txError: state.error,\n setTxError: (error: string | null) =>\n dispatch({ type: \"setError\", payload: error }),\n availableBalance,\n availableStables,\n destinationBalance,\n formatBalance,\n steps,\n explorerUrls: state.explorerUrls,\n handleSwap: startSwap,\n continueSwap,\n exactOutSourceOptions,\n exactOutSelectedKeys: effectiveExactOutSelectedKeys,\n toggleExactOutSource,\n isExactOutSourceSelectionDirty,\n updatingExactOutSources,\n reset,\n areInputsValid,\n };\n};\n\nexport default useSwaps;\n", + "content": "import {\n type RefObject,\n useCallback,\n useEffect,\n useMemo,\n useReducer,\n useRef,\n useState,\n} from \"react\";\nimport type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type {\n SwapExactInParams,\n SwapExactOutParams,\n SwapEvent,\n SwapPlanStep,\n OnSwapIntentHookData,\n Source as SwapSource,\n TokenBalance,\n ChainBalance,\n} from \"@avail-project/nexus-sdk-v2\";\nimport { formatTokenBalance } from \"@avail-project/nexus-sdk-v2/utils\";\nimport { padHex, parseUnits, type Hex } from \"viem\";\nimport {\n useTransactionSteps,\n SWAP_EXPECTED_STEPS,\n useNexusError,\n useDebouncedCallback,\n usePolling,\n} from \"../../common\";\nimport {\n buildSourceOptionKey,\n getIntentMatchedOptionKeys,\n getIntentSourcesSignature,\n} from \"../utils/source-matching\";\n\ntype NexusClient = ReturnType;\n\nconst ZERO_ADDRESS = \"0x0000000000000000000000000000000000000000\";\nconst EVM_NATIVE_PLACEHOLDER = \"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\";\n\nfunction normalizeAddress(address: string): string {\n return address.toLowerCase();\n}\n\nfunction toComparableSdkAddress(address: string): string {\n const normalized = normalizeAddress(address);\n const effectiveAddress =\n normalized === ZERO_ADDRESS ? EVM_NATIVE_PLACEHOLDER : normalized;\n\n try {\n return padHex(effectiveAddress as Hex, { size: 32 }).toLowerCase();\n } catch {\n return effectiveAddress;\n }\n}\n\ntype AssetBreakdownWithOptionalIcon = ChainBalance & {\n icon?: string;\n};\n\nfunction getBreakdownTokenIcon(\n breakdown: ChainBalance,\n): string {\n const icon = (breakdown as AssetBreakdownWithOptionalIcon).icon;\n return typeof icon === \"string\" && icon.length > 0 ? icon : \"\";\n}\n\nexport type SourceTokenInfo = {\n contractAddress: `0x${string}`;\n decimals: number;\n logo: string;\n name: string;\n symbol: string;\n balance?: string;\n balanceInFiat?: string;\n chainId?: number;\n};\n\nexport type DestinationTokenInfo = {\n tokenAddress: `0x${string}`;\n decimals: number;\n logo: string;\n name: string;\n symbol: string;\n chainId?: number;\n balance?: string;\n balanceInFiat?: string;\n};\n\nexport type ExactOutSourceOption = {\n key: string;\n chainId: number;\n chainName: string;\n chainLogo: string;\n tokenAddress: `0x${string}`;\n tokenSymbol: string;\n tokenLogo: string;\n balance: string;\n decimals: number;\n};\n\nexport type TransactionStatus =\n | \"idle\"\n | \"simulating\"\n | \"swapping\"\n | \"success\"\n | \"error\";\n\nexport type SwapMode = \"exactIn\" | \"exactOut\";\n\nexport interface SwapInputs {\n fromChainID?: number;\n fromToken?: SourceTokenInfo;\n fromAmount?: string;\n toChainID?: number;\n toToken?: DestinationTokenInfo;\n toAmount?: string;\n}\n\nexport type SwapState = {\n inputs: SwapInputs;\n swapMode: SwapMode;\n status: TransactionStatus;\n error: string | null;\n explorerUrls: {\n sourceExplorerUrl: string | null;\n destinationExplorerUrl: string | null;\n };\n};\n\ntype Action =\n | { type: \"setInputs\"; payload: Partial }\n | { type: \"setStatus\"; payload: TransactionStatus }\n | { type: \"setError\"; payload: string | null }\n | { type: \"setSwapMode\"; payload: SwapMode }\n | {\n type: \"setExplorerUrls\";\n payload: Partial;\n }\n | { type: \"reset\" };\n\nconst initialState: SwapState = {\n inputs: {\n fromToken: undefined,\n toToken: undefined,\n fromAmount: undefined,\n toAmount: undefined,\n fromChainID: undefined,\n toChainID: undefined,\n },\n swapMode: \"exactIn\",\n status: \"idle\",\n error: null,\n explorerUrls: {\n sourceExplorerUrl: null,\n destinationExplorerUrl: null,\n },\n};\n\nfunction reducer(state: SwapState, action: Action): SwapState {\n switch (action.type) {\n case \"setInputs\": {\n return {\n ...state,\n inputs: {\n ...state.inputs,\n ...action.payload,\n },\n };\n }\n case \"setStatus\":\n return { ...state, status: action.payload };\n case \"setError\":\n return { ...state, error: action.payload };\n case \"setSwapMode\":\n return { ...state, swapMode: action.payload };\n case \"setExplorerUrls\":\n return {\n ...state,\n explorerUrls: { ...state.explorerUrls, ...action.payload },\n };\n case \"reset\":\n return { ...initialState };\n default:\n return state;\n }\n}\n\ninterface UseSwapsProps {\n nexusSDK: NexusClient | null;\n swapIntent: RefObject;\n swapBalance: TokenBalance[] | null;\n fetchBalance: () => Promise;\n onComplete?: (amount?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n}\n\n// v2 swap step shape (minimal, used for step-tracking)\ntype SwapStep = {\n typeID?: string;\n type?: string;\n explorerURL?: string;\n [key: string]: unknown;\n};\n\nconst useSwaps = ({\n nexusSDK,\n swapIntent,\n swapBalance,\n fetchBalance,\n onComplete,\n onStart,\n onError,\n}: UseSwapsProps) => {\n const [state, dispatch] = useReducer(reducer, initialState);\n const {\n steps,\n seed,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n const swapRunIdRef = useRef(0);\n const lastSyncedIntentSourcesSignatureRef = useRef(\"\");\n const lastSyncedIntentSelectionKeyRef = useRef(\"\");\n\n const currentIntentSources = swapIntent.current?.intent?.sources ?? [];\n const currentIntentSourcesSignature = useMemo(\n () => getIntentSourcesSignature(currentIntentSources),\n [currentIntentSources],\n );\n\n const exactOutSourceOptions = useMemo(() => {\n const optionsByKey = new Map();\n const excludedDestinationChainId = state.inputs.toChainID;\n\n const upsertOption = (option: ExactOutSourceOption) => {\n optionsByKey.set(option.key, option);\n };\n\n for (const asset of swapBalance ?? []) {\n for (const entry of asset.chainBalances ?? []) {\n const balance = entry.balance ?? \"0\";\n const parsed = Number.parseFloat(balance);\n if (!Number.isFinite(parsed) || parsed <= 0) continue;\n\n const tokenAddress = entry.contractAddress as `0x${string}`;\n const chainId = entry.chain.id;\n if (\n typeof excludedDestinationChainId === \"number\" &&\n chainId === excludedDestinationChainId\n ) {\n continue;\n }\n upsertOption({\n key: buildSourceOptionKey(chainId, tokenAddress),\n chainId,\n chainName: entry.chain.name,\n chainLogo: entry.chain.logo,\n tokenAddress,\n // v2: breakdown has no .symbol — use parent asset.symbol\n tokenSymbol: asset.symbol,\n tokenLogo: getBreakdownTokenIcon(entry),\n balance,\n decimals: entry.decimals ?? asset.decimals,\n });\n }\n }\n\n for (const source of currentIntentSources) {\n const chainId = source.chain.id;\n if (\n typeof excludedDestinationChainId === \"number\" &&\n chainId === excludedDestinationChainId\n ) {\n continue;\n }\n const tokenAddress = source.token.contractAddress as `0x${string}`;\n const key = buildSourceOptionKey(chainId, tokenAddress);\n if (optionsByKey.has(key)) continue;\n\n upsertOption({\n key,\n chainId,\n chainName: source.chain.name,\n chainLogo: source.chain.logo,\n tokenAddress,\n tokenSymbol: source.token.symbol,\n tokenLogo: \"\",\n balance: source.amount ?? \"0\",\n decimals: source.token.decimals,\n });\n }\n\n const options = [...optionsByKey.values()];\n\n // v2: sortSourcesByPriority removed — sort by balance descending\n const destinationChainId = state.inputs.toChainID;\n const destinationToken = state.inputs.toToken;\n if (!destinationChainId || !destinationToken || !swapBalance?.length) {\n return options.sort((a: ExactOutSourceOption, b: ExactOutSourceOption) => {\n if (a.tokenSymbol === b.tokenSymbol) {\n return a.chainName.localeCompare(b.chainName);\n }\n return a.tokenSymbol.localeCompare(b.tokenSymbol);\n });\n }\n\n return options.sort((a: ExactOutSourceOption, b: ExactOutSourceOption) => {\n const aBalance = Number.parseFloat(a.balance);\n const bBalance = Number.parseFloat(b.balance);\n if (Number.isFinite(aBalance) && Number.isFinite(bBalance)) {\n if (aBalance !== bBalance) return bBalance - aBalance;\n }\n if (a.tokenSymbol === b.tokenSymbol) {\n return a.chainName.localeCompare(b.chainName);\n }\n return a.tokenSymbol.localeCompare(b.tokenSymbol);\n });\n }, [\n currentIntentSources,\n currentIntentSourcesSignature,\n state.inputs.toToken,\n state.inputs.toChainID,\n swapBalance,\n ]);\n\n const exactOutAllSourceKeys = useMemo(\n () => exactOutSourceOptions.map((opt) => opt.key),\n [exactOutSourceOptions],\n );\n\n const [exactOutSelectedKeys, setExactOutSelectedKeys] = useState<\n string[] | null\n >(null);\n const [appliedExactOutSelectionKey, setAppliedExactOutSelectionKey] =\n useState(\"ALL\");\n\n const effectiveExactOutSelectedKeys = useMemo(() => {\n const allKeys = exactOutAllSourceKeys;\n if (allKeys.length === 0) return [];\n\n const selectedKeys = exactOutSelectedKeys ?? allKeys;\n const selectedSet = new Set(selectedKeys);\n const filtered = allKeys.filter((key) => selectedSet.has(key));\n return filtered.length > 0 ? filtered : allKeys;\n }, [exactOutSelectedKeys, exactOutAllSourceKeys]);\n\n const isExactOutAllSelected = useMemo(() => {\n if (exactOutAllSourceKeys.length === 0) return true;\n return (\n effectiveExactOutSelectedKeys.length === exactOutAllSourceKeys.length\n );\n }, [exactOutAllSourceKeys, effectiveExactOutSelectedKeys]);\n\n const toggleExactOutSource = useCallback(\n (key: string) => {\n setExactOutSelectedKeys((prev) => {\n const allKeys = exactOutAllSourceKeys;\n if (allKeys.length === 0) return prev;\n\n const current = prev ?? allKeys;\n const set = new Set(current);\n if (set.has(key)) {\n set.delete(key);\n } else {\n set.add(key);\n }\n\n const next = allKeys.filter((k) => set.has(k));\n if (next.length === 0) return prev ?? allKeys; // keep at least 1\n if (next.length === allKeys.length) return null; // back to default \"all\"\n return next;\n });\n },\n [exactOutAllSourceKeys],\n );\n\n const applyExactOutSelectionKeys = useCallback(\n (keys: string[]) => {\n const allKeys = exactOutAllSourceKeys;\n if (allKeys.length === 0) return;\n\n const selectedSet = new Set(keys);\n const filtered = allKeys.filter((k) => selectedSet.has(k));\n const unique = [...new Set(filtered)];\n if (unique.length === 0) return;\n\n const isAllSelected = unique.length === allKeys.length;\n const selectionKey = isAllSelected ? \"ALL\" : [...unique].sort().join(\"|\");\n\n setExactOutSelectedKeys(isAllSelected ? null : unique);\n setAppliedExactOutSelectionKey(selectionKey);\n },\n [exactOutAllSourceKeys],\n );\n\n const exactOutSelectionKey = useMemo(() => {\n if (isExactOutAllSelected) return \"ALL\";\n return [...effectiveExactOutSelectedKeys].sort().join(\"|\");\n }, [effectiveExactOutSelectedKeys, isExactOutAllSelected]);\n\n const syncExactOutSelectionFromIntent = useCallback(\n (\n intentSources: NonNullable[\"sources\"],\n force = false,\n ) => {\n if (intentSources.length === 0 || exactOutSourceOptions.length === 0) {\n return false;\n }\n\n const signature = getIntentSourcesSignature(intentSources);\n const usedKeys = getIntentMatchedOptionKeys(\n intentSources,\n exactOutSourceOptions,\n );\n if (usedKeys.length === 0) return false;\n const usedSelectionKey = [...new Set(usedKeys)].sort().join(\"|\");\n if (\n !force &&\n signature === lastSyncedIntentSourcesSignatureRef.current &&\n usedSelectionKey === lastSyncedIntentSelectionKeyRef.current\n ) {\n return false;\n }\n\n applyExactOutSelectionKeys(usedKeys);\n lastSyncedIntentSourcesSignatureRef.current = signature;\n lastSyncedIntentSelectionKeyRef.current = usedSelectionKey;\n return true;\n },\n [applyExactOutSelectionKeys, exactOutSourceOptions],\n );\n\n const exactOutFromSources = useMemo(() => {\n if (state.swapMode !== \"exactOut\") return undefined;\n if (exactOutSourceOptions.length === 0) return undefined;\n\n const selectedSet = new Set(effectiveExactOutSelectedKeys);\n const sources: SwapSource[] = [];\n const seen = new Set();\n\n for (const opt of exactOutSourceOptions) {\n if (!selectedSet.has(opt.key)) continue;\n if (seen.has(opt.key)) continue;\n seen.add(opt.key);\n sources.push({ chainId: opt.chainId, tokenAddress: opt.tokenAddress });\n }\n\n return sources.length > 0 ? sources : undefined;\n }, [state.swapMode, effectiveExactOutSelectedKeys, exactOutSourceOptions]);\n\n const isExactOutSourceSelectionDirty = useMemo(() => {\n return (\n state.swapMode === \"exactOut\" &&\n exactOutSelectionKey !== appliedExactOutSelectionKey\n );\n }, [state.swapMode, exactOutSelectionKey, appliedExactOutSelectionKey]);\n\n const [updatingExactOutSources, setUpdatingExactOutSources] = useState(false);\n\n // Validation for exact-in mode\n const areExactInInputsValid = useMemo(() => {\n return (\n state?.inputs?.fromChainID !== undefined &&\n state?.inputs?.toChainID !== undefined &&\n state?.inputs?.fromToken &&\n state?.inputs?.toToken &&\n state?.inputs?.fromAmount &&\n Number(state.inputs.fromAmount) > 0\n );\n }, [state.inputs]);\n\n // Validation for exact-out mode\n const areExactOutInputsValid = useMemo(() => {\n return (\n state?.inputs?.toChainID !== undefined &&\n state?.inputs?.toToken &&\n state?.inputs?.toAmount &&\n Number(state.inputs.toAmount) > 0\n );\n }, [state.inputs]);\n\n // Combined validation based on current mode\n const areInputsValid = useMemo(() => {\n return state.swapMode === \"exactIn\"\n ? areExactInInputsValid\n : areExactOutInputsValid;\n }, [state.swapMode, areExactInInputsValid, areExactOutInputsValid]);\n\n const handleNexusError = useNexusError();\n\n /**\n * v2 swap event handler\n * SwapEvent is a typed discriminated union: { type: 'plan_preview' | 'plan_progress' | ... }\n * Explorer URLs are on the event itself (not on step.explorerURL)\n */\n const handleSwapEvent = useCallback((event: SwapEvent, runId: number, completedFromEventRef: { current: boolean }) => {\n if (swapRunIdRef.current !== runId) return;\n\n if (event.type === \"plan_preview\") {\n // Seed step tracker from plan; cast to our internal step shape\n const planSteps = (event as { type: string; plan: { steps: SwapPlanStep[] } }).plan?.steps ?? [];\n seed(planSteps.map((s, i) => {\n const stepType = (s as { type?: string }).type ?? `step-${i}`;\n return {\n typeID: `step-${i}`,\n stepType,\n ...s,\n };\n }));\n return;\n }\n\n if (event.type === \"plan_progress\") {\n const progressEvent = event as {\n type: string;\n stepType: string;\n state: string;\n step: SwapPlanStep;\n explorerUrl?: string;\n error?: string;\n };\n\n // v2: explorerUrl is on the event, not step.explorerURL\n const explorerUrl = progressEvent.explorerUrl;\n\n if (\n progressEvent.stepType === \"source_swap\" &&\n (progressEvent.state === \"submitted\" || progressEvent.state === \"confirmed\") &&\n explorerUrl\n ) {\n dispatch({\n type: \"setExplorerUrls\",\n payload: { sourceExplorerUrl: explorerUrl },\n });\n }\n\n if (\n progressEvent.stepType === \"destination_swap\" &&\n (progressEvent.state === \"submitted\" || progressEvent.state === \"confirmed\") &&\n explorerUrl\n ) {\n dispatch({\n type: \"setExplorerUrls\",\n payload: { destinationExplorerUrl: explorerUrl },\n });\n }\n\n const step = progressEvent.step as SwapStep;\n onStepComplete({\n typeID: progressEvent.stepType,\n type: progressEvent.stepType,\n ...step,\n explorerURL: explorerUrl,\n });\n\n // Drive success/failure from events, not from the SDK promise resolution\n if (progressEvent.state === \"failed\" && !completedFromEventRef.current) {\n completedFromEventRef.current = true;\n const errorMessage = progressEvent.error ?? \"Swap failed\";\n dispatch({ type: \"setStatus\", payload: \"error\" });\n dispatch({ type: \"setError\", payload: errorMessage });\n onError?.(errorMessage);\n return;\n }\n\n if (\n progressEvent.stepType === \"destination_swap\" &&\n progressEvent.state === \"completed\" &&\n !completedFromEventRef.current\n ) {\n completedFromEventRef.current = true;\n dispatch({ type: \"setStatus\", payload: \"success\" });\n onComplete?.(swapIntent.current?.intent?.destination?.amount);\n void fetchBalance();\n }\n }\n }, [seed, onStepComplete, dispatch, onError, onComplete, fetchBalance, swapRunIdRef, swapIntent]);\n\n const handleExactInSwap = async (runId: number, completedFromEventRef: { current: boolean }) => {\n const fromToken = state.inputs.fromToken;\n const toToken = state.inputs.toToken;\n const fromAmount = state.inputs.fromAmount;\n const toChainID = state.inputs.toChainID;\n const fromChainID = state.inputs.fromChainID;\n\n if (\n !nexusSDK ||\n !areExactInInputsValid ||\n !fromToken ||\n !toToken ||\n !fromAmount ||\n !toChainID ||\n !fromChainID\n )\n return;\n\n const sourceBalance = swapBalance\n ?.flatMap((token) => token.chainBalances ?? [])\n ?.find(\n (chain) =>\n chain.chain?.id === fromChainID &&\n normalizeAddress(chain.contractAddress) ===\n normalizeAddress(fromToken.contractAddress),\n );\n if (\n !sourceBalance ||\n Number.parseFloat(sourceBalance.balance ?? \"0\") <= 0\n ) {\n throw new Error(\n \"No balance found for this wallet on supported source chains.\",\n );\n }\n\n const amountBigInt = parseUnits(fromAmount, fromToken.decimals);\n\n // v2: SwapExactInParams — sources replaces `from`, amountRaw in source\n const swapInput: SwapExactInParams = {\n sources: [\n {\n chainId: fromChainID,\n tokenAddress: fromToken.contractAddress,\n amountRaw: amountBigInt,\n },\n ],\n toChainId: toChainID,\n toTokenAddress: toToken.tokenAddress,\n };\n\n // v2: returns SuccessfulSwapResult directly; throws on error (no .success wrapper)\n await nexusSDK.swapWithExactIn(swapInput, {\n onEvent: (event) => handleSwapEvent(event, runId, completedFromEventRef),\n hooks: {\n onIntent: (data) => {\n swapIntent.current = data;\n },\n },\n });\n };\n\n const handleExactOutSwap = async (runId: number, completedFromEventRef: { current: boolean }) => {\n const toToken = state.inputs.toToken;\n const toAmount = state.inputs.toAmount;\n const toChainID = state.inputs.toChainID;\n\n if (\n !nexusSDK ||\n !areExactOutInputsValid ||\n !toToken ||\n !toAmount ||\n !toChainID\n )\n return;\n if (swapBalance && exactOutSourceOptions.length === 0) {\n throw new Error(\n \"No balance found for this wallet on supported source chains.\",\n );\n }\n if (!exactOutFromSources || exactOutFromSources.length === 0) {\n throw new Error(\"Select at least one source with available balance.\");\n }\n\n const amountBigInt = parseUnits(toAmount, toToken.decimals);\n\n // v2: SwapExactOutParams — toAmountRaw replaces toAmount\n const swapInput: SwapExactOutParams = {\n toAmountRaw: amountBigInt,\n toChainId: toChainID,\n toTokenAddress: toToken.tokenAddress,\n ...(exactOutFromSources ? { sources: exactOutFromSources } : {}),\n };\n\n // v2: returns SuccessfulSwapResult directly; throws on error\n await nexusSDK.swapWithExactOut(swapInput, {\n onEvent: (event) => handleSwapEvent(event, runId, completedFromEventRef),\n hooks: {\n onIntent: (data) => {\n swapIntent.current = data;\n },\n },\n });\n };\n\n const runSwap = async (runId: number) => {\n if (!nexusSDK || !areInputsValid || !swapBalance) return;\n\n // Used by handleSwapEvent to signal completion without waiting for the promise\n const completedFromEventRef = { current: false };\n\n try {\n onStart?.();\n dispatch({ type: \"setStatus\", payload: \"simulating\" });\n dispatch({ type: \"setError\", payload: null });\n seed(SWAP_EXPECTED_STEPS);\n\n if (state.swapMode === \"exactOut\") {\n setAppliedExactOutSelectionKey(exactOutSelectionKey);\n } else {\n setAppliedExactOutSelectionKey(\"ALL\");\n }\n\n if (state.swapMode === \"exactIn\") {\n await handleExactInSwap(runId, completedFromEventRef);\n } else {\n await handleExactOutSwap(runId, completedFromEventRef);\n }\n\n if (swapRunIdRef.current !== runId) return;\n if (!completedFromEventRef.current) {\n // Fallback: SDK resolved but terminal event never arrived (single-step flows)\n dispatch({ type: \"setStatus\", payload: \"success\" });\n onComplete?.(swapIntent.current?.intent?.destination?.amount);\n await fetchBalance();\n }\n } catch (error) {\n if (swapRunIdRef.current !== runId) return;\n if (completedFromEventRef.current) return; // event already handled failure\n const { message } = handleNexusError(error);\n dispatch({ type: \"setStatus\", payload: \"error\" });\n dispatch({ type: \"setError\", payload: message });\n onError?.(message);\n swapIntent.current?.deny();\n swapIntent.current = null;\n setExactOutSelectedKeys(null);\n setAppliedExactOutSelectionKey(\"ALL\");\n setUpdatingExactOutSources(false);\n lastSyncedIntentSourcesSignatureRef.current = \"\";\n lastSyncedIntentSelectionKeyRef.current = \"\";\n void fetchBalance();\n }\n };\n\n const startSwap = () => {\n swapRunIdRef.current += 1;\n const runId = swapRunIdRef.current;\n void runSwap(runId);\n return runId;\n };\n\n const debouncedSwapStart = useDebouncedCallback(startSwap, 1200);\n\n const reset = () => {\n // invalidate any in-flight swap run\n swapRunIdRef.current += 1;\n dispatch({ type: \"reset\" });\n resetSteps();\n swapIntent.current?.deny();\n swapIntent.current = null;\n setExactOutSelectedKeys(null);\n setAppliedExactOutSelectionKey(\"ALL\");\n setUpdatingExactOutSources(false);\n lastSyncedIntentSourcesSignatureRef.current = \"\";\n lastSyncedIntentSelectionKeyRef.current = \"\";\n };\n\n useEffect(() => {\n if (state.swapMode !== \"exactOut\") return;\n if (state.status !== \"simulating\") return;\n if (exactOutSourceOptions.length === 0) return;\n\n const runId = swapRunIdRef.current;\n let cancelled = false;\n\n void (async () => {\n const start = Date.now();\n while (!cancelled && Date.now() - start < 10000) {\n if (swapRunIdRef.current !== runId) return;\n\n const intentSources = swapIntent.current?.intent?.sources ?? [];\n if (intentSources.length > 0) {\n syncExactOutSelectionFromIntent(intentSources);\n return;\n }\n\n // eslint-disable-next-line no-await-in-loop\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [\n currentIntentSourcesSignature,\n exactOutSourceOptions,\n state.status,\n state.swapMode,\n syncExactOutSelectionFromIntent,\n swapIntent,\n ]);\n\n const availableBalance = useMemo(() => {\n if (\n !nexusSDK ||\n !swapBalance ||\n !state.inputs?.fromToken ||\n !state.inputs?.fromChainID\n )\n return undefined;\n return (\n swapBalance\n ?.flatMap((token) => token.chainBalances ?? [])\n ?.find(\n (chain) =>\n chain.chain?.id === state.inputs?.fromChainID &&\n normalizeAddress(chain.contractAddress) ===\n normalizeAddress(state.inputs?.fromToken?.contractAddress ?? \"\"),\n ) ?? undefined\n );\n }, [\n state.inputs?.fromToken,\n state.inputs?.fromChainID,\n swapBalance,\n nexusSDK,\n ]);\n\n const destinationBalance = useMemo(() => {\n if (\n !nexusSDK ||\n !swapBalance ||\n !state.inputs?.toToken ||\n !state.inputs?.toChainID\n )\n return undefined;\n return (\n swapBalance\n ?.flatMap((token) => token.chainBalances ?? [])\n ?.find(\n (chain) =>\n chain.chain?.id === state?.inputs?.toChainID &&\n normalizeAddress(chain.contractAddress) ===\n normalizeAddress(state?.inputs?.toToken?.tokenAddress ?? \"\"),\n ) ?? undefined\n );\n }, [state?.inputs?.toToken, state?.inputs?.toChainID, swapBalance, nexusSDK]);\n\n const availableStables = useMemo(() => {\n if (!nexusSDK || !swapBalance) return [];\n const stableSymbols = new Set([\"USDT\", \"USDC\", \"ETH\", \"DAI\", \"WBTC\"]);\n // v2: breakdown has no .symbol — use token.symbol from the parent TokenBalance\n const filteredToken = swapBalance.filter((token) =>\n stableSymbols.has(token.symbol.toUpperCase()),\n );\n return filteredToken ?? [];\n }, [swapBalance, nexusSDK]);\n\n const formatBalance = (\n balance?: string | number,\n symbol?: string,\n decimals?: number,\n ) => {\n if (!balance || !symbol || !decimals) return undefined;\n return formatTokenBalance(balance, {\n symbol: symbol,\n decimals: decimals,\n });\n };\n\n useEffect(() => {\n if (!swapBalance) {\n fetchBalance();\n }\n }, [swapBalance]);\n\n useEffect(() => {\n // Check validity based on current swap mode\n const isValidForCurrentMode =\n state.swapMode === \"exactIn\"\n ? areExactInInputsValid &&\n state?.inputs?.fromAmount &&\n state?.inputs?.fromChainID &&\n state?.inputs?.fromToken &&\n state?.inputs?.toChainID &&\n state?.inputs?.toToken\n : areExactOutInputsValid &&\n state?.inputs?.toAmount &&\n state?.inputs?.toChainID &&\n state?.inputs?.toToken;\n\n if (!isValidForCurrentMode) {\n swapIntent.current?.deny();\n swapIntent.current = null;\n lastSyncedIntentSourcesSignatureRef.current = \"\";\n lastSyncedIntentSelectionKeyRef.current = \"\";\n return;\n }\n if (state.status === \"idle\") {\n debouncedSwapStart();\n }\n }, [\n state.inputs,\n state.swapMode,\n areExactInInputsValid,\n areExactOutInputsValid,\n state.status,\n ]);\n\n const refreshSimulation = async () => {\n try {\n const updated = await swapIntent.current?.refresh();\n if (updated) {\n swapIntent.current!.intent = updated;\n }\n } catch (e) {\n console.error(e);\n }\n };\n\n usePolling(\n state.status === \"simulating\" && Boolean(swapIntent.current),\n async () => {\n await refreshSimulation();\n },\n 15000,\n );\n\n const continueSwap = useCallback(async () => {\n if (state.status !== \"simulating\") return;\n\n if (state.swapMode !== \"exactOut\" || !isExactOutSourceSelectionDirty) {\n dispatch({ type: \"setStatus\", payload: \"swapping\" });\n swapIntent.current?.allow();\n return;\n }\n\n if (!nexusSDK || !areInputsValid) return;\n\n setUpdatingExactOutSources(true);\n try {\n const previousIntent = swapIntent.current;\n swapRunIdRef.current += 1;\n const runId = swapRunIdRef.current;\n\n previousIntent?.deny();\n\n void runSwap(runId);\n const start = Date.now();\n while (Date.now() - start < 10000) {\n if (swapRunIdRef.current !== runId) return;\n const nextIntent = swapIntent.current;\n const sourcesReady =\n nextIntent &&\n nextIntent !== previousIntent &&\n (nextIntent.intent.sources?.length ?? 0) > 0;\n if (sourcesReady) break;\n // eslint-disable-next-line no-await-in-loop\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n\n if (swapRunIdRef.current !== runId) return;\n const nextIntent = swapIntent.current;\n if (!nextIntent || nextIntent === previousIntent) return;\n if ((nextIntent.intent.sources?.length ?? 0) === 0) return;\n syncExactOutSelectionFromIntent(nextIntent.intent.sources, true);\n // Updated sources are now reflected in the intent. Wait for explicit user\n // confirmation before proceeding.\n return;\n } finally {\n setUpdatingExactOutSources(false);\n }\n }, [\n areInputsValid,\n isExactOutSourceSelectionDirty,\n nexusSDK,\n runSwap,\n syncExactOutSelectionFromIntent,\n state.status,\n state.swapMode,\n swapIntent,\n ]);\n\n return {\n status: state.status,\n inputs: state.inputs,\n swapMode: state.swapMode,\n setSwapMode: (mode: SwapMode) =>\n dispatch({ type: \"setSwapMode\", payload: mode }),\n setStatus: (status: TransactionStatus) =>\n dispatch({ type: \"setStatus\", payload: status }),\n setInputs: (inputs: Partial) => {\n if (state.status === \"error\") {\n dispatch({ type: \"setError\", payload: null });\n dispatch({ type: \"setStatus\", payload: \"idle\" });\n }\n dispatch({ type: \"setInputs\", payload: inputs });\n },\n txError: state.error,\n setTxError: (error: string | null) =>\n dispatch({ type: \"setError\", payload: error }),\n availableBalance,\n availableStables,\n destinationBalance,\n formatBalance,\n steps,\n explorerUrls: state.explorerUrls,\n handleSwap: startSwap,\n continueSwap,\n exactOutSourceOptions,\n exactOutSelectedKeys: effectiveExactOutSelectedKeys,\n toggleExactOutSource,\n isExactOutSourceSelectionDirty,\n updatingExactOutSources,\n reset,\n areInputsValid,\n };\n};\n\nexport default useSwaps;\n", "type": "registry:component", "target": "components/swaps/hooks/useSwaps.ts" }, { "path": "registry/nexus-elements/swaps/swap-widget.tsx", - "content": "\"use client\";\n\nimport { useCallback, useMemo, useRef } from \"react\";\nimport { ArrowDownUp, Loader2, RefreshCcw } from \"lucide-react\";\nimport { useNexus } from \"../nexus/NexusProvider\";\nimport { Button } from \"../ui/button\";\nimport { Separator } from \"../ui/separator\";\nimport useHover from \"./hooks/useHover\";\nimport SourceContainer from \"./components/source-container\";\nimport DestinationContainer from \"./components/destination-container\";\nimport ViewTransaction from \"./components/view-transaction\";\nimport useSwaps, { type SwapInputs } from \"./hooks/useSwaps\";\n\nfunction SwapWidget({\n onComplete,\n onStart,\n onError,\n}: Readonly<{\n onComplete?: (amount?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n}>) {\n const sourceContainer = useRef(null);\n const destinationContainer = useRef(null);\n const { nexusSDK, swapIntent, swapBalance, fetchSwapBalance, getFiatValue } =\n useNexus();\n const {\n status,\n inputs,\n swapMode,\n setSwapMode,\n txError,\n setInputs,\n setTxError,\n steps,\n reset,\n explorerUrls,\n availableBalance,\n availableStables,\n formatBalance,\n destinationBalance,\n continueSwap,\n exactOutSourceOptions,\n exactOutSelectedKeys,\n toggleExactOutSource,\n isExactOutSourceSelectionDirty,\n updatingExactOutSources,\n } = useSwaps({\n nexusSDK,\n swapIntent,\n swapBalance,\n fetchBalance: fetchSwapBalance,\n onComplete,\n onStart,\n onError,\n });\n const sourceHovered = useHover(sourceContainer);\n const destinationHovered = useHover(destinationContainer);\n\n const handleInputSwitch = useCallback(() => {\n swapIntent.current?.deny();\n swapIntent.current = null;\n\n // Always reset to exactIn mode and clear amounts when switching\n setSwapMode(\"exactIn\");\n\n if (!inputs?.fromToken || !inputs?.toToken) {\n const switched: SwapInputs = {\n fromChainID: inputs.toChainID,\n toChainID: inputs.fromChainID,\n fromToken: undefined,\n toToken: undefined,\n fromAmount: undefined,\n toAmount: undefined,\n };\n setInputs(switched);\n return;\n }\n const isValidSource = swapBalance?.some((asset) =>\n (asset.breakdown ?? []).some(\n (entry) =>\n entry.chain?.id === inputs.toChainID &&\n entry.contractAddress.toLowerCase() ===\n inputs.toToken?.tokenAddress?.toLowerCase(),\n ),\n );\n if (!isValidSource) {\n const switched: SwapInputs = {\n fromChainID: inputs.toChainID,\n toToken: {\n tokenAddress: inputs.fromToken?.contractAddress,\n decimals: inputs.fromToken?.decimals,\n symbol: inputs.fromToken?.symbol,\n name: inputs.fromToken?.name,\n logo: inputs.fromToken?.logo,\n },\n fromToken: undefined,\n toChainID: inputs.fromChainID,\n fromAmount: undefined,\n toAmount: undefined,\n };\n setInputs(switched);\n return;\n }\n const switched: SwapInputs = {\n fromToken: {\n contractAddress: inputs.toToken?.tokenAddress,\n decimals: inputs.toToken?.decimals,\n symbol: inputs.toToken?.symbol,\n name: inputs.toToken?.name,\n logo: inputs.toToken?.logo,\n },\n fromChainID: inputs.toChainID,\n toToken: {\n tokenAddress: inputs.fromToken?.contractAddress,\n decimals: inputs.fromToken?.decimals,\n symbol: inputs.fromToken?.symbol,\n name: inputs.fromToken?.name,\n logo: inputs.fromToken?.logo,\n },\n toChainID: inputs.fromChainID,\n fromAmount: undefined,\n toAmount: undefined,\n };\n setInputs(switched);\n }, [inputs, swapIntent, swapBalance, setSwapMode, setInputs]);\n\n const buttonIcons = useMemo(() => {\n if (status === \"simulating\") {\n return ;\n }\n return swapMode === \"exactIn\" ? (\n \n ) : (\n \n );\n }, [status, swapMode]);\n\n return (\n <>\n
\n
\n \n \n
\n\n {/* Swap arrow / mode toggle */}\n \n {buttonIcons}\n \n \n\n {/* Buy section */}\n \n \n
\n \n {status === \"error\" && (\n

{txError}

\n )}\n \n\n {status !== \"idle\" && (\n \n )}\n \n );\n}\n\nexport default SwapWidget;\n", + "content": "\"use client\";\n\nimport { useCallback, useMemo, useRef } from \"react\";\nimport { ArrowDownUp, Loader2, RefreshCcw } from \"lucide-react\";\nimport { useNexus } from \"../nexus/NexusProvider\";\nimport { Button } from \"../ui/button\";\nimport { Separator } from \"../ui/separator\";\nimport useHover from \"./hooks/useHover\";\nimport SourceContainer from \"./components/source-container\";\nimport DestinationContainer from \"./components/destination-container\";\nimport ViewTransaction from \"./components/view-transaction\";\nimport useSwaps, { type SwapInputs } from \"./hooks/useSwaps\";\n\nfunction SwapWidget({\n onComplete,\n onStart,\n onError,\n}: Readonly<{\n onComplete?: (amount?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n}>) {\n const sourceContainer = useRef(null);\n const destinationContainer = useRef(null);\n const { nexusSDK, swapWidgetIntent, swapBalance, fetchSwapBalance, getFiatValue } =\n useNexus();\n const {\n status,\n inputs,\n swapMode,\n setSwapMode,\n txError,\n setInputs,\n setTxError,\n steps,\n reset,\n explorerUrls,\n availableBalance,\n availableStables,\n formatBalance,\n destinationBalance,\n continueSwap,\n exactOutSourceOptions,\n exactOutSelectedKeys,\n toggleExactOutSource,\n isExactOutSourceSelectionDirty,\n updatingExactOutSources,\n } = useSwaps({\n nexusSDK,\n swapIntent: swapWidgetIntent,\n swapBalance,\n fetchBalance: fetchSwapBalance,\n onComplete,\n onStart,\n onError,\n });\n const sourceHovered = useHover(sourceContainer);\n const destinationHovered = useHover(destinationContainer);\n\n const handleInputSwitch = useCallback(() => {\n swapWidgetIntent.current?.deny();\n swapWidgetIntent.current = null;\n\n // Always reset to exactIn mode and clear amounts when switching\n setSwapMode(\"exactIn\");\n\n if (!inputs?.fromToken || !inputs?.toToken) {\n const switched: SwapInputs = {\n fromChainID: inputs.toChainID,\n toChainID: inputs.fromChainID,\n fromToken: undefined,\n toToken: undefined,\n fromAmount: undefined,\n toAmount: undefined,\n };\n setInputs(switched);\n return;\n }\n const isValidSource = swapBalance?.some((asset) =>\n (asset.chainBalances ?? []).some(\n (entry) =>\n entry.chain?.id === inputs.toChainID &&\n entry.contractAddress.toLowerCase() ===\n inputs.toToken?.tokenAddress?.toLowerCase(),\n ),\n );\n if (!isValidSource) {\n const switched: SwapInputs = {\n fromChainID: inputs.toChainID,\n toToken: {\n tokenAddress: inputs.fromToken?.contractAddress,\n decimals: inputs.fromToken?.decimals,\n symbol: inputs.fromToken?.symbol,\n name: inputs.fromToken?.name,\n logo: inputs.fromToken?.logo,\n },\n fromToken: undefined,\n toChainID: inputs.fromChainID,\n fromAmount: undefined,\n toAmount: undefined,\n };\n setInputs(switched);\n return;\n }\n const switched: SwapInputs = {\n fromToken: {\n contractAddress: inputs.toToken?.tokenAddress,\n decimals: inputs.toToken?.decimals,\n symbol: inputs.toToken?.symbol,\n name: inputs.toToken?.name,\n logo: inputs.toToken?.logo,\n },\n fromChainID: inputs.toChainID,\n toToken: {\n tokenAddress: inputs.fromToken?.contractAddress,\n decimals: inputs.fromToken?.decimals,\n symbol: inputs.fromToken?.symbol,\n name: inputs.fromToken?.name,\n logo: inputs.fromToken?.logo,\n },\n toChainID: inputs.fromChainID,\n fromAmount: undefined,\n toAmount: undefined,\n };\n setInputs(switched);\n }, [inputs, swapWidgetIntent, swapBalance, setSwapMode, setInputs]);\n\n const buttonIcons = useMemo(() => {\n if (status === \"simulating\") {\n return ;\n }\n return swapMode === \"exactIn\" ? (\n \n ) : (\n \n );\n }, [status, swapMode]);\n\n return (\n <>\n
\n
\n \n \n
\n\n {/* Swap arrow / mode toggle */}\n \n {buttonIcons}\n \n \n\n {/* Buy section */}\n \n \n
\n \n {status === \"error\" && (\n

{txError}

\n )}\n \n\n {status !== \"idle\" && (\n \n )}\n \n );\n}\n\nexport default SwapWidget;\n", "type": "registry:component", "target": "components/swaps/swap-widget.tsx" }, @@ -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-sdk-v2\";\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.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.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.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 // v2: RFF_FEE_EXPIRED was removed; use string key for forward compat\n [\"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 );\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 as unknown as { data?: { context?: unknown } })?.data?.context,\n details: (err as unknown as { data?: { details?: unknown } })?.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" }, @@ -168,13 +168,13 @@ }, { "path": "registry/nexus-elements/common/hooks/useTransactionExecution.ts", - "content": "import {\n type BridgeStepType,\n NEXUS_EVENTS,\n type NexusSDK,\n type OnAllowanceHookData,\n type OnIntentHookData,\n} from \"@avail-project/nexus-core\";\nimport {\n type Dispatch,\n type RefObject,\n type SetStateAction,\n useCallback,\n useRef,\n} from \"react\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport {\n type SourceSelectionValidation,\n type TransactionFlowEvent,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n} from \"../types/transaction-flow\";\n\ninterface NexusErrorInfo {\n code: string;\n message: string;\n context?: unknown;\n details?: unknown;\n}\n\ntype NexusErrorHandler = (error: unknown) => NexusErrorInfo;\n\ninterface UseTransactionExecutionProps {\n operationName: \"bridge\" | \"transfer\";\n nexusSDK: NexusSDK | null;\n intent: RefObject;\n allowance: RefObject;\n inputs: TransactionFlowInputs;\n configuredMaxAmount?: string;\n allAvailableSourceChainIds: number[];\n sourceChainsForSdk?: number[];\n sourceSelectionKey: string;\n sourceSelection: SourceSelectionValidation;\n loading: boolean;\n txError: string | null;\n areInputsValid: boolean;\n executeTransaction: TransactionFlowExecutor;\n getMaxForCurrentSelection: () => Promise;\n onStepsList: (steps: BridgeStepType[]) => void;\n onStepComplete: (step: BridgeStepType) => void;\n resetSteps: () => void;\n setStatus: (status: TransactionStatus) => void;\n resetInputs: () => void;\n setRefreshing: Dispatch>;\n setIsDialogOpen: Dispatch>;\n setTxError: Dispatch>;\n setLastExplorerUrl: Dispatch>;\n setSelectedSourceChains: Dispatch>;\n setAppliedSourceSelectionKey: Dispatch>;\n stopwatch: {\n start: () => void;\n stop: () => void;\n reset: () => void;\n };\n handleNexusError: NexusErrorHandler;\n onStart?: () => void;\n onComplete?: (explorerUrl?: string) => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n notifyHistoryRefresh?: () => void;\n}\n\nexport function useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n}: UseTransactionExecutionProps) {\n const commitLockRef = useRef(false);\n const runIdRef = useRef(0);\n\n const refreshIntent = async (options?: { reportError?: boolean }) => {\n if (!intent.current) return false;\n const activeRunId = runIdRef.current;\n setRefreshing(true);\n try {\n const updated = await intent.current.refresh(sourceChainsForSdk);\n if (activeRunId !== runIdRef.current) return false;\n if (updated) {\n intent.current.intent = updated;\n }\n setAppliedSourceSelectionKey(sourceSelectionKey);\n return true;\n } catch (error) {\n if (activeRunId !== runIdRef.current) return false;\n console.error(\"Transaction failed:\", error);\n if (options?.reportError) {\n const message = \"Unable to refresh source selection. Please try again.\";\n setTxError(message);\n onError?.(message);\n }\n return false;\n } finally {\n if (activeRunId === runIdRef.current) {\n setRefreshing(false);\n }\n }\n };\n\n const onSuccess = async (explorerUrl?: string) => {\n stopwatch.stop();\n setStatus(\"success\");\n onComplete?.(explorerUrl);\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n await fetchBalance();\n notifyHistoryRefresh?.();\n };\n\n const handleTransaction = async () => {\n if (commitLockRef.current) return;\n commitLockRef.current = true;\n const currentRunId = ++runIdRef.current;\n let didEnterExecutingState = false;\n const cleanupSupersededExecution = () => {\n if (!didEnterExecutingState) return;\n setRefreshing(false);\n setIsDialogOpen(false);\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n setStatus(\"idle\");\n };\n\n try {\n if (\n !inputs?.amount ||\n !inputs?.recipient ||\n !inputs?.chain ||\n !inputs?.token\n ) {\n console.error(\"Missing required inputs\");\n return;\n }\n if (!nexusSDK) {\n const message = \"Nexus SDK not initialized\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n const parsedAmount = Number(inputs.amount);\n if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) {\n const message = \"Enter a valid amount greater than 0.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n const amountBigInt = nexusSDK.convertTokenReadableAmountToBigInt(\n inputs.amount,\n inputs.token,\n inputs.chain,\n );\n\n if (configuredMaxAmount) {\n const configuredMaxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n configuredMaxAmount,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > configuredMaxRaw) {\n const message = `Amount exceeds maximum limit of ${configuredMaxAmount} ${inputs.token}.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n }\n\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (currentRunId !== runIdRef.current) return;\n if (!maxForCurrentSelection) {\n const message = `Unable to determine max ${operationName} amount for selected sources. Please try again.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n const maxForSelectionRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxForCurrentSelection,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > maxForSelectionRaw) {\n const message = `Selected sources can provide up to ${maxForCurrentSelection} ${inputs.token}. Reduce amount or enable more sources.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n setStatus(\"executing\");\n didEnterExecutingState = true;\n setTxError(null);\n onStart?.();\n setLastExplorerUrl(\"\");\n setAppliedSourceSelectionKey(sourceSelectionKey);\n\n const onEvent = (event: TransactionFlowEvent) => {\n if (currentRunId !== runIdRef.current) return;\n if (event.name === NEXUS_EVENTS.STEPS_LIST) {\n const list = Array.isArray(event.args) ? event.args : [];\n onStepsList(list as BridgeStepType[]);\n }\n if (event.name === NEXUS_EVENTS.STEP_COMPLETE) {\n if (\n !Array.isArray(event.args) &&\n \"type\" in event.args &&\n event.args.type === \"INTENT_HASH_SIGNED\"\n ) {\n stopwatch.start();\n }\n if (!Array.isArray(event.args)) {\n onStepComplete(event.args as BridgeStepType);\n }\n }\n };\n\n const transactionResult = await executeTransaction({\n token: inputs.token,\n amount: amountBigInt,\n toChainId: inputs.chain,\n recipient: inputs.recipient,\n sourceChains: sourceChainsForSdk,\n onEvent,\n });\n\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution();\n return;\n }\n if (!transactionResult) {\n throw new Error(\"Transaction rejected by user\");\n }\n setLastExplorerUrl(transactionResult.explorerUrl);\n await onSuccess(transactionResult.explorerUrl);\n } catch (error) {\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution();\n return;\n }\n const { message, code, context, details } = handleNexusError(error);\n console.error(`Fast ${operationName} transaction failed:`, {\n code,\n message,\n context,\n details,\n });\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n setTxError(message);\n onError?.(message);\n setIsDialogOpen(false);\n setSelectedSourceChains(null);\n setRefreshing(false);\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n void fetchBalance();\n setStatus(\"error\");\n } finally {\n commitLockRef.current = false;\n }\n };\n\n const reset = () => {\n runIdRef.current += 1;\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setStatus(\"idle\");\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n };\n\n const startTransaction = () => {\n if (!intent.current) return;\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (sourceSelection.isBelowRequired && inputs?.token) {\n const message = `Selected sources are not enough. Add ${sourceSelection.missingToProceed} ${inputs.token} more to make this transaction.`;\n setTxError(message);\n onError?.(message);\n return;\n }\n void (async () => {\n const refreshed = await refreshIntent({ reportError: true });\n if (!refreshed || !intent.current) return;\n intent.current.allow();\n setIsDialogOpen(true);\n setTxError(null);\n })();\n };\n\n const commitAmount = async () => {\n if (intent.current || loading || txError || !areInputsValid) return;\n await handleTransaction();\n };\n\n const invalidatePendingExecution = useCallback(() => {\n runIdRef.current += 1;\n if (intent.current) {\n intent.current.deny();\n intent.current = null;\n }\n setRefreshing(false);\n setAppliedSourceSelectionKey(\"ALL\");\n }, [intent, setAppliedSourceSelectionKey, setRefreshing]);\n\n return {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n };\n}\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type { OnAllowanceHookData, OnIntentHookData } from \"@avail-project/nexus-sdk-v2\";\nimport {\n type Dispatch,\n type RefObject,\n type SetStateAction,\n useCallback,\n useRef,\n} from \"react\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport {\n type SourceSelectionValidation,\n type TransactionFlowEvent,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n} from \"../types/transaction-flow\";\n\ntype NexusClient = ReturnType;\n\ninterface NexusErrorInfo {\n code: string;\n message: string;\n context?: unknown;\n details?: unknown;\n}\n\ntype NexusErrorHandler = (error: unknown) => NexusErrorInfo;\n\n// v2 plan_progress step types for bridge\nconst BRIDGE_STEP_INTENT_SIGNED = \"request_signing\";\n\ninterface UseTransactionExecutionProps {\n operationName: \"bridge\" | \"transfer\";\n nexusSDK: NexusClient | null;\n intent: RefObject;\n allowance: RefObject;\n inputs: TransactionFlowInputs;\n configuredMaxAmount?: string;\n allAvailableSourceChainIds: number[];\n sourceChainsForSdk?: number[];\n sourceSelectionKey: string;\n sourceSelection: SourceSelectionValidation;\n loading: boolean;\n txError: string | null;\n areInputsValid: boolean;\n executeTransaction: TransactionFlowExecutor;\n getMaxForCurrentSelection: () => Promise;\n onStepsList: (steps: { typeID?: string; type?: string; [key: string]: unknown }[]) => void;\n onStepComplete: (step: { typeID?: string; type?: string; [key: string]: unknown }) => void;\n resetSteps: () => void;\n setStatus: (status: TransactionStatus) => void;\n resetInputs: () => void;\n setRefreshing: Dispatch>;\n setIsDialogOpen: Dispatch>;\n setTxError: Dispatch>;\n setLastExplorerUrl: Dispatch>;\n setSelectedSourceChains: Dispatch>;\n setAppliedSourceSelectionKey: Dispatch>;\n stopwatch: {\n start: () => void;\n stop: () => void;\n reset: () => void;\n };\n handleNexusError: NexusErrorHandler;\n onStart?: () => void;\n onComplete?: (explorerUrl?: string) => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n notifyHistoryRefresh?: () => void;\n}\n\nexport function useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n}: UseTransactionExecutionProps) {\n const commitLockRef = useRef(false);\n const runIdRef = useRef(0);\n\n const refreshIntent = async (options?: { reportError?: boolean }) => {\n if (!intent.current) return false;\n const activeRunId = runIdRef.current;\n setRefreshing(true);\n try {\n const updated = await intent.current.refresh(sourceChainsForSdk);\n if (activeRunId !== runIdRef.current) return false;\n if (updated) {\n intent.current.intent = updated;\n }\n setAppliedSourceSelectionKey(sourceSelectionKey);\n return true;\n } catch (error) {\n if (activeRunId !== runIdRef.current) return false;\n console.error(\"Transaction failed:\", error);\n if (options?.reportError) {\n const message = \"Unable to refresh source selection. Please try again.\";\n setTxError(message);\n onError?.(message);\n }\n return false;\n } finally {\n if (activeRunId === runIdRef.current) {\n setRefreshing(false);\n }\n }\n };\n\n const onSuccess = async (explorerUrl?: string) => {\n stopwatch.stop();\n setStatus(\"success\");\n onComplete?.(explorerUrl);\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n await fetchBalance();\n notifyHistoryRefresh?.();\n };\n\n const handleTransaction = async () => {\n if (commitLockRef.current) return;\n commitLockRef.current = true;\n const currentRunId = ++runIdRef.current;\n let didEnterExecutingState = false;\n // Declared here (outside try/catch) so both the event handler and the catch block\n // can read/write it — prevents the catch from clobbering event-driven completions\n let completedFromEvent = false;\n const cleanupSupersededExecution = () => {\n if (!didEnterExecutingState) return;\n // Don't tear down the dialog if an event already handled success/failure —\n // resetInputs() inside onSuccess triggers invalidatePendingExecution which\n // increments runIdRef, making this branch fire spuriously.\n if (completedFromEvent) return;\n setRefreshing(false);\n setIsDialogOpen(false);\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n setStatus(\"idle\");\n };\n\n\n try {\n if (\n !inputs?.amount ||\n !inputs?.recipient ||\n !inputs?.chain ||\n !inputs?.token\n ) {\n console.error(\"Missing required inputs\");\n return;\n }\n if (!nexusSDK) {\n const message = \"Nexus SDK not initialized\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n const parsedAmount = Number(inputs.amount);\n if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) {\n const message = \"Enter a valid amount greater than 0.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n // v2: convertTokenReadableAmountToBigInt(amount, tokenSymbol, chainId)\n const amountBigInt = nexusSDK.convertTokenReadableAmountToBigInt(\n inputs.amount,\n inputs.token,\n inputs.chain,\n );\n\n if (configuredMaxAmount) {\n const configuredMaxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n configuredMaxAmount,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > configuredMaxRaw) {\n const message = `Amount exceeds maximum limit of ${configuredMaxAmount} ${inputs.token}.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n }\n\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (currentRunId !== runIdRef.current) return;\n if (!maxForCurrentSelection) {\n const message = `Unable to determine max ${operationName} amount for selected sources. Please try again.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n const maxForSelectionRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxForCurrentSelection,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > maxForSelectionRaw) {\n const message = `Selected sources can provide up to ${maxForCurrentSelection} ${inputs.token}. Reduce amount or enable more sources.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n setStatus(\"executing\");\n didEnterExecutingState = true;\n setTxError(null);\n onStart?.();\n setLastExplorerUrl(\"\");\n setAppliedSourceSelectionKey(sourceSelectionKey);\n\n // Terminal step types — when state:\"completed\" fires on these, the operation is done\n const TERMINAL_STEP_TYPES = new Set([\n \"bridge_fill\", // bridge & transfer final fill\n \"destination_swap\", // swap final step\n ]);\n\n // v2 onEvent uses typed discriminated union: { type, ... }\n const onEvent = (event: TransactionFlowEvent) => {\n if (currentRunId !== runIdRef.current) return;\n\n if (event.type === \"plan_preview\") {\n // Seed UI with the step list from the plan\n type StepShape = { typeID?: string; type?: string; [key: string]: unknown };\n const steps = ((event as { type: string; plan: { steps: StepShape[] } }).plan?.steps ?? []) as StepShape[];\n onStepsList(steps);\n }\n\n if (event.type === \"plan_progress\") {\n const progressEvent = event as {\n type: string;\n stepType: string;\n state: string;\n step: { typeID?: string; type?: string; [key: string]: unknown };\n error?: string;\n };\n\n // Always mark step as complete/updated in UI\n onStepComplete(progressEvent.step);\n\n const isTerminal = TERMINAL_STEP_TYPES.has(progressEvent.stepType);\n\n if (progressEvent.state === \"failed\") {\n // Any step failure → abort\n if (!completedFromEvent) {\n completedFromEvent = true;\n const errorMessage = progressEvent.error ?? \"Transaction failed\";\n stopwatch.stop();\n setTxError(errorMessage);\n onError?.(errorMessage);\n setStatus(\"error\");\n }\n return;\n }\n\n if (isTerminal && progressEvent.state === \"completed\") {\n // Terminal step completed → success\n if (!completedFromEvent) {\n completedFromEvent = true;\n stopwatch.stop();\n // explorerUrl is on the event itself, not the step object\n const explorerUrl = (event as { explorerUrl?: string }).explorerUrl;\n if (explorerUrl) setLastExplorerUrl(explorerUrl);\n void onSuccess(explorerUrl);\n }\n }\n }\n\n if (event.type === \"status\") {\n const statusEvent = event as { type: string; status: string };\n if (statusEvent.status === \"completed\" && !completedFromEvent) {\n completedFromEvent = true;\n stopwatch.stop();\n void onSuccess(undefined);\n }\n }\n };\n\n const transactionResult = await executeTransaction({\n token: inputs.token,\n amount: amountBigInt,\n toChainId: inputs.chain,\n recipient: inputs.recipient,\n sources: sourceChainsForSdk,\n onEvent,\n });\n\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution(); // no-op when completedFromEvent=true\n if (!completedFromEvent) return; // only bail if not already completed\n // else fall through — still want to capture explorerUrl from the result\n }\n if (!transactionResult) {\n if (!completedFromEvent) {\n throw new Error(\"Transaction rejected by user\");\n }\n // Already handled via events\n return;\n }\n\n // SDK promise resolved — use result for explorerUrl if event-driven success didn't set it\n if (!completedFromEvent) {\n // Fallback: SDK resolved but we never got a terminal event (e.g. single-step flows)\n setLastExplorerUrl(transactionResult.explorerUrl ?? \"\");\n await onSuccess(transactionResult.explorerUrl);\n } else {\n // Event-driven success already ran — capture the explorerUrl from the resolved result\n if (transactionResult.explorerUrl) {\n setLastExplorerUrl(transactionResult.explorerUrl);\n }\n }\n\n } catch (error) {\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution();\n return;\n }\n // If event-driven success/failure already handled this transaction, ignore SDK-level errors\n // (the SDK may throw or return oddly after a successful fill event)\n if (completedFromEvent) return;\n const { message, code, context, details } = handleNexusError(error);\n console.error(`Fast ${operationName} transaction failed:`, {\n code,\n message,\n context,\n details,\n });\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n setTxError(message);\n onError?.(message);\n setIsDialogOpen(false);\n setSelectedSourceChains(null);\n setRefreshing(false);\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n void fetchBalance();\n setStatus(\"error\");\n } finally {\n commitLockRef.current = false;\n }\n };\n\n const reset = () => {\n runIdRef.current += 1;\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setStatus(\"idle\");\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n };\n\n const startTransaction = () => {\n if (!intent.current) return;\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (sourceSelection.isBelowRequired && inputs?.token) {\n const message = `Selected sources are not enough. Add ${sourceSelection.missingToProceed} ${inputs.token} more to make this transaction.`;\n setTxError(message);\n onError?.(message);\n return;\n }\n void (async () => {\n const refreshed = await refreshIntent({ reportError: true });\n if (!refreshed || !intent.current) return;\n intent.current.allow();\n setIsDialogOpen(true);\n // Start the stopwatch AFTER the dialog opens so the isDialogOpen effect\n // does not immediately reset it (the effect only resets when dialog is closed)\n stopwatch.reset();\n stopwatch.start();\n setTxError(null);\n })();\n };\n\n const commitAmount = async () => {\n if (intent.current || loading || txError || !areInputsValid) return;\n await handleTransaction();\n };\n\n const invalidatePendingExecution = useCallback(() => {\n runIdRef.current += 1;\n if (intent.current) {\n intent.current.deny();\n intent.current = null;\n }\n setRefreshing(false);\n setAppliedSourceSelectionKey(\"ALL\");\n }, [intent, setAppliedSourceSelectionKey, setRefreshing]);\n\n return {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n };\n}\n", "type": "registry:component", "target": "components/common/hooks/useTransactionExecution.ts" }, { "path": "registry/nexus-elements/common/hooks/useTransactionFlow.ts", - "content": "import {\n type BridgeStepType,\n type NexusNetwork,\n NexusSDK,\n type OnAllowanceHookData,\n type OnIntentHookData,\n parseUnits,\n type UserAsset,\n} from \"@avail-project/nexus-core\";\nimport {\n useEffect,\n useMemo,\n useCallback,\n useRef,\n useState,\n useReducer,\n type RefObject,\n} from \"react\";\nimport { type Address, isAddress } from \"viem\";\nimport { useNexusError } from \"./useNexusError\";\nimport { useTransactionExecution } from \"./useTransactionExecution\";\nimport { usePolling } from \"./usePolling\";\nimport { useStopwatch } from \"./useStopwatch\";\nimport { useDebouncedCallback } from \"./useDebouncedCallback\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport { useTransactionSteps } from \"../tx/useTransactionSteps\";\nimport {\n type SourceCoverageState,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n type TransactionFlowPrefill,\n type TransactionFlowType,\n} from \"../types/transaction-flow\";\nimport {\n MAX_AMOUNT_DEBOUNCE_MS,\n buildInitialInputs,\n clampAmountToMax,\n formatAmountForDisplay,\n getCoverageDecimals,\n normalizeMaxAmount,\n} from \"../utils/transaction-flow\";\n\ninterface BaseTransactionFlowProps {\n type: TransactionFlowType;\n network: NexusNetwork;\n nexusSDK: NexusSDK | null;\n intent: RefObject;\n allowance: RefObject;\n bridgableBalance: UserAsset[] | null;\n prefill?: TransactionFlowPrefill;\n onComplete?: (explorerUrl?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n maxAmount?: string | number;\n isSourceMenuOpen?: boolean;\n notifyHistoryRefresh?: () => void;\n executeTransaction: TransactionFlowExecutor;\n}\n\nexport interface UseTransactionFlowProps extends BaseTransactionFlowProps {\n connectedAddress?: Address;\n}\n\ntype State = {\n inputs: TransactionFlowInputs;\n status: TransactionStatus;\n};\n\ntype Action =\n | { type: \"setInputs\"; payload: Partial }\n | { type: \"resetInputs\" }\n | { type: \"setStatus\"; payload: TransactionStatus };\n\nexport function useTransactionFlow(props: UseTransactionFlowProps) {\n const {\n type,\n network,\n nexusSDK,\n intent,\n bridgableBalance,\n prefill,\n onComplete,\n onStart,\n onError,\n fetchBalance,\n allowance,\n maxAmount,\n isSourceMenuOpen = false,\n notifyHistoryRefresh,\n executeTransaction,\n } = props;\n\n const connectedAddress = props.connectedAddress;\n const operationName = type === \"bridge\" ? \"bridge\" : \"transfer\";\n const handleNexusError = useNexusError();\n const initialState: State = {\n inputs: buildInitialInputs({ type, network, connectedAddress, prefill }),\n status: \"idle\",\n };\n\n function reducer(state: State, action: Action): State {\n switch (action.type) {\n case \"setInputs\":\n return { ...state, inputs: { ...state.inputs, ...action.payload } };\n case \"resetInputs\":\n return {\n ...state,\n inputs: buildInitialInputs({\n type,\n network,\n connectedAddress,\n prefill,\n }),\n };\n case \"setStatus\":\n return { ...state, status: action.payload };\n default:\n return state;\n }\n }\n\n const [state, dispatch] = useReducer(reducer, initialState);\n const inputs = state.inputs;\n const setInputs = (\n next: TransactionFlowInputs | Partial,\n ) => {\n dispatch({\n type: \"setInputs\",\n payload: next as Partial,\n });\n };\n\n const loading = state.status === \"executing\";\n const [refreshing, setRefreshing] = useState(false);\n const [isDialogOpen, setIsDialogOpen] = useState(false);\n const [txError, setTxError] = useState(null);\n const [lastExplorerUrl, setLastExplorerUrl] = useState(\"\");\n const previousConnectedAddressRef = useRef
(\n connectedAddress,\n );\n const maxAmountRequestIdRef = useRef(0);\n const [selectedSourceChains, setSelectedSourceChains] = useState<\n number[] | null\n >(null);\n const [selectedSourcesMaxAmount, setSelectedSourcesMaxAmount] = useState<\n string | null\n >(null);\n const [appliedSourceSelectionKey, setAppliedSourceSelectionKey] =\n useState(\"ALL\");\n const {\n steps,\n onStepsList,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n const configuredMaxAmount = useMemo(\n () => normalizeMaxAmount(maxAmount),\n [maxAmount],\n );\n\n const areInputsValid = useMemo(() => {\n const hasToken = inputs?.token !== undefined && inputs?.token !== null;\n const hasChain = inputs?.chain !== undefined && inputs?.chain !== null;\n const hasAmount = Boolean(inputs?.amount) && Number(inputs?.amount) > 0;\n const hasValidRecipient =\n Boolean(inputs?.recipient) && isAddress(inputs.recipient as string);\n return hasToken && hasChain && hasAmount && hasValidRecipient;\n }, [inputs]);\n\n const filteredBridgableBalance = useMemo(() => {\n return bridgableBalance?.find((bal) =>\n inputs?.token === \"USDM\"\n ? bal?.symbol === \"USDC\"\n : bal?.symbol === inputs?.token,\n );\n }, [bridgableBalance, inputs?.token]);\n\n const availableSources = useMemo(() => {\n const breakdown = filteredBridgableBalance?.breakdown ?? [];\n const destinationChainId = inputs?.chain;\n const nonZero = breakdown.filter((source) => {\n if (Number.parseFloat(source.balance ?? \"0\") <= 0) return false;\n if (typeof destinationChainId === \"number\") {\n return source.chain.id !== destinationChainId;\n }\n return true;\n });\n const decimals = filteredBridgableBalance?.decimals;\n if (!nexusSDK || typeof decimals !== \"number\") {\n return nonZero.sort(\n (a, b) => Number.parseFloat(b.balance) - Number.parseFloat(a.balance),\n );\n }\n return nonZero.sort((a, b) => {\n try {\n const aRaw = parseUnits(a.balance ?? \"0\", decimals);\n const bRaw = parseUnits(b.balance ?? \"0\", decimals);\n if (aRaw === bRaw) return 0;\n return aRaw > bRaw ? -1 : 1;\n } catch {\n return Number.parseFloat(b.balance) - Number.parseFloat(a.balance);\n }\n });\n }, [\n inputs?.chain,\n filteredBridgableBalance?.breakdown,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n ]);\n\n const allAvailableSourceChainIds = useMemo(\n () => availableSources.map((source) => source.chain.id),\n [availableSources],\n );\n\n const effectiveSelectedSourceChains = useMemo(() => {\n if (selectedSourceChains && selectedSourceChains.length > 0) {\n const availableSet = new Set(allAvailableSourceChainIds);\n const filteredSelection = selectedSourceChains.filter((id) =>\n availableSet.has(id),\n );\n if (filteredSelection.length > 0) {\n return filteredSelection;\n }\n }\n return allAvailableSourceChainIds;\n }, [selectedSourceChains, allAvailableSourceChainIds]);\n\n const sourceChainsForSdk =\n effectiveSelectedSourceChains.length > 0\n ? effectiveSelectedSourceChains\n : undefined;\n\n const sourceSelectionKey = useMemo(() => {\n if (allAvailableSourceChainIds.length === 0) return \"NONE\";\n if (!selectedSourceChains || selectedSourceChains.length === 0) {\n return \"ALL\";\n }\n return [...effectiveSelectedSourceChains].sort((a, b) => a - b).join(\"|\");\n }, [\n allAvailableSourceChainIds.length,\n effectiveSelectedSourceChains,\n selectedSourceChains,\n ]);\n const hasPendingSourceSelectionChanges =\n sourceSelectionKey !== appliedSourceSelectionKey;\n const intentSourceSpendAmount = intent.current?.intent?.sourcesTotal;\n\n const getMaxForCurrentSelection = useCallback(async () => {\n if (!nexusSDK || !inputs?.token || !inputs?.chain) return undefined;\n const maxBalAvailable = await nexusSDK.calculateMaxForBridge({\n token: inputs.token,\n toChainId: inputs.chain,\n recipient: inputs.recipient,\n sourceChains: sourceChainsForSdk,\n });\n if (!maxBalAvailable?.amount) return \"0\";\n return clampAmountToMax({\n amount: maxBalAvailable.amount,\n maxAmount: configuredMaxAmount,\n nexusSDK,\n token: inputs.token,\n chainId: inputs.chain,\n });\n }, [\n configuredMaxAmount,\n inputs?.chain,\n inputs?.recipient,\n inputs?.token,\n nexusSDK,\n sourceChainsForSdk,\n ]);\n\n const toggleSourceChain = useCallback(\n (chainId: number) => {\n setSelectedSourceChains((prev) => {\n if (allAvailableSourceChainIds.length === 0) return prev;\n const current =\n prev && prev.length > 0 ? prev : allAvailableSourceChainIds;\n const next = current.includes(chainId)\n ? current.filter((id) => id !== chainId)\n : [...current, chainId];\n if (next.length === 0) {\n return current;\n }\n const isAllSelected =\n next.length === allAvailableSourceChainIds.length &&\n allAvailableSourceChainIds.every((id) => next.includes(id));\n return isAllSelected ? null : next;\n });\n },\n [allAvailableSourceChainIds],\n );\n\n const sourceSelection = useMemo(() => {\n const amount =\n intentSourceSpendAmount?.trim() ?? inputs?.amount?.trim() ?? \"\";\n const decimals = getCoverageDecimals({\n type,\n token: inputs?.token,\n chainId: inputs?.chain,\n fallback: filteredBridgableBalance?.decimals,\n });\n const selectedChainSet = new Set(effectiveSelectedSourceChains);\n const selectedTotalRaw =\n !nexusSDK || typeof decimals !== \"number\"\n ? BigInt(0)\n : availableSources.reduce((sum, source) => {\n if (!selectedChainSet.has(source.chain.id)) return sum;\n try {\n return sum + parseUnits(source.balance ?? \"0\", decimals);\n } catch {\n return sum;\n }\n }, BigInt(0));\n const selectedTotal =\n !nexusSDK || typeof decimals !== \"number\"\n ? \"0\"\n : formatAmountForDisplay(selectedTotalRaw, decimals, nexusSDK);\n const baseSelection = {\n selectedTotal,\n requiredTotal: amount || \"0\",\n requiredSafetyTotal: amount || \"0\",\n missingToProceed: \"0\",\n missingToSafety: \"0\",\n coverageState: \"healthy\" as SourceCoverageState,\n coverageToSafetyPercent: 100,\n isBelowRequired: false,\n isBelowSafetyBuffer: false,\n };\n\n if (!nexusSDK || !inputs?.token || !inputs?.chain || !amount) {\n return baseSelection;\n }\n\n try {\n const requiredRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n inputs.token,\n inputs.chain,\n );\n if (requiredRaw <= BigInt(0)) {\n return baseSelection;\n }\n\n const missingToProceedRaw =\n selectedTotalRaw >= requiredRaw\n ? BigInt(0)\n : requiredRaw - selectedTotalRaw;\n const missingToSafetyRaw = missingToProceedRaw;\n\n const coverageState: SourceCoverageState =\n selectedTotalRaw < requiredRaw ? \"error\" : \"healthy\";\n\n const coverageBasisPoints =\n requiredRaw === BigInt(0)\n ? 10_000\n : selectedTotalRaw >= requiredRaw\n ? 10_000\n : Number((selectedTotalRaw * BigInt(10_000)) / requiredRaw);\n\n return {\n selectedTotal,\n requiredTotal: amount,\n requiredSafetyTotal: amount,\n missingToProceed: formatAmountForDisplay(\n missingToProceedRaw,\n decimals,\n nexusSDK,\n ),\n missingToSafety: formatAmountForDisplay(\n missingToSafetyRaw,\n decimals,\n nexusSDK,\n ),\n coverageState,\n coverageToSafetyPercent: coverageBasisPoints / 100,\n isBelowRequired: coverageState === \"error\",\n isBelowSafetyBuffer: coverageState === \"error\",\n };\n } catch {\n return baseSelection;\n }\n }, [\n type,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n inputs?.chain,\n inputs?.amount,\n inputs?.token,\n intentSourceSpendAmount,\n availableSources,\n effectiveSelectedSourceChains,\n ]);\n\n const stopwatch = useStopwatch({ intervalMs: 100 });\n const setStatus = useCallback(\n (status: TransactionStatus) =>\n dispatch({ type: \"setStatus\", payload: status }),\n [],\n );\n\n const resetInputs = useCallback(() => {\n dispatch({ type: \"resetInputs\" });\n }, []);\n\n const {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n } = useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n });\n\n usePolling(\n Boolean(intent.current) &&\n !isDialogOpen &&\n !isSourceMenuOpen &&\n !hasPendingSourceSelectionChanges,\n async () => {\n await refreshIntent();\n },\n 15000,\n );\n\n const debouncedRefreshMaxForSelection = useDebouncedCallback(\n async (requestId: number) => {\n try {\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (requestId !== maxAmountRequestIdRef.current) return;\n setSelectedSourcesMaxAmount(maxForCurrentSelection ?? \"0\");\n } catch (error) {\n if (requestId !== maxAmountRequestIdRef.current) return;\n console.error(\"Unable to calculate max for selected sources:\", error);\n setSelectedSourcesMaxAmount(\"0\");\n }\n },\n MAX_AMOUNT_DEBOUNCE_MS,\n );\n\n useEffect(() => {\n debouncedRefreshMaxForSelection.cancel();\n if (!nexusSDK || !inputs?.token || !inputs?.chain) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(null);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(\"0\");\n return;\n }\n const requestId = ++maxAmountRequestIdRef.current;\n debouncedRefreshMaxForSelection(requestId);\n }, [\n allAvailableSourceChainIds.length,\n configuredMaxAmount,\n debouncedRefreshMaxForSelection,\n inputs?.recipient,\n sourceSelectionKey,\n inputs?.chain,\n inputs?.token,\n nexusSDK,\n ]);\n\n useEffect(() => {\n if (type !== \"bridge\" || !connectedAddress) return;\n const previousConnectedAddress = previousConnectedAddressRef.current;\n if (!previousConnectedAddress) {\n previousConnectedAddressRef.current = connectedAddress;\n return;\n }\n if (connectedAddress === previousConnectedAddress) return;\n previousConnectedAddressRef.current = connectedAddress;\n if (prefill?.recipient) return;\n if (!inputs?.recipient || inputs.recipient === previousConnectedAddress) {\n dispatch({ type: \"setInputs\", payload: { recipient: connectedAddress } });\n }\n }, [type, connectedAddress, inputs?.recipient, prefill?.recipient]);\n\n useEffect(() => {\n invalidatePendingExecution();\n }, [inputs, invalidatePendingExecution]);\n\n useEffect(() => {\n setSelectedSourceChains(null);\n }, [inputs?.token]);\n\n useEffect(() => {\n if (isDialogOpen) return;\n stopwatch.stop();\n stopwatch.reset();\n if (state.status === \"success\" || state.status === \"error\") {\n resetSteps();\n setLastExplorerUrl(\"\");\n setStatus(\"idle\");\n }\n }, [isDialogOpen, resetSteps, setStatus, state.status, stopwatch]);\n\n useEffect(() => {\n if (txError) {\n setTxError(null);\n }\n }, [inputs, txError]);\n\n return {\n inputs,\n setInputs,\n timer: stopwatch.seconds,\n setIsDialogOpen,\n setTxError,\n loading,\n refreshing,\n isDialogOpen,\n txError,\n handleTransaction,\n reset,\n filteredBridgableBalance,\n startTransaction,\n commitAmount,\n lastExplorerUrl,\n steps,\n status: state.status,\n availableSources,\n selectedSourceChains: effectiveSelectedSourceChains,\n toggleSourceChain,\n isSourceSelectionInsufficient: sourceSelection.isBelowRequired,\n isSourceSelectionBelowSafetyBuffer: sourceSelection.isBelowSafetyBuffer,\n isSourceSelectionReadyForAccept:\n sourceSelection.coverageState === \"healthy\",\n sourceCoverageState: sourceSelection.coverageState,\n sourceCoveragePercent: sourceSelection.coverageToSafetyPercent,\n missingToProceed: sourceSelection.missingToProceed,\n missingToSafety: sourceSelection.missingToSafety,\n selectedTotal: sourceSelection.selectedTotal,\n requiredTotal: sourceSelection.requiredTotal,\n requiredSafetyTotal: sourceSelection.requiredSafetyTotal,\n maxAvailableAmount: selectedSourcesMaxAmount ?? undefined,\n isInputsValid: areInputsValid,\n };\n}\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type {\n NexusNetwork,\n OnAllowanceHookData,\n OnIntentHookData,\n TokenBalance,\n ChainBalance,\n} from \"@avail-project/nexus-sdk-v2\";\nimport { parseUnits } from \"viem\";\nimport {\n useEffect,\n useMemo,\n useCallback,\n useRef,\n useState,\n useReducer,\n type RefObject,\n} from \"react\";\nimport { type Address, isAddress } from \"viem\";\nimport { useNexusError } from \"./useNexusError\";\nimport { useTransactionExecution } from \"./useTransactionExecution\";\nimport { usePolling } from \"./usePolling\";\nimport { useStopwatch } from \"./useStopwatch\";\nimport { useDebouncedCallback } from \"./useDebouncedCallback\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport { useTransactionSteps } from \"../tx/useTransactionSteps\";\nimport {\n type SourceCoverageState,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n type TransactionFlowPrefill,\n type TransactionFlowType,\n} from \"../types/transaction-flow\";\nimport {\n MAX_AMOUNT_DEBOUNCE_MS,\n buildInitialInputs,\n clampAmountToMax,\n formatAmountForDisplay,\n getCoverageDecimals,\n normalizeMaxAmount,\n} from \"../utils/transaction-flow\";\n\ntype NexusClient = ReturnType;\n\n// v2 uses a generic step shape; minimal type to satisfy getStepKey constraint\ntype BridgePlanStep = {\n typeID?: string;\n type?: string;\n [key: string]: unknown;\n};\n\ninterface BaseTransactionFlowProps {\n type: TransactionFlowType;\n network: NexusNetwork;\n nexusSDK: NexusClient | null;\n intent: RefObject;\n allowance: RefObject;\n bridgableBalance: TokenBalance[] | null;\n prefill?: TransactionFlowPrefill;\n onComplete?: (explorerUrl?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n maxAmount?: string | number;\n isSourceMenuOpen?: boolean;\n notifyHistoryRefresh?: () => void;\n executeTransaction: TransactionFlowExecutor;\n}\n\nexport interface UseTransactionFlowProps extends BaseTransactionFlowProps {\n connectedAddress?: Address;\n}\n\ntype State = {\n inputs: TransactionFlowInputs;\n status: TransactionStatus;\n};\n\ntype Action =\n | { type: \"setInputs\"; payload: Partial }\n | { type: \"resetInputs\" }\n | { type: \"setStatus\"; payload: TransactionStatus };\n\nexport function useTransactionFlow(props: UseTransactionFlowProps) {\n const {\n type,\n network,\n nexusSDK,\n intent,\n bridgableBalance,\n prefill,\n onComplete,\n onStart,\n onError,\n fetchBalance,\n allowance,\n maxAmount,\n isSourceMenuOpen = false,\n notifyHistoryRefresh,\n executeTransaction,\n } = props;\n\n const connectedAddress = props.connectedAddress;\n const operationName = type === \"bridge\" ? \"bridge\" : \"transfer\";\n const handleNexusError = useNexusError();\n const initialState: State = {\n inputs: buildInitialInputs({ type, network, connectedAddress, prefill }),\n status: \"idle\",\n };\n\n function reducer(state: State, action: Action): State {\n switch (action.type) {\n case \"setInputs\":\n return { ...state, inputs: { ...state.inputs, ...action.payload } };\n case \"resetInputs\":\n return {\n ...state,\n inputs: buildInitialInputs({\n type,\n network,\n connectedAddress,\n prefill,\n }),\n };\n case \"setStatus\":\n return { ...state, status: action.payload };\n default:\n return state;\n }\n }\n\n const [state, dispatch] = useReducer(reducer, initialState);\n const inputs = state.inputs;\n const setInputs = (\n next: TransactionFlowInputs | Partial,\n ) => {\n dispatch({\n type: \"setInputs\",\n payload: next as Partial,\n });\n };\n\n const loading = state.status === \"executing\";\n const [refreshing, setRefreshing] = useState(false);\n const [isDialogOpen, setIsDialogOpen] = useState(false);\n const [txError, setTxError] = useState(null);\n const [lastExplorerUrl, setLastExplorerUrl] = useState(\"\");\n const previousConnectedAddressRef = useRef
(\n connectedAddress,\n );\n const maxAmountRequestIdRef = useRef(0);\n const [selectedSourceChains, setSelectedSourceChains] = useState<\n number[] | null\n >(null);\n const [selectedSourcesMaxAmount, setSelectedSourcesMaxAmount] = useState<\n string | null\n >(null);\n const [appliedSourceSelectionKey, setAppliedSourceSelectionKey] =\n useState(\"ALL\");\n const {\n steps,\n onStepsList,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n const configuredMaxAmount = useMemo(\n () => normalizeMaxAmount(maxAmount),\n [maxAmount],\n );\n\n const areInputsValid = useMemo(() => {\n const hasToken = inputs?.token !== undefined && inputs?.token !== null;\n const hasChain = inputs?.chain !== undefined && inputs?.chain !== null;\n const hasAmount = Boolean(inputs?.amount) && Number(inputs?.amount) > 0;\n const hasValidRecipient =\n Boolean(inputs?.recipient) && isAddress(inputs.recipient as string);\n return hasToken && hasChain && hasAmount && hasValidRecipient;\n }, [inputs]);\n\n const filteredBridgableBalance = useMemo(() => {\n return bridgableBalance?.find((bal) =>\n inputs?.token === \"USDM\"\n ? bal?.symbol === \"USDC\"\n : bal?.symbol === inputs?.token,\n );\n }, [bridgableBalance, inputs?.token]);\n\n const availableSources = useMemo(() => {\n // v2: chainBalances replaces breakdown\n const chainBalances = filteredBridgableBalance?.chainBalances ?? [];\n const destinationChainId = inputs?.chain;\n const nonZero = chainBalances.filter((source: ChainBalance) => {\n if (Number.parseFloat(source.balance ?? \"0\") <= 0) return false;\n if (typeof destinationChainId === \"number\") {\n return source.chain.id !== destinationChainId;\n }\n return true;\n });\n const decimals = filteredBridgableBalance?.decimals;\n if (!nexusSDK || typeof decimals !== \"number\") {\n return nonZero.sort(\n (a, b) => Number.parseFloat(b.balance) - Number.parseFloat(a.balance),\n );\n }\n return nonZero.sort((a: ChainBalance, b: ChainBalance) => {\n try {\n const aRaw = parseUnits(a.balance ?? \"0\", decimals);\n const bRaw = parseUnits(b.balance ?? \"0\", decimals);\n if (aRaw === bRaw) return 0;\n return aRaw > bRaw ? -1 : 1;\n } catch {\n return Number.parseFloat(b.balance) - Number.parseFloat(a.balance);\n }\n });\n }, [\n inputs?.chain,\n filteredBridgableBalance?.chainBalances,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n ]);\n\n const allAvailableSourceChainIds = useMemo(\n () => availableSources.map((source: ChainBalance) => source.chain.id),\n [availableSources],\n );\n\n const effectiveSelectedSourceChains = useMemo(() => {\n if (selectedSourceChains && selectedSourceChains.length > 0) {\n const availableSet = new Set(allAvailableSourceChainIds);\n const filteredSelection = selectedSourceChains.filter((id: number) =>\n availableSet.has(id),\n );\n if (filteredSelection.length > 0) {\n return filteredSelection;\n }\n }\n return allAvailableSourceChainIds;\n }, [selectedSourceChains, allAvailableSourceChainIds]);\n\n const sourceChainsForSdk =\n effectiveSelectedSourceChains.length > 0\n ? effectiveSelectedSourceChains\n : undefined;\n\n const sourceSelectionKey = useMemo(() => {\n if (allAvailableSourceChainIds.length === 0) return \"NONE\";\n if (!selectedSourceChains || selectedSourceChains.length === 0) {\n return \"ALL\";\n }\n return [...effectiveSelectedSourceChains].sort((a: number, b: number) => a - b).join(\"|\");\n }, [\n allAvailableSourceChainIds.length,\n effectiveSelectedSourceChains,\n selectedSourceChains,\n ]);\n const hasPendingSourceSelectionChanges =\n sourceSelectionKey !== appliedSourceSelectionKey;\n const intentSourceSpendAmount = intent.current?.intent?.sourcesTotal;\n\n /**\n * v2: calculateMaxForBridge is removed. Use simulateBridge to get the max amount,\n * or fall back to summing available source balances directly.\n */\n const getMaxForCurrentSelection = useCallback(async () => {\n if (!nexusSDK || !inputs?.token || !inputs?.chain) return undefined;\n\n // Sum balances from selected sources as a direct proxy for max\n const decimals = filteredBridgableBalance?.decimals;\n if (typeof decimals !== \"number\") return \"0\";\n\n const selectedSet = new Set(\n sourceChainsForSdk ?? allAvailableSourceChainIds,\n );\n const totalRaw = availableSources.reduce((sum: bigint, source: ChainBalance) => {\n if (!selectedSet.has(source.chain.id)) return sum;\n try {\n return sum + parseUnits(source.balance ?? \"0\", decimals);\n } catch {\n return sum;\n }\n }, BigInt(0));\n\n const totalReadable = formatToBigIntReadable(totalRaw, decimals);\n if (!totalReadable) return \"0\";\n\n return clampAmountToMax({\n amount: totalReadable,\n maxAmount: configuredMaxAmount,\n nexusSDK,\n token: inputs.token,\n chainId: inputs.chain,\n });\n }, [\n configuredMaxAmount,\n inputs?.chain,\n inputs?.token,\n nexusSDK,\n sourceChainsForSdk,\n allAvailableSourceChainIds,\n availableSources,\n filteredBridgableBalance?.decimals,\n ]);\n\n const toggleSourceChain = useCallback(\n (chainId: number) => {\n setSelectedSourceChains((prev) => {\n if (allAvailableSourceChainIds.length === 0) return prev;\n const current =\n prev && prev.length > 0 ? prev : allAvailableSourceChainIds;\n const next = current.includes(chainId)\n ? current.filter((id: number) => id !== chainId)\n : [...current, chainId];\n if (next.length === 0) {\n return current;\n }\n const isAllSelected =\n next.length === allAvailableSourceChainIds.length &&\n allAvailableSourceChainIds.every((id) => next.includes(id));\n return isAllSelected ? null : next;\n });\n },\n [allAvailableSourceChainIds],\n );\n\n const sourceSelection = useMemo(() => {\n const amount =\n intentSourceSpendAmount?.trim() ?? inputs?.amount?.trim() ?? \"\";\n const decimals = getCoverageDecimals({\n type,\n token: inputs?.token,\n chainId: inputs?.chain,\n fallback: filteredBridgableBalance?.decimals,\n });\n const selectedChainSet = new Set(effectiveSelectedSourceChains);\n const selectedTotalRaw =\n !nexusSDK || typeof decimals !== \"number\"\n ? BigInt(0)\n : availableSources.reduce((sum: bigint, source: ChainBalance) => {\n if (!selectedChainSet.has(source.chain.id)) return sum;\n try {\n return sum + parseUnits(source.balance ?? \"0\", decimals);\n } catch {\n return sum;\n }\n }, BigInt(0));\n const selectedTotal =\n !nexusSDK || typeof decimals !== \"number\"\n ? \"0\"\n : formatAmountForDisplay(selectedTotalRaw, decimals, nexusSDK);\n const baseSelection = {\n selectedTotal,\n requiredTotal: amount || \"0\",\n requiredSafetyTotal: amount || \"0\",\n missingToProceed: \"0\",\n missingToSafety: \"0\",\n coverageState: \"healthy\" as SourceCoverageState,\n coverageToSafetyPercent: 100,\n isBelowRequired: false,\n isBelowSafetyBuffer: false,\n };\n\n if (!nexusSDK || !inputs?.token || !inputs?.chain || !amount) {\n return baseSelection;\n }\n\n try {\n // v2: convertTokenReadableAmountToBigInt(amount, tokenSymbol, chainId)\n const requiredRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n inputs.token,\n inputs.chain,\n );\n if (requiredRaw <= BigInt(0)) {\n return baseSelection;\n }\n\n const missingToProceedRaw =\n selectedTotalRaw >= requiredRaw\n ? BigInt(0)\n : requiredRaw - selectedTotalRaw;\n const missingToSafetyRaw = missingToProceedRaw;\n\n const coverageState: SourceCoverageState =\n selectedTotalRaw < requiredRaw ? \"error\" : \"healthy\";\n\n const coverageBasisPoints =\n requiredRaw === BigInt(0)\n ? 10_000\n : selectedTotalRaw >= requiredRaw\n ? 10_000\n : Number((selectedTotalRaw * BigInt(10_000)) / requiredRaw);\n\n return {\n selectedTotal,\n requiredTotal: amount,\n requiredSafetyTotal: amount,\n missingToProceed: formatAmountForDisplay(\n missingToProceedRaw,\n decimals,\n nexusSDK,\n ),\n missingToSafety: formatAmountForDisplay(\n missingToSafetyRaw,\n decimals,\n nexusSDK,\n ),\n coverageState,\n coverageToSafetyPercent: coverageBasisPoints / 100,\n isBelowRequired: coverageState === \"error\",\n isBelowSafetyBuffer: coverageState === \"error\",\n };\n } catch {\n return baseSelection;\n }\n }, [\n type,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n inputs?.chain,\n inputs?.amount,\n inputs?.token,\n intentSourceSpendAmount,\n availableSources,\n effectiveSelectedSourceChains,\n ]);\n\n const stopwatch = useStopwatch({ intervalMs: 100 });\n const setStatus = useCallback(\n (status: TransactionStatus) =>\n dispatch({ type: \"setStatus\", payload: status }),\n [],\n );\n\n const resetInputs = useCallback(() => {\n dispatch({ type: \"resetInputs\" });\n }, []);\n\n const {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n } = useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n });\n\n usePolling(\n Boolean(intent.current) &&\n !isDialogOpen &&\n !isSourceMenuOpen &&\n !hasPendingSourceSelectionChanges,\n async () => {\n await refreshIntent();\n },\n 15000,\n );\n\n const debouncedRefreshMaxForSelection = useDebouncedCallback(\n async (requestId: number) => {\n try {\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (requestId !== maxAmountRequestIdRef.current) return;\n setSelectedSourcesMaxAmount(maxForCurrentSelection ?? \"0\");\n } catch (error) {\n if (requestId !== maxAmountRequestIdRef.current) return;\n console.error(\"Unable to calculate max for selected sources:\", error);\n setSelectedSourcesMaxAmount(\"0\");\n }\n },\n MAX_AMOUNT_DEBOUNCE_MS,\n );\n\n useEffect(() => {\n debouncedRefreshMaxForSelection.cancel();\n if (!nexusSDK || !inputs?.token || !inputs?.chain) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(null);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(\"0\");\n return;\n }\n const requestId = ++maxAmountRequestIdRef.current;\n debouncedRefreshMaxForSelection(requestId);\n }, [\n allAvailableSourceChainIds.length,\n configuredMaxAmount,\n debouncedRefreshMaxForSelection,\n inputs?.recipient,\n sourceSelectionKey,\n inputs?.chain,\n inputs?.token,\n nexusSDK,\n ]);\n\n useEffect(() => {\n if (type !== \"bridge\" || !connectedAddress) return;\n const previousConnectedAddress = previousConnectedAddressRef.current;\n if (!previousConnectedAddress) {\n previousConnectedAddressRef.current = connectedAddress;\n return;\n }\n if (connectedAddress === previousConnectedAddress) return;\n previousConnectedAddressRef.current = connectedAddress;\n if (prefill?.recipient) return;\n if (!inputs?.recipient || inputs.recipient === previousConnectedAddress) {\n dispatch({ type: \"setInputs\", payload: { recipient: connectedAddress } });\n }\n }, [type, connectedAddress, inputs?.recipient, prefill?.recipient]);\n\n useEffect(() => {\n invalidatePendingExecution();\n }, [inputs, invalidatePendingExecution]);\n\n useEffect(() => {\n setSelectedSourceChains(null);\n }, [inputs?.token]);\n\n // Safety-net: stop the stopwatch as soon as status reaches a terminal state.\n // This ensures the timer freezes even if the onEvent closure's stopwatch.stop()\n // didn't fire (e.g. stale closure reference or SDK promise resolved oddly).\n useEffect(() => {\n if (state.status === \"success\" || state.status === \"error\") {\n stopwatch.stop();\n }\n }, [state.status, stopwatch]);\n\n useEffect(() => {\n if (isDialogOpen) return;\n stopwatch.stop();\n stopwatch.reset();\n if (state.status === \"success\" || state.status === \"error\") {\n resetSteps();\n setLastExplorerUrl(\"\");\n setStatus(\"idle\");\n }\n }, [isDialogOpen, resetSteps, setStatus, state.status, stopwatch]);\n\n useEffect(() => {\n if (txError) {\n setTxError(null);\n }\n }, [inputs, txError]);\n\n\n return {\n inputs,\n setInputs,\n timer: stopwatch.seconds,\n setIsDialogOpen,\n setTxError,\n loading,\n refreshing,\n isDialogOpen,\n txError,\n handleTransaction,\n reset,\n filteredBridgableBalance,\n startTransaction,\n commitAmount,\n lastExplorerUrl,\n steps,\n status: state.status,\n availableSources,\n selectedSourceChains: effectiveSelectedSourceChains,\n toggleSourceChain,\n isSourceSelectionInsufficient: sourceSelection.isBelowRequired,\n isSourceSelectionBelowSafetyBuffer: sourceSelection.isBelowSafetyBuffer,\n isSourceSelectionReadyForAccept:\n sourceSelection.coverageState === \"healthy\",\n sourceCoverageState: sourceSelection.coverageState,\n sourceCoveragePercent: sourceSelection.coverageToSafetyPercent,\n missingToProceed: sourceSelection.missingToProceed,\n missingToSafety: sourceSelection.missingToSafety,\n selectedTotal: sourceSelection.selectedTotal,\n requiredTotal: sourceSelection.requiredTotal,\n requiredSafetyTotal: sourceSelection.requiredSafetyTotal,\n maxAvailableAmount: selectedSourcesMaxAmount ?? undefined,\n isInputsValid: areInputsValid,\n };\n}\n\n/** Helper: format a bigint rawAmount with decimals into a readable decimal string. */\nfunction formatToBigIntReadable(raw: bigint, decimals: number): string {\n if (raw <= BigInt(0)) return \"0\";\n const divisor = BigInt(10 ** decimals);\n const whole = raw / divisor;\n const fraction = raw % divisor;\n if (fraction === BigInt(0)) return whole.toString();\n const fractionStr = fraction.toString().padStart(decimals, \"0\").replace(/0+$/, \"\");\n return `${whole}.${fractionStr}`;\n}\n", "type": "registry:component", "target": "components/common/hooks/useTransactionFlow.ts" }, @@ -186,7 +186,7 @@ }, { "path": "registry/nexus-elements/common/tx/steps.ts", - "content": "import type { SwapStepType } from \"@avail-project/nexus-core\";\nimport type { GenericStep } from \"./types\";\nimport { getStepKey } from \"./types\";\n\n/**\n * Predefined expected steps for swaps to seed UI before events arrive.\n * Kept here to avoid duplication across exact-in and exact-out hooks.\n */\nexport const SWAP_EXPECTED_STEPS: SwapStepType[] = [\n { type: \"SWAP_START\", typeID: \"SWAP_START\" } as SwapStepType,\n { type: \"DETERMINING_SWAP\", typeID: \"DETERMINING_SWAP\" } as SwapStepType,\n {\n type: \"CREATE_PERMIT_FOR_SOURCE_SWAP\",\n typeID:\n \"CREATE_PERMIT_FOR_SOURCE_SWAP\" as unknown as SwapStepType[\"typeID\"],\n } as SwapStepType,\n {\n type: \"SOURCE_SWAP_BATCH_TX\",\n typeID: \"SOURCE_SWAP_BATCH_TX\",\n } as SwapStepType,\n {\n type: \"SOURCE_SWAP_HASH\",\n typeID: \"SOURCE_SWAP_HASH\" as unknown as SwapStepType[\"typeID\"],\n } as SwapStepType,\n { type: \"RFF_ID\", typeID: \"RFF_ID\" } as SwapStepType,\n {\n type: \"DESTINATION_SWAP_BATCH_TX\",\n typeID: \"DESTINATION_SWAP_BATCH_TX\",\n } as SwapStepType,\n {\n type: \"DESTINATION_SWAP_HASH\",\n typeID: \"DESTINATION_SWAP_HASH\" as unknown as SwapStepType[\"typeID\"],\n } as SwapStepType,\n { type: \"SWAP_COMPLETE\", typeID: \"SWAP_COMPLETE\" } as SwapStepType,\n];\n\nexport function seedSteps(expected: T[]): Array> {\n return expected.map((st, index) => ({\n id: index,\n completed: false,\n step: st,\n }));\n}\n\nexport function computeAllCompleted(steps: Array>): boolean {\n return steps.length > 0 && steps.every((s) => s.completed);\n}\n\n/**\n * Replace the current list of steps with a new list, preserving completion\n * for any steps that were already marked completed (matched by key).\n */\nexport function mergeStepsList(\n prev: Array>,\n list: T[]\n): Array> {\n const completedKeys = new Set();\n for (const prevStep of prev) {\n if (prevStep.completed) {\n completedKeys.add(getStepKey(prevStep.step));\n }\n }\n const next: Array> = [];\n for (let index = 0; index < list.length; index++) {\n const step = list[index];\n const key = getStepKey(step);\n next.push({\n id: index,\n completed: completedKeys.has(key),\n step,\n });\n }\n return next;\n}\n\n/**\n * Mark a step complete in-place; if the step doesn't yet exist, append it.\n */\nexport function mergeStepComplete(\n prev: Array>,\n step: T\n): Array> {\n const key = getStepKey(step);\n const updated: Array> = [];\n let found = false;\n for (const s of prev) {\n if (getStepKey(s.step) === key) {\n updated.push({ ...s, completed: true, step: { ...s.step, ...step } });\n found = true;\n } else {\n updated.push(s);\n }\n }\n if (!found) {\n updated.push({\n id: updated.length,\n completed: true,\n step,\n });\n }\n return updated;\n}\n", + "content": "// v2: SwapStepType is no longer exported from the SDK — use a local step shape\n// that matches v2 SwapPlanStep discriminator pattern\nexport type SwapStepType = {\n typeID?: string;\n type?: string;\n [key: string]: unknown;\n};\n\nimport type { GenericStep } from \"./types\";\nimport { getStepKey } from \"./types\";\n\n/**\n * Predefined expected steps for swaps to seed UI before events arrive.\n * Uses v2 stepType names that match SwapPlanProgressEvent.stepType discriminators.\n */\nexport const SWAP_EXPECTED_STEPS: SwapStepType[] = [\n { type: \"source_swap\", typeID: \"source_swap\" },\n { type: \"eoa_to_ephemeral_transfer\", typeID: \"eoa_to_ephemeral_transfer\" },\n { type: \"bridge_deposit\", typeID: \"bridge_deposit\" },\n { type: \"bridge_intent_submission\", typeID: \"bridge_intent_submission\" },\n { type: \"bridge_fill\", typeID: \"bridge_fill\" },\n { type: \"destination_swap\", typeID: \"destination_swap\" },\n];\n\nexport function seedSteps(expected: T[]): Array> {\n return expected.map((st, index) => ({\n id: index,\n completed: false,\n step: st,\n }));\n}\n\nexport function computeAllCompleted(steps: Array>): boolean {\n return steps.length > 0 && steps.every((s) => s.completed);\n}\n\n/**\n * Replace the current list of steps with a new list, preserving completion\n * for any steps that were already marked completed (matched by key).\n */\nexport function mergeStepsList(\n prev: Array>,\n list: T[]\n): Array> {\n const completedKeys = new Set();\n for (const prevStep of prev) {\n if (prevStep.completed) {\n completedKeys.add(getStepKey(prevStep.step));\n }\n }\n const next: Array> = [];\n for (let index = 0; index < list.length; index++) {\n const step = list[index];\n const key = getStepKey(step);\n next.push({\n id: index,\n completed: completedKeys.has(key),\n step,\n });\n }\n return next;\n}\n\n/**\n * Mark a step complete in-place; if the step doesn't yet exist, append it.\n */\nexport function mergeStepComplete(\n prev: Array>,\n step: T\n): Array> {\n const key = getStepKey(step);\n const updated: Array> = [];\n let found = false;\n for (const s of prev) {\n if (getStepKey(s.step) === key) {\n updated.push({ ...s, completed: true, step: { ...s.step, ...step } });\n found = true;\n } else {\n updated.push(s);\n }\n }\n if (!found) {\n updated.push({\n id: updated.length,\n completed: true,\n step,\n });\n }\n return updated;\n}\n", "type": "registry:component", "target": "components/common/tx/steps.ts" }, @@ -204,25 +204,25 @@ }, { "path": "registry/nexus-elements/common/types/transaction-flow.ts", - "content": "import {\n type NexusSDK,\n type SUPPORTED_CHAINS_IDS,\n type SUPPORTED_TOKENS,\n} from \"@avail-project/nexus-core\";\nimport { type Address } from \"viem\";\n\nexport type TransactionFlowType = \"bridge\" | \"transfer\";\n\nexport interface TransactionFlowInputs {\n chain: SUPPORTED_CHAINS_IDS;\n token: SUPPORTED_TOKENS;\n amount?: string;\n recipient?: `0x${string}`;\n}\n\nexport interface TransactionFlowPrefill {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n}\n\ntype BridgeOptions = NonNullable[1]>;\n\nexport type TransactionFlowEvent =\n NonNullable extends (event: infer E) => void\n ? E\n : never;\n\nexport type TransactionFlowOnEvent = NonNullable;\n\nexport interface TransactionFlowExecuteParams {\n token: SUPPORTED_TOKENS;\n amount: bigint;\n toChainId: SUPPORTED_CHAINS_IDS;\n recipient: `0x${string}`;\n sourceChains?: number[];\n onEvent: TransactionFlowOnEvent;\n}\n\nexport type TransactionFlowExecutor = (\n params: TransactionFlowExecuteParams,\n) => Promise<{ explorerUrl: string } | null>;\n\nexport type SourceCoverageState = \"healthy\" | \"warning\" | \"error\";\n\nexport interface SourceSelectionValidation {\n coverageState: SourceCoverageState;\n isBelowRequired: boolean;\n missingToProceed: string;\n missingToSafety: string;\n}\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport { type Address } from \"viem\";\n\ntype NexusClient = ReturnType;\n\n// v2 uses string token symbols (toTokenSymbol) with number chain IDs\nexport type TransactionFlowType = \"bridge\" | \"transfer\";\n\nexport interface TransactionFlowInputs {\n chain: number;\n token: string;\n amount?: string;\n recipient?: `0x${string}`;\n}\n\nexport interface TransactionFlowPrefill {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n}\n\n// v2 bridge onEvent uses typed discriminated union, not NEXUS_EVENTS\nexport type TransactionFlowEvent =\n | { type: \"status\"; status: string }\n | { type: \"plan_preview\"; plan: { steps: unknown[] } }\n | { type: \"plan_confirmed\"; plan: { steps: unknown[] } }\n | { type: \"plan_progress\"; stepType: string; state: string; step: unknown };\n\nexport type TransactionFlowOnEvent = (event: TransactionFlowEvent) => void;\n\nexport interface TransactionFlowExecuteParams {\n token: string;\n amount: bigint;\n toChainId: number;\n recipient: `0x${string}`;\n sources?: number[];\n onEvent: TransactionFlowOnEvent;\n}\n\nexport type TransactionFlowExecutor = (\n params: TransactionFlowExecuteParams,\n) => Promise<{ explorerUrl: string } | null>;\n\nexport type SourceCoverageState = \"healthy\" | \"warning\" | \"error\";\n\nexport interface SourceSelectionValidation {\n coverageState: SourceCoverageState;\n isBelowRequired: boolean;\n missingToProceed: string;\n missingToSafety: string;\n}\n", "type": "registry:component", "target": "components/common/types/transaction-flow.ts" }, { "path": "registry/nexus-elements/common/utils/constant.ts", - "content": "import { SUPPORTED_CHAINS } from \"@avail-project/nexus-core\";\nimport { formatUnits, parseUnits } from \"viem\";\n\nexport const SHORT_CHAIN_NAME: Record = {\n [SUPPORTED_CHAINS.ETHEREUM]: \"Ethereum\",\n [SUPPORTED_CHAINS.BASE]: \"Base\",\n [SUPPORTED_CHAINS.ARBITRUM]: \"Arbitrum\",\n [SUPPORTED_CHAINS.OPTIMISM]: \"Optimism\",\n [SUPPORTED_CHAINS.POLYGON]: \"Polygon\",\n [SUPPORTED_CHAINS.AVALANCHE]: \"Avalanche\",\n [SUPPORTED_CHAINS.SCROLL]: \"Scroll\",\n [SUPPORTED_CHAINS.MEGAETH]: \"MegaETH\",\n [SUPPORTED_CHAINS.KAIA]: \"Kaia\",\n [SUPPORTED_CHAINS.BNB]: \"BNB\",\n [SUPPORTED_CHAINS.MONAD]: \"Monad\",\n [SUPPORTED_CHAINS.HYPEREVM]: \"HyperEVM\",\n [SUPPORTED_CHAINS.CITREA]: \"Citrea\",\n // [SUPPORTED_CHAINS.TRON]: \"Tron\",\n [SUPPORTED_CHAINS.SEPOLIA]: \"Sepolia\",\n [SUPPORTED_CHAINS.BASE_SEPOLIA]: \"Base Sepolia\",\n [SUPPORTED_CHAINS.ARBITRUM_SEPOLIA]: \"Arbitrum Sepolia\",\n [SUPPORTED_CHAINS.OPTIMISM_SEPOLIA]: \"Optimism Sepolia\",\n [SUPPORTED_CHAINS.POLYGON_AMOY]: \"Polygon Amoy\",\n [SUPPORTED_CHAINS.MONAD_TESTNET]: \"Monad Testnet\",\n // [SUPPORTED_CHAINS.TRON_SHASTA]: \"Tron Shasta\",\n} as const;\n\nconst DEFAULT_SAFETY_MARGIN = 0.01; // 1%\n\n/**\n * Compute an amount string for fraction buttons (25%, 50%, 75%, 100%).\n *\n * @param balanceStr - user's balance as a human decimal string (e.g. \"12.345\") OR as base-unit integer string if `balanceIsBaseUnits` true\n * @param fraction - fraction e.g. 0.25, 0.5, 0.75, 1\n * @param decimals - token decimals (6 for USDC/USDT, 18 for ETH)\n * @param safetyMargin - 0.01 for 1% default\n * @param balanceIsBaseUnits - if true, balanceStr is already base units integer string (wei / smallest unit)\n * @returns decimal string clipped to token decimals (rounded down)\n */\nexport function computeAmountFromFraction(\n balanceStr: string,\n fraction: number,\n decimals: number,\n safetyMargin = DEFAULT_SAFETY_MARGIN,\n balanceIsBaseUnits = false,\n): string {\n if (!balanceStr) return \"0\";\n\n // parse balance into base units (BigInt)\n const balanceUnits: bigint = balanceIsBaseUnits\n ? BigInt(balanceStr)\n : parseUnits(balanceStr, decimals);\n\n if (balanceUnits === BigInt(0)) return \"0\";\n\n // Use an integer precision multiplier to avoid FP issues\n const PREC = 1_000_000; // 1e6 precision for fraction & safety margin\n const safetyMul = BigInt(Math.max(0, Math.floor((1 - safetyMargin) * PREC))); // (1 - safetyMargin) * PREC\n const fractionMul = BigInt(Math.max(0, Math.floor(fraction * PREC))); // fraction * PREC\n\n // Apply safety margin: floor(balance * (1 - safetyMargin))\n const maxAfterSafety = (balanceUnits * safetyMul) / BigInt(PREC);\n\n // Apply fraction and floor: floor(maxAfterSafety * fraction)\n let desiredUnits = (maxAfterSafety * fractionMul) / BigInt(PREC);\n\n // Extra clamp just in case\n if (desiredUnits > balanceUnits) desiredUnits = balanceUnits;\n if (desiredUnits < BigInt(0)) desiredUnits = BigInt(0);\n\n // format back to human readable decimal string with token decimals (formatUnits truncates/keeps decimals)\n // formatUnits will produce exactly decimals digits if fractional part exists; we'll strip trailing zeros.\n const raw = formatUnits(desiredUnits, decimals);\n // strip trailing zeros and possible trailing dot\n return raw\n .replace(/(\\.\\d*?[1-9])0+$/u, \"$1\")\n .replace(/\\.0+$/u, \"\")\n .replace(/^\\.$/u, \"0\");\n}\n\nexport const usdFormatter = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n});\n\nconst usdFormatterPrecise = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 3,\n maximumFractionDigits: 3,\n});\n\n/**\n * Formats USD values for UI.\n * Values between 0 and 0.001 are shown as \"< $0.001\".\n * Values between 0.001 and 0.01 are shown with 3 decimals.\n */\nexport function formatUsdForDisplay(value: number): string {\n if (!Number.isFinite(value)) return usdFormatter.format(0);\n const absValue = Math.abs(value);\n\n if (absValue === 0) return usdFormatter.format(0);\n if (absValue < 0.001) return \"< $0.001\";\n if (absValue < 0.01) {\n return usdFormatterPrecise.format(value);\n }\n\n return usdFormatter.format(value);\n}\n", + "content": "import { formatUnits, parseUnits } from \"viem\";\n\n// v2: SUPPORTED_CHAINS removed — using literal EVM chain IDs\nexport const SHORT_CHAIN_NAME: Record = {\n 1: \"Ethereum\",\n 8453: \"Base\",\n 42161: \"Arbitrum\",\n 10: \"Optimism\",\n 137: \"Polygon\",\n 43114: \"Avalanche\",\n 534352: \"Scroll\",\n 6342: \"MegaETH\",\n 8217: \"Kaia\",\n 56: \"BNB\",\n 10143: \"Monad\",\n 999: \"HyperEVM\",\n 5115: \"Citrea\",\n 11155111: \"Sepolia\",\n 84532: \"Base Sepolia\",\n 421614: \"Arbitrum Sepolia\",\n 11155420: \"Optimism Sepolia\",\n 80002: \"Polygon Amoy\",\n} as const;\n\nconst DEFAULT_SAFETY_MARGIN = 0.01; // 1%\n\n/**\n * Compute an amount string for fraction buttons (25%, 50%, 75%, 100%).\n *\n * @param balanceStr - user's balance as a human decimal string (e.g. \"12.345\") OR as base-unit integer string if `balanceIsBaseUnits` true\n * @param fraction - fraction e.g. 0.25, 0.5, 0.75, 1\n * @param decimals - token decimals (6 for USDC/USDT, 18 for ETH)\n * @param safetyMargin - 0.01 for 1% default\n * @param balanceIsBaseUnits - if true, balanceStr is already base units integer string (wei / smallest unit)\n * @returns decimal string clipped to token decimals (rounded down)\n */\nexport function computeAmountFromFraction(\n balanceStr: string,\n fraction: number,\n decimals: number,\n safetyMargin = DEFAULT_SAFETY_MARGIN,\n balanceIsBaseUnits = false,\n): string {\n if (!balanceStr) return \"0\";\n\n // parse balance into base units (BigInt)\n const balanceUnits: bigint = balanceIsBaseUnits\n ? BigInt(balanceStr)\n : parseUnits(balanceStr, decimals);\n\n if (balanceUnits === BigInt(0)) return \"0\";\n\n // Use an integer precision multiplier to avoid FP issues\n const PREC = 1_000_000; // 1e6 precision for fraction & safety margin\n const safetyMul = BigInt(Math.max(0, Math.floor((1 - safetyMargin) * PREC))); // (1 - safetyMargin) * PREC\n const fractionMul = BigInt(Math.max(0, Math.floor(fraction * PREC))); // fraction * PREC\n\n // Apply safety margin: floor(balance * (1 - safetyMargin))\n const maxAfterSafety = (balanceUnits * safetyMul) / BigInt(PREC);\n\n // Apply fraction and floor: floor(maxAfterSafety * fraction)\n let desiredUnits = (maxAfterSafety * fractionMul) / BigInt(PREC);\n\n // Extra clamp just in case\n if (desiredUnits > balanceUnits) desiredUnits = balanceUnits;\n if (desiredUnits < BigInt(0)) desiredUnits = BigInt(0);\n\n // format back to human readable decimal string with token decimals (formatUnits truncates/keeps decimals)\n // formatUnits will produce exactly decimals digits if fractional part exists; we'll strip trailing zeros.\n const raw = formatUnits(desiredUnits, decimals);\n // strip trailing zeros and possible trailing dot\n return raw\n .replace(/(\\.\\d*?[1-9])0+$/u, \"$1\")\n .replace(/\\.0+$/u, \"\")\n .replace(/^\\.$/u, \"0\");\n}\n\nexport const usdFormatter = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n});\n\nconst usdFormatterPrecise = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 3,\n maximumFractionDigits: 3,\n});\n\n/**\n * Formats USD values for UI.\n * Values between 0 and 0.001 are shown as \"< $0.001\".\n * Values between 0.001 and 0.01 are shown with 3 decimals.\n */\nexport function formatUsdForDisplay(value: number): string {\n if (!Number.isFinite(value)) return usdFormatter.format(0);\n const absValue = Math.abs(value);\n\n if (absValue === 0) return usdFormatter.format(0);\n if (absValue < 0.001) return \"< $0.001\";\n if (absValue < 0.01) {\n return usdFormatterPrecise.format(value);\n }\n\n return usdFormatter.format(value);\n}\n", "type": "registry:component", "target": "components/common/utils/constant.ts" }, { "path": "registry/nexus-elements/common/utils/token-pricing.ts", - "content": "import type { SupportedChainsAndTokensResult } from \"@avail-project/nexus-core\";\n\nconst COINBASE_SPOT_API_BASE = \"https://api.coinbase.com/v2/prices\";\nconst COINBASE_EXCHANGE_RATES_API_BASE =\n \"https://api.coinbase.com/v2/exchange-rates\";\n\nexport const DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS = 4_000;\nexport const USD_PEGGED_FALLBACK_RATE = 1;\nexport const DEFAULT_USD_PEGGED_TOKEN_SYMBOLS = [\n \"USDT\",\n \"USDC\",\n \"USDS\",\n \"DAI\",\n \"USDM\",\n \"FDUSD\",\n \"BUSD\",\n \"TUSD\",\n \"PYUSD\",\n \"GUSD\",\n \"LUSD\",\n \"USDE\",\n \"USDP\",\n] as const;\n\ntype CoinbaseSpotPriceResponse = {\n data?: {\n amount?: string | number;\n };\n};\n\ntype CoinbaseExchangeRatesResponse = {\n data?: {\n rates?: Record;\n };\n};\n\ntype SupportedTokenMetadata = {\n symbol?: string;\n equivalentCurrency?: string;\n};\n\ntype SupportedChainMetadata = {\n tokens?: SupportedTokenMetadata[];\n};\n\nexport function normalizeTokenSymbol(tokenSymbol: string): string {\n return tokenSymbol.trim().toUpperCase();\n}\n\nexport function toFinitePositiveNumber(value: unknown): number | null {\n const parsed = Number.parseFloat(String(value));\n if (!Number.isFinite(parsed) || parsed <= 0) {\n return null;\n }\n return parsed;\n}\n\nexport function getCoinbaseSymbolCandidates(tokenSymbol: string): string[] {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return [];\n\n const baseSymbol = normalized.split(/[._-]/)[0] ?? normalized;\n const wrappedBase =\n baseSymbol.startsWith(\"W\") && baseSymbol.length > 3\n ? baseSymbol.slice(1)\n : null;\n\n return Array.from(\n new Set(\n [normalized, baseSymbol, wrappedBase].filter(\n (symbol): symbol is string => Boolean(symbol),\n ),\n ),\n );\n}\n\nexport function buildUsdPeggedSymbolSet(\n supportedChains: SupportedChainsAndTokensResult | null,\n baseSymbols: Iterable = DEFAULT_USD_PEGGED_TOKEN_SYMBOLS,\n): Set {\n const symbolSet = new Set(baseSymbols);\n\n for (const chain of (supportedChains ?? []) as SupportedChainMetadata[]) {\n for (const token of chain.tokens ?? []) {\n const symbol = normalizeTokenSymbol(token.symbol ?? \"\");\n const equivalent = normalizeTokenSymbol(token.equivalentCurrency ?? \"\");\n if (!symbol) continue;\n\n if (equivalent && symbolSet.has(equivalent)) {\n symbolSet.add(symbol);\n }\n }\n }\n\n return symbolSet;\n}\n\nasync function fetchJsonWithTimeout(\n url: string,\n requestTimeoutMs: number,\n): Promise {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);\n try {\n const response = await fetch(url, {\n signal: controller.signal,\n });\n if (!response.ok) return null;\n return (await response.json()) as T;\n } catch {\n return null;\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nexport async function fetchCoinbaseUsdRate(\n tokenSymbol: string,\n requestTimeoutMs = DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS,\n): Promise {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return null;\n\n for (const candidate of getCoinbaseSymbolCandidates(normalized)) {\n const spotBody = await fetchJsonWithTimeout(\n `${COINBASE_SPOT_API_BASE}/${encodeURIComponent(candidate)}-USD/spot`,\n requestTimeoutMs,\n );\n const spotAmount = toFinitePositiveNumber(spotBody?.data?.amount);\n if (spotAmount) return spotAmount;\n\n const exchangeRatesBody =\n await fetchJsonWithTimeout(\n `${COINBASE_EXCHANGE_RATES_API_BASE}?currency=${encodeURIComponent(candidate)}`,\n requestTimeoutMs,\n );\n const exchangeRatesAmount = toFinitePositiveNumber(\n exchangeRatesBody?.data?.rates?.USD,\n );\n if (exchangeRatesAmount) return exchangeRatesAmount;\n }\n\n return null;\n}\n", + "content": "// v2: getSupportedChains() return type is inferred directly; define a structural type\ntype SupportedChainsAndTokensResult = readonly {\n tokens?: { symbol?: string; equivalentCurrency?: string }[];\n [key: string]: unknown;\n}[];\n\nconst COINBASE_SPOT_API_BASE = \"https://api.coinbase.com/v2/prices\";\nconst COINBASE_EXCHANGE_RATES_API_BASE =\n \"https://api.coinbase.com/v2/exchange-rates\";\n\nexport const DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS = 4_000;\nexport const USD_PEGGED_FALLBACK_RATE = 1;\nexport const DEFAULT_USD_PEGGED_TOKEN_SYMBOLS = [\n \"USDT\",\n \"USDC\",\n \"USDS\",\n \"DAI\",\n \"USDM\",\n \"FDUSD\",\n \"BUSD\",\n \"TUSD\",\n \"PYUSD\",\n \"GUSD\",\n \"LUSD\",\n \"USDE\",\n \"USDP\",\n] as const;\n\ntype CoinbaseSpotPriceResponse = {\n data?: {\n amount?: string | number;\n };\n};\n\ntype CoinbaseExchangeRatesResponse = {\n data?: {\n rates?: Record;\n };\n};\n\ntype SupportedTokenMetadata = {\n symbol?: string;\n equivalentCurrency?: string;\n};\n\ntype SupportedChainMetadata = {\n tokens?: SupportedTokenMetadata[];\n};\n\nexport function normalizeTokenSymbol(tokenSymbol: string): string {\n return tokenSymbol.trim().toUpperCase();\n}\n\nexport function toFinitePositiveNumber(value: unknown): number | null {\n const parsed = Number.parseFloat(String(value));\n if (!Number.isFinite(parsed) || parsed <= 0) {\n return null;\n }\n return parsed;\n}\n\nexport function getCoinbaseSymbolCandidates(tokenSymbol: string): string[] {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return [];\n\n const baseSymbol = normalized.split(/[._-]/)[0] ?? normalized;\n const wrappedBase =\n baseSymbol.startsWith(\"W\") && baseSymbol.length > 3\n ? baseSymbol.slice(1)\n : null;\n\n return Array.from(\n new Set(\n [normalized, baseSymbol, wrappedBase].filter(\n (symbol): symbol is string => Boolean(symbol),\n ),\n ),\n );\n}\n\nexport function buildUsdPeggedSymbolSet(\n supportedChains: SupportedChainsAndTokensResult | null,\n baseSymbols: Iterable = DEFAULT_USD_PEGGED_TOKEN_SYMBOLS,\n): Set {\n const symbolSet = new Set(baseSymbols);\n\n for (const chain of (supportedChains ?? []) as SupportedChainMetadata[]) {\n for (const token of chain.tokens ?? []) {\n const symbol = normalizeTokenSymbol(token.symbol ?? \"\");\n const equivalent = normalizeTokenSymbol(token.equivalentCurrency ?? \"\");\n if (!symbol) continue;\n\n if (equivalent && symbolSet.has(equivalent)) {\n symbolSet.add(symbol);\n }\n }\n }\n\n return symbolSet;\n}\n\nasync function fetchJsonWithTimeout(\n url: string,\n requestTimeoutMs: number,\n): Promise {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);\n try {\n const response = await fetch(url, {\n signal: controller.signal,\n });\n if (!response.ok) return null;\n return (await response.json()) as T;\n } catch {\n return null;\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nexport async function fetchCoinbaseUsdRate(\n tokenSymbol: string,\n requestTimeoutMs = DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS,\n): Promise {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return null;\n\n for (const candidate of getCoinbaseSymbolCandidates(normalized)) {\n const spotBody = await fetchJsonWithTimeout(\n `${COINBASE_SPOT_API_BASE}/${encodeURIComponent(candidate)}-USD/spot`,\n requestTimeoutMs,\n );\n const spotAmount = toFinitePositiveNumber(spotBody?.data?.amount);\n if (spotAmount) return spotAmount;\n\n const exchangeRatesBody =\n await fetchJsonWithTimeout(\n `${COINBASE_EXCHANGE_RATES_API_BASE}?currency=${encodeURIComponent(candidate)}`,\n requestTimeoutMs,\n );\n const exchangeRatesAmount = toFinitePositiveNumber(\n exchangeRatesBody?.data?.rates?.USD,\n );\n if (exchangeRatesAmount) return exchangeRatesAmount;\n }\n\n return null;\n}\n", "type": "registry:component", "target": "components/common/utils/token-pricing.ts" }, { "path": "registry/nexus-elements/common/utils/transaction-flow.ts", - "content": "import {\n formatUnits,\n type NexusNetwork,\n NexusSDK,\n SUPPORTED_CHAINS,\n type SUPPORTED_CHAINS_IDS,\n type SUPPORTED_TOKENS,\n} from \"@avail-project/nexus-core\";\nimport { type Address } from \"viem\";\n\nconst MAX_AMOUNT_REGEX = /^\\d*\\.?\\d+$/;\n\nexport const MAX_AMOUNT_DEBOUNCE_MS = 300;\n\nexport const normalizeMaxAmount = (\n maxAmount?: string | number,\n): string | undefined => {\n if (maxAmount === undefined || maxAmount === null) return undefined;\n const value = String(maxAmount).trim();\n if (!value || value === \".\" || !MAX_AMOUNT_REGEX.test(value)) {\n return undefined;\n }\n const parsed = Number.parseFloat(value);\n if (!Number.isFinite(parsed) || parsed <= 0) return undefined;\n return value;\n};\n\nexport const clampAmountToMax = ({\n amount,\n maxAmount,\n nexusSDK,\n token,\n chainId,\n}: {\n amount: string;\n maxAmount?: string;\n nexusSDK: NexusSDK;\n token: SUPPORTED_TOKENS;\n chainId: SUPPORTED_CHAINS_IDS;\n}): string => {\n if (!maxAmount) return amount;\n try {\n const amountRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n token,\n chainId,\n );\n const maxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxAmount,\n token,\n chainId,\n );\n return amountRaw > maxRaw ? maxAmount : amount;\n } catch {\n return amount;\n }\n};\n\nexport const formatAmountForDisplay = (\n amount: bigint,\n decimals: number | undefined,\n nexusSDK: NexusSDK,\n): string => {\n if (typeof decimals !== \"number\") return amount.toString();\n const formatted = formatUnits(amount, decimals);\n if (!formatted.includes(\".\")) return formatted;\n const [whole, fraction] = formatted.split(\".\");\n const trimmedFraction = fraction.slice(0, 6).replace(/0+$/, \"\");\n if (!trimmedFraction && whole === \"0\" && amount > BigInt(0)) {\n return \"0.000001\";\n }\n return trimmedFraction ? `${whole}.${trimmedFraction}` : whole;\n};\n\nexport const buildInitialInputs = ({\n type,\n network,\n connectedAddress,\n prefill,\n}: {\n type: \"bridge\" | \"transfer\";\n network: NexusNetwork;\n connectedAddress?: Address;\n prefill?: {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n };\n}) => {\n return {\n chain:\n (prefill?.chainId as SUPPORTED_CHAINS_IDS) ??\n (network === \"testnet\"\n ? SUPPORTED_CHAINS.SEPOLIA\n : SUPPORTED_CHAINS.ETHEREUM),\n token: (prefill?.token as SUPPORTED_TOKENS) ?? \"USDC\",\n amount: prefill?.amount ?? undefined,\n recipient:\n (prefill?.recipient as `0x${string}`) ??\n (type === \"bridge\" ? connectedAddress : undefined),\n };\n};\n\nexport const getCoverageDecimals = ({\n type,\n token,\n chainId,\n fallback,\n}: {\n type: \"bridge\" | \"transfer\";\n token?: SUPPORTED_TOKENS;\n chainId?: SUPPORTED_CHAINS_IDS;\n fallback: number | undefined;\n}) => {\n if (token === \"USDM\") return 18;\n if (\n type === \"bridge\" &&\n token === \"USDC\" &&\n chainId === SUPPORTED_CHAINS.BNB\n ) {\n return 18;\n }\n return fallback;\n};\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type { NexusNetwork } from \"@avail-project/nexus-sdk-v2\";\nimport { formatUnits, type Address } from \"viem\";\n\ntype NexusClient = ReturnType;\n\nconst MAX_AMOUNT_REGEX = /^\\d*\\.?\\d+$/;\n\n// v2 chain IDs for defaults\nconst SEPOLIA_CHAIN_ID = 11155111;\nconst ETHEREUM_CHAIN_ID = 1;\n// v2: BNB chain ID for edge-case decimal override\nconst BNB_CHAIN_ID = 56;\n\nexport const MAX_AMOUNT_DEBOUNCE_MS = 300;\n\nexport const normalizeMaxAmount = (\n maxAmount?: string | number,\n): string | undefined => {\n if (maxAmount === undefined || maxAmount === null) return undefined;\n const value = String(maxAmount).trim();\n if (!value || value === \".\" || !MAX_AMOUNT_REGEX.test(value)) {\n return undefined;\n }\n const parsed = Number.parseFloat(value);\n if (!Number.isFinite(parsed) || parsed <= 0) return undefined;\n return value;\n};\n\nexport const clampAmountToMax = ({\n amount,\n maxAmount,\n nexusSDK,\n token,\n chainId,\n}: {\n amount: string;\n maxAmount?: string;\n nexusSDK: NexusClient;\n token: string;\n chainId: number;\n}): string => {\n if (!maxAmount) return amount;\n try {\n // v2: convertTokenReadableAmountToBigInt(amount, tokenSymbol, chainId)\n const amountRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n token,\n chainId,\n );\n const maxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxAmount,\n token,\n chainId,\n );\n return amountRaw > maxRaw ? maxAmount : amount;\n } catch {\n return amount;\n }\n};\n\nexport const formatAmountForDisplay = (\n amount: bigint,\n decimals: number | undefined,\n // nexusSDK kept for API compatibility but formatUnits is now imported directly\n _nexusSDK: NexusClient,\n): string => {\n if (typeof decimals !== \"number\") return amount.toString();\n const formatted = formatUnits(amount, decimals);\n if (!formatted.includes(\".\")) return formatted;\n const [whole, fraction] = formatted.split(\".\");\n const trimmedFraction = fraction.slice(0, 6).replace(/0+$/, \"\");\n if (!trimmedFraction && whole === \"0\" && amount > BigInt(0)) {\n return \"0.000001\";\n }\n return trimmedFraction ? `${whole}.${trimmedFraction}` : whole;\n};\n\nexport const buildInitialInputs = ({\n type,\n network,\n connectedAddress,\n prefill,\n}: {\n type: \"bridge\" | \"transfer\";\n network: NexusNetwork;\n connectedAddress?: Address;\n prefill?: {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n };\n}) => {\n return {\n // v2 uses plain number chain IDs and string token symbols\n chain:\n prefill?.chainId ??\n (network === \"testnet\" ? SEPOLIA_CHAIN_ID : ETHEREUM_CHAIN_ID),\n token: prefill?.token ?? \"USDC\",\n amount: prefill?.amount ?? undefined,\n recipient:\n (prefill?.recipient as `0x${string}`) ??\n (type === \"bridge\" ? connectedAddress : undefined),\n };\n};\n\nexport const getCoverageDecimals = ({\n type,\n token,\n chainId,\n fallback,\n}: {\n type: \"bridge\" | \"transfer\";\n token?: string;\n chainId?: number;\n fallback: number | undefined;\n}) => {\n if (token === \"USDM\") return 18;\n if (type === \"bridge\" && token === \"USDC\" && chainId === BNB_CHAIN_ID) {\n return 18;\n }\n return fallback;\n};\n", "type": "registry:component", "target": "components/common/utils/transaction-flow.ts" } diff --git a/public/r/transfer.json b/public/r/transfer.json index 8d172ec..70e6f40 100644 --- a/public/r/transfer.json +++ b/public/r/transfer.json @@ -5,7 +5,7 @@ "title": "Fast Transfer", "description": "A simple component built with Nexus to enable cross chain transfer", "dependencies": [ - "@avail-project/nexus-core@1.2.0", + "@avail-project/nexus-sdk-v2", "lucide-react", "viem" ], @@ -27,61 +27,61 @@ "files": [ { "path": "registry/nexus-elements/transfer/components/allowance-modal.tsx", - "content": "\"use client\";\nimport React, {\n type FC,\n memo,\n type RefObject,\n useEffect,\n useMemo,\n useState,\n} from \"react\";\nimport { Button } from \"../../ui/button\";\nimport { Input } from \"../../ui/input\";\nimport { Label } from \"../../ui/label\";\nimport {\n type AllowanceHookSource,\n CHAIN_METADATA,\n formatTokenBalance,\n type OnAllowanceHookData,\n parseUnits,\n} from \"@avail-project/nexus-core\";\nimport { useNexusError } from \"../../common\";\n\ninterface AllowanceModalProps {\n allowance: RefObject;\n callback?: () => void;\n onCloseCallback?: () => void;\n onError?: (message: string) => void;\n}\n\ntype AllowanceChoice = \"min\" | \"max\" | \"custom\";\n\ninterface AllowanceOptionProps {\n index: number;\n name: string;\n choice: AllowanceChoice;\n selectedChoice?: AllowanceChoice;\n onSelect: (index: number, choice: AllowanceChoice) => void;\n title: string;\n description?: string;\n children?: React.ReactNode;\n allowanceValue?: string;\n}\n\nconst ALLOWANCE_CHOICES: Array<{\n choice: AllowanceChoice;\n title: string;\n description: string;\n}> = [\n {\n choice: \"min\",\n title: \"Minimum\",\n description: \"Grant the lowest allowance required for this action.\",\n },\n {\n choice: \"max\",\n title: \"Maximum\",\n description: \"Approve once and skip future approvals for this token.\",\n },\n {\n choice: \"custom\",\n title: \"Custom amount\",\n description: \"Specify an allowance that fits your threshold.\",\n },\n];\n\nconst AllowanceOption: FC = ({\n index,\n name,\n choice,\n selectedChoice,\n onSelect,\n title,\n description,\n children,\n allowanceValue,\n}) => {\n const isActive = selectedChoice === choice;\n\n return (\n \n );\n};\n\nconst AllowanceModal: FC = ({\n allowance,\n callback,\n onCloseCallback,\n onError,\n}) => {\n const handleNexusError = useNexusError();\n const [selectedOption, setSelectedOption] = useState([]);\n const [customValues, setCustomValues] = useState([]);\n\n const { sources, allow, deny } = allowance.current ?? {\n sources: [],\n allow: () => {},\n deny: () => {},\n };\n\n const defaultChoices = useMemo(\n () => Array.from({ length: sources.length }, () => \"min\"),\n [sources.length],\n );\n\n const isCustomValueValid = (\n value: string,\n minimumRaw: bigint,\n decimals: number,\n ): boolean => {\n if (!value || value.trim() === \"\") return false;\n try {\n const parsedValue = parseUnits(value, decimals);\n if (parsedValue === undefined) return false;\n return parsedValue >= minimumRaw;\n } catch {\n return false;\n }\n };\n\n const hasValidationErrors = useMemo(() => {\n return sources.some((source, index) => {\n if (selectedOption[index] !== \"custom\") return false;\n const value = customValues[index];\n if (!value || value.trim() === \"\") return false;\n return !isCustomValueValid(\n value,\n source.allowance.minimumRaw,\n source.token.decimals,\n );\n });\n }, [sources, selectedOption, customValues, isCustomValueValid]);\n\n const onClose = () => {\n deny();\n allowance.current = null;\n onCloseCallback?.();\n };\n\n const onApprove = () => {\n const processed = sources.map((_, i) => {\n const opt = selectedOption[i];\n if (opt === \"min\" || opt === \"max\") return opt;\n const rawValue = customValues[i]?.trim();\n if (!rawValue) return \"min\";\n const parsed = Number(rawValue);\n if (!Number.isFinite(parsed) || parsed < 0) return \"min\";\n return rawValue;\n });\n try {\n allow(processed);\n allowance.current = null;\n callback?.();\n } catch (error) {\n const { message } = handleNexusError(error);\n console.error(\"AllowanceModal onApprove error\", error);\n allowance.current = null;\n onError?.(message);\n onCloseCallback?.();\n }\n };\n\n const handleChoiceChange = (index: number, value: AllowanceChoice) => {\n setSelectedOption((prev) => {\n const next = [...(prev.length ? prev : defaultChoices)];\n next[index] = value;\n return next;\n });\n };\n\n const formatAmount = (value: string | bigint, source: AllowanceHookSource) =>\n formatTokenBalance(value, {\n symbol: source.token.symbol,\n decimals: source.token.decimals,\n }) ?? \"—\";\n\n useEffect(() => {\n setSelectedOption(defaultChoices);\n }, [defaultChoices]);\n\n useEffect(() => {\n setCustomValues(Array.from({ length: sources.length }, () => \"\"));\n }, [sources.length]);\n\n return (\n <>\n
\n

\n Set Token Allowances\n

\n

\n Review every required token and choose the minimum, an unlimited max,\n or define a custom amount before approving.\n

\n
\n\n
\n {sources?.map((source: AllowanceHookSource, index: number) => (\n \n
\n
\n
\n \n
\n
\n

\n {source.token.symbol}\n

\n

\n {source.chain.name}\n

\n
\n
\n\n
\n

\n Current allowance\n

\n

\n {formatAmount(source.allowance.currentRaw, source)}\n

\n
\n
\n\n
\n {ALLOWANCE_CHOICES.map((choice) => {\n if (choice.choice === \"custom\") {\n const customValue = customValues[index] ?? \"\";\n const isCustomSelected = selectedOption[index] === \"custom\";\n const showError =\n isCustomSelected &&\n customValue.trim() !== \"\" &&\n !isCustomValueValid(\n customValue,\n source.allowance.minimumRaw,\n source.token.decimals,\n );\n return (\n \n
\n {\n const next = [...customValues];\n next[index] = e.target.value;\n setCustomValues(next);\n }}\n maxLength={source.token.decimals}\n className={`h-9 w-40 rounded-lg border bg-background/80 text-sm disabled:opacity-60 ${\n showError ? \"border-destructive\" : \"\"\n }`}\n disabled={!isCustomSelected}\n />\n {showError && (\n

\n Min: {source.allowance.minimum}\n

\n )}\n
\n \n );\n }\n return (\n \n );\n })}\n
\n
\n ))}\n \n\n
\n \n \n Approve Selected\n \n
\n \n );\n};\n\nAllowanceModal.displayName = \"AllowanceModal\";\n\nexport default memo(AllowanceModal);\n", + "content": "\"use client\";\nimport React, {\n type FC,\n memo,\n type RefObject,\n useEffect,\n useMemo,\n useState,\n} from \"react\";\nimport { Button } from \"../../ui/button\";\nimport { Input } from \"../../ui/input\";\nimport { Label } from \"../../ui/label\";\nimport {\n type AllowanceHookSource,\n type OnAllowanceHookData,\n} from \"@avail-project/nexus-sdk-v2\";\nimport { formatTokenBalance } from \"@avail-project/nexus-sdk-v2/utils\";\nimport { parseUnits } from \"viem\";\nimport { useNexusError } from \"../../common\";\n\ninterface AllowanceModalProps {\n allowance: RefObject;\n callback?: () => void;\n onCloseCallback?: () => void;\n onError?: (message: string) => void;\n}\n\ntype AllowanceChoice = \"min\" | \"max\" | \"custom\";\n\ninterface AllowanceOptionProps {\n index: number;\n name: string;\n choice: AllowanceChoice;\n selectedChoice?: AllowanceChoice;\n onSelect: (index: number, choice: AllowanceChoice) => void;\n title: string;\n description?: string;\n children?: React.ReactNode;\n allowanceValue?: string;\n}\n\nconst ALLOWANCE_CHOICES: Array<{\n choice: AllowanceChoice;\n title: string;\n description: string;\n}> = [\n {\n choice: \"min\",\n title: \"Minimum\",\n description: \"Grant the lowest allowance required for this action.\",\n },\n {\n choice: \"max\",\n title: \"Maximum\",\n description: \"Approve once and skip future approvals for this token.\",\n },\n {\n choice: \"custom\",\n title: \"Custom amount\",\n description: \"Specify an allowance that fits your threshold.\",\n },\n];\n\nconst AllowanceOption: FC = ({\n index,\n name,\n choice,\n selectedChoice,\n onSelect,\n title,\n description,\n children,\n allowanceValue,\n}) => {\n const isActive = selectedChoice === choice;\n\n return (\n \n );\n};\n\nconst AllowanceModal: FC = ({\n allowance,\n callback,\n onCloseCallback,\n onError,\n}) => {\n const handleNexusError = useNexusError();\n const [selectedOption, setSelectedOption] = useState([]);\n const [customValues, setCustomValues] = useState([]);\n\n const { sources, allow, deny } = allowance.current ?? {\n sources: [],\n allow: () => {},\n deny: () => {},\n };\n\n const defaultChoices = useMemo(\n () => Array.from({ length: sources.length }, () => \"min\"),\n [sources.length],\n );\n\n const isCustomValueValid = (\n value: string,\n minimumRaw: bigint,\n decimals: number,\n ): boolean => {\n if (!value || value.trim() === \"\") return false;\n try {\n const parsedValue = parseUnits(value, decimals);\n if (parsedValue === undefined) return false;\n return parsedValue >= minimumRaw;\n } catch {\n return false;\n }\n };\n\n const hasValidationErrors = useMemo(() => {\n return sources.some((source, index) => {\n if (selectedOption[index] !== \"custom\") return false;\n const value = customValues[index];\n if (!value || value.trim() === \"\") return false;\n return !isCustomValueValid(\n value,\n source.allowance.minimumRaw,\n source.token.decimals,\n );\n });\n }, [sources, selectedOption, customValues, isCustomValueValid]);\n\n const onClose = () => {\n deny();\n allowance.current = null;\n onCloseCallback?.();\n };\n\n const onApprove = () => {\n const processed = sources.map((_, i) => {\n const opt = selectedOption[i];\n if (opt === \"min\" || opt === \"max\") return opt;\n const rawValue = customValues[i]?.trim();\n if (!rawValue) return \"min\";\n const parsed = Number(rawValue);\n if (!Number.isFinite(parsed) || parsed < 0) return \"min\";\n return rawValue;\n });\n try {\n allow(processed);\n allowance.current = null;\n callback?.();\n } catch (error) {\n const { message } = handleNexusError(error);\n console.error(\"AllowanceModal onApprove error\", error);\n allowance.current = null;\n onError?.(message);\n onCloseCallback?.();\n }\n };\n\n const handleChoiceChange = (index: number, value: AllowanceChoice) => {\n setSelectedOption((prev) => {\n const next = [...(prev.length ? prev : defaultChoices)];\n next[index] = value;\n return next;\n });\n };\n\n const formatAmount = (value: string | bigint, source: AllowanceHookSource) =>\n formatTokenBalance(value, {\n symbol: source.token.symbol,\n decimals: source.token.decimals,\n }) ?? \"—\";\n\n useEffect(() => {\n setSelectedOption(defaultChoices);\n }, [defaultChoices]);\n\n useEffect(() => {\n setCustomValues(Array.from({ length: sources.length }, () => \"\"));\n }, [sources.length]);\n\n return (\n <>\n
\n

\n Set Token Allowances\n

\n

\n Review every required token and choose the minimum, an unlimited max,\n or define a custom amount before approving.\n

\n
\n\n
\n {sources?.map((source: AllowanceHookSource, index: number) => (\n \n
\n
\n
\n \n
\n
\n

\n {source.token.symbol}\n

\n

\n {source.chain.name}\n

\n
\n
\n\n
\n

\n Current allowance\n

\n

\n {formatAmount(source.allowance.currentRaw, source)}\n

\n
\n
\n\n
\n {ALLOWANCE_CHOICES.map((choice) => {\n if (choice.choice === \"custom\") {\n const customValue = customValues[index] ?? \"\";\n const isCustomSelected = selectedOption[index] === \"custom\";\n const showError =\n isCustomSelected &&\n customValue.trim() !== \"\" &&\n !isCustomValueValid(\n customValue,\n source.allowance.minimumRaw,\n source.token.decimals,\n );\n return (\n \n
\n {\n const next = [...customValues];\n next[index] = e.target.value;\n setCustomValues(next);\n }}\n maxLength={source.token.decimals}\n className={`h-9 w-40 rounded-lg border bg-background/80 text-sm disabled:opacity-60 ${\n showError ? \"border-destructive\" : \"\"\n }`}\n disabled={!isCustomSelected}\n />\n {showError && (\n

\n Min: {source.allowance.minimum}\n

\n )}\n
\n \n );\n }\n return (\n \n );\n })}\n
\n
\n ))}\n \n\n
\n \n \n Approve Selected\n \n
\n \n );\n};\n\nAllowanceModal.displayName = \"AllowanceModal\";\n\nexport default memo(AllowanceModal);\n", "type": "registry:component", "target": "components/transfer/components/allowance-modal.tsx" }, { "path": "registry/nexus-elements/transfer/components/amount-input.tsx", - "content": "import { type FC, Fragment, useEffect, useMemo, useRef } from \"react\";\nimport { Input } from \"../../ui/input\";\nimport { Button } from \"../../ui/button\";\nimport { formatTokenBalance, type UserAsset } from \"@avail-project/nexus-core\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { type FastTransferState } from \"../hooks/useTransfer\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"../../ui/accordion\";\nimport { SHORT_CHAIN_NAME } from \"../../common\";\nimport {\n clampAmountToMax,\n normalizeMaxAmount,\n} from \"../../common/utils/transaction-flow\";\nimport { LoaderCircle } from \"lucide-react\";\n\ninterface AmountInputProps {\n amount?: string;\n onChange: (value: string) => void;\n bridgableBalance?: UserAsset;\n onCommit?: (value: string) => void;\n disabled?: boolean;\n inputs: FastTransferState;\n maxAmount?: string | number;\n maxAvailableAmount?: string;\n}\n\nconst AmountInput: FC = ({\n amount,\n onChange,\n bridgableBalance,\n onCommit,\n disabled,\n inputs,\n maxAmount,\n maxAvailableAmount,\n}) => {\n const { nexusSDK, loading } = useNexus();\n const commitTimerRef = useRef(null);\n const normalizedMaxAmount = useMemo(\n () => normalizeMaxAmount(maxAmount),\n [maxAmount],\n );\n\n const applyMaxCap = (value: string) => {\n if (!nexusSDK || !inputs?.token || !inputs?.chain) {\n return value;\n }\n return clampAmountToMax({\n amount: value,\n maxAmount: normalizedMaxAmount,\n nexusSDK,\n token: inputs.token,\n chainId: inputs.chain,\n });\n };\n\n const scheduleCommit = (val: string) => {\n if (!onCommit || disabled) return;\n if (commitTimerRef.current) clearTimeout(commitTimerRef.current);\n commitTimerRef.current = setTimeout(() => {\n onCommit(val);\n }, 800);\n };\n\n const onMaxClick = () => {\n if (!maxAvailableAmount) return;\n const capped = applyMaxCap(maxAvailableAmount);\n onChange(capped);\n onCommit?.(capped);\n };\n\n useEffect(() => {\n return () => {\n if (commitTimerRef.current) {\n clearTimeout(commitTimerRef.current);\n commitTimerRef.current = null;\n }\n };\n }, []);\n\n return (\n
\n
\n {\n let next = e.target.value.replaceAll(/[^0-9.]/g, \"\");\n const parts = next.split(\".\");\n if (parts.length > 2)\n next = parts[0] + \".\" + parts.slice(1).join(\"\");\n if (next === \".\") next = \"0.\";\n onChange(next);\n scheduleCommit(next);\n }}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n if (commitTimerRef.current) {\n clearTimeout(commitTimerRef.current);\n commitTimerRef.current = null;\n }\n onCommit?.(amount ?? \"\");\n }\n }}\n className=\"w-full border-none bg-transparent rounded-r-none focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none py-0 px-3 h-12!\"\n aria-invalid={Boolean(amount) && Number.isNaN(Number(amount))}\n disabled={disabled || loading}\n />\n
\n {bridgableBalance && (\n

\n {formatTokenBalance(bridgableBalance?.balance, {\n symbol: bridgableBalance?.symbol,\n decimals: bridgableBalance?.decimals,\n })}\n

\n )}\n {loading && !bridgableBalance && (\n \n )}\n \n MAX\n \n
\n
\n \n \n \n View Balance Breakdown\n \n \n
\n {bridgableBalance?.breakdown.map((chain) => {\n if (Number.parseFloat(chain.balance) === 0) return null;\n if (inputs?.chain === chain.chain.id) return null;\n return (\n \n
\n
\n
\n \n
\n \n {SHORT_CHAIN_NAME[chain.chain.id]}\n \n
\n

\n {formatTokenBalance(chain.balance, {\n symbol: chain.symbol,\n decimals: chain.decimals,\n })}\n

\n
\n
\n );\n })}\n
\n
\n
\n
\n
\n );\n};\n\nexport default AmountInput;\n", + "content": "import { type FC, Fragment, useEffect, useMemo, useRef } from \"react\";\nimport { Input } from \"../../ui/input\";\nimport { Button } from \"../../ui/button\";\nimport { type TokenBalance as UserAsset } from \"@avail-project/nexus-sdk-v2\";\nimport { formatTokenBalance } from \"@avail-project/nexus-sdk-v2/utils\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { type FastTransferState } from \"../hooks/useTransfer\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"../../ui/accordion\";\nimport { SHORT_CHAIN_NAME } from \"../../common\";\nimport {\n clampAmountToMax,\n normalizeMaxAmount,\n} from \"../../common/utils/transaction-flow\";\nimport { LoaderCircle } from \"lucide-react\";\n\ninterface AmountInputProps {\n amount?: string;\n onChange: (value: string) => void;\n bridgableBalance?: UserAsset;\n onCommit?: (value: string) => void;\n disabled?: boolean;\n inputs: FastTransferState;\n maxAmount?: string | number;\n maxAvailableAmount?: string;\n}\n\nconst AmountInput: FC = ({\n amount,\n onChange,\n bridgableBalance,\n onCommit,\n disabled,\n inputs,\n maxAmount,\n maxAvailableAmount,\n}) => {\n const { nexusSDK, loading } = useNexus();\n const commitTimerRef = useRef(null);\n const normalizedMaxAmount = useMemo(\n () => normalizeMaxAmount(maxAmount),\n [maxAmount],\n );\n\n const applyMaxCap = (value: string) => {\n if (!nexusSDK || !inputs?.token || !inputs?.chain) {\n return value;\n }\n return clampAmountToMax({\n amount: value,\n maxAmount: normalizedMaxAmount,\n nexusSDK,\n token: inputs.token,\n chainId: inputs.chain,\n });\n };\n\n const scheduleCommit = (val: string) => {\n if (!onCommit || disabled) return;\n if (commitTimerRef.current) clearTimeout(commitTimerRef.current);\n commitTimerRef.current = setTimeout(() => {\n onCommit(val);\n }, 800);\n };\n\n const onMaxClick = () => {\n if (!maxAvailableAmount) return;\n const capped = applyMaxCap(maxAvailableAmount);\n onChange(capped);\n onCommit?.(capped);\n };\n\n useEffect(() => {\n return () => {\n if (commitTimerRef.current) {\n clearTimeout(commitTimerRef.current);\n commitTimerRef.current = null;\n }\n };\n }, []);\n\n return (\n
\n
\n {\n let next = e.target.value.replaceAll(/[^0-9.]/g, \"\");\n const parts = next.split(\".\");\n if (parts.length > 2)\n next = parts[0] + \".\" + parts.slice(1).join(\"\");\n if (next === \".\") next = \"0.\";\n onChange(next);\n scheduleCommit(next);\n }}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n if (commitTimerRef.current) {\n clearTimeout(commitTimerRef.current);\n commitTimerRef.current = null;\n }\n onCommit?.(amount ?? \"\");\n }\n }}\n className=\"w-full border-none bg-transparent rounded-r-none focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none py-0 px-3 h-12!\"\n aria-invalid={Boolean(amount) && Number.isNaN(Number(amount))}\n disabled={disabled || loading}\n />\n
\n {bridgableBalance && (\n

\n {formatTokenBalance(bridgableBalance?.balance, {\n symbol: bridgableBalance?.symbol,\n decimals: bridgableBalance?.decimals,\n })}\n

\n )}\n {loading && !bridgableBalance && (\n \n )}\n \n MAX\n \n
\n
\n \n \n \n View Balance Breakdown\n \n \n
\n {bridgableBalance?.chainBalances?.map((chain: any) => {\n if (Number.parseFloat(chain.balance) === 0) return null;\n if (inputs?.chain === chain.chain.id) return null;\n return (\n \n
\n
\n
\n \n
\n \n {SHORT_CHAIN_NAME[chain.chain.id]}\n \n
\n

\n {formatTokenBalance(chain.balance, {\n symbol: chain.symbol,\n decimals: chain.decimals,\n })}\n

\n
\n
\n );\n })}\n
\n
\n
\n
\n
\n );\n};\n\nexport default AmountInput;\n", "type": "registry:component", "target": "components/transfer/components/amount-input.tsx" }, { "path": "registry/nexus-elements/transfer/components/chain-select.tsx", - "content": "import { type FC, useMemo } from \"react\";\nimport { Label } from \"../../ui/label\";\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../../ui/select\";\nimport { type SUPPORTED_CHAINS_IDS } from \"@avail-project/nexus-core\";\nimport { cn } from \"@/lib/utils\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\n\ninterface ChainSelectProps {\n selectedChain: number;\n disabled?: boolean;\n hidden?: boolean;\n className?: string;\n label?: string;\n handleSelect: (chainId: SUPPORTED_CHAINS_IDS) => void;\n}\n\nconst ChainSelect: FC = ({\n selectedChain,\n disabled,\n hidden = false,\n className,\n label,\n handleSelect,\n}) => {\n const { supportedChainsAndTokens } = useNexus();\n const selectedChainData = useMemo(() => {\n if (!supportedChainsAndTokens) return null;\n return supportedChainsAndTokens.find((c) => c.id === selectedChain);\n }, [selectedChain, supportedChainsAndTokens]);\n\n if (hidden) return null;\n return (\n {\n if (!disabled) {\n handleSelect(Number.parseInt(value) as SUPPORTED_CHAINS_IDS);\n }\n }}\n >\n
\n {label && }\n \n \n {selectedChainData && (\n \n \n

\n {selectedChainData?.name}\n

\n
\n )}\n \n \n \n\n \n \n {supportedChainsAndTokens?.map((chain) => {\n return (\n \n
\n \n

{chain.name}

\n
\n
\n );\n })}\n
\n
\n \n );\n};\n\nexport default ChainSelect;\n", + "content": "import { type FC, useMemo } from \"react\";\nimport { Label } from \"../../ui/label\";\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../../ui/select\";\n// v2: SUPPORTED_CHAINS_IDS removed — use plain number\nimport { cn } from \"@/lib/utils\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\n\ninterface ChainSelectProps {\n selectedChain: number;\n disabled?: boolean;\n hidden?: boolean;\n className?: string;\n label?: string;\n handleSelect: (chainId: number) => void;\n}\n\nconst ChainSelect: FC = ({\n selectedChain,\n disabled,\n hidden = false,\n className,\n label,\n handleSelect,\n}) => {\n const { supportedChainsAndTokens } = useNexus();\n const selectedChainData = useMemo(() => {\n if (!supportedChainsAndTokens) return null;\n return supportedChainsAndTokens.find((c) => c.id === selectedChain);\n }, [selectedChain, supportedChainsAndTokens]);\n\n if (hidden) return null;\n return (\n {\n if (!disabled) {\n handleSelect(Number.parseInt(value));\n }\n }}\n >\n
\n {label && }\n \n \n {selectedChainData && (\n \n \n

\n {selectedChainData?.name}\n

\n
\n )}\n \n \n \n\n \n \n {supportedChainsAndTokens?.map((chain) => {\n return (\n \n
\n \n

{chain.name}

\n
\n
\n );\n })}\n
\n
\n \n );\n};\n\nexport default ChainSelect;\n", "type": "registry:component", "target": "components/transfer/components/chain-select.tsx" }, { "path": "registry/nexus-elements/transfer/components/fee-breakdown.tsx", - "content": "import { type FC } from \"react\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"../../ui/accordion\";\nimport {\n formatTokenBalance,\n SUPPORTED_TOKENS,\n type ReadableIntent,\n} from \"@avail-project/nexus-core\";\nimport { Skeleton } from \"../../ui/skeleton\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../../ui/tooltip\";\nimport { MessageCircleQuestion } from \"lucide-react\";\n\ninterface FeeBreakdownProps {\n intent: ReadableIntent;\n tokenSymbol: SUPPORTED_TOKENS;\n isLoading?: boolean;\n}\n\nconst FeeBreakdown: FC = ({\n intent,\n tokenSymbol,\n isLoading = false,\n}) => {\n const { nexusSDK } = useNexus();\n\n const feeRows = [\n {\n key: \"caGas\",\n label: \"Fast Transfer Gas Fees\",\n value: intent?.fees?.caGas,\n description:\n \"Gas cost required to execute the transfer on the destination chain.\",\n },\n {\n key: \"gasSupplied\",\n label: \"Gas Supplied\",\n value: intent?.fees?.gasSupplied,\n description:\n \"Extra gas tokens supplied to ensure the transfer completes on-chain.\",\n },\n {\n key: \"solver\",\n label: \"Solver Fees\",\n value: intent?.fees?.solver,\n description:\n \"Paid to the solver that routes and confirms the transfer quickly.\",\n },\n {\n key: \"protocol\",\n label: \"Protocol Fees\",\n value: intent?.fees?.protocol,\n description:\n \"Nexus protocol fee that funds bridge maintenance and operations.\",\n },\n ];\n return (\n \n \n
\n

Total fees

\n\n
\n {isLoading ? (\n \n ) : (\n

\n {formatTokenBalance(intent.fees?.total, {\n symbol: tokenSymbol,\n decimals: intent?.token?.decimals,\n })}\n

\n )}\n \n

View Breakup

\n \n
\n
\n \n
\n {feeRows.map(({ key, label, value, description }) => {\n if (Number.parseFloat(value ?? \"0\") <= 0) return null;\n return (\n \n
\n
\n

{label}

\n \n \n \n
\n {isLoading ? (\n \n ) : (\n

\n {formatTokenBalance(value, {\n symbol: tokenSymbol,\n decimals: intent?.token?.decimals,\n })}\n

\n )}\n
\n \n

{description}

\n \n
\n );\n })}\n
\n
\n
\n
\n );\n};\n\nexport default FeeBreakdown;\n", + "content": "import { type FC } from \"react\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"../../ui/accordion\";\nimport { type BridgeIntent } from \"@avail-project/nexus-sdk-v2\";\nimport { formatTokenBalance } from \"@avail-project/nexus-sdk-v2/utils\";\nimport { Skeleton } from \"../../ui/skeleton\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../../ui/tooltip\";\nimport { MessageCircleQuestion } from \"lucide-react\";\n\ninterface FeeBreakdownProps {\n intent: BridgeIntent;\n tokenSymbol?: string; // v2: was SUPPORTED_TOKENS\n isLoading?: boolean;\n}\n\nconst FeeBreakdown: FC = ({\n intent,\n tokenSymbol,\n isLoading = false,\n}) => {\n const { nexusSDK } = useNexus();\n\n const feeRows = [\n {\n key: \"caGas\",\n label: \"Fast Transfer Gas Fees\",\n value: intent?.fees?.caGas,\n description:\n \"Gas cost required to execute the transfer on the destination chain.\",\n },\n {\n key: \"caGas\",\n label: \"Gas Supplied\",\n value: intent?.fees?.caGas,\n description:\n \"Extra gas tokens supplied to ensure the transfer completes on-chain.\",\n },\n {\n key: \"solver\",\n label: \"Solver Fees\",\n value: intent?.fees?.solver,\n description:\n \"Paid to the solver that routes and confirms the transfer quickly.\",\n },\n {\n key: \"protocol\",\n label: \"Protocol Fees\",\n value: intent?.fees?.protocol,\n description:\n \"Nexus protocol fee that funds bridge maintenance and operations.\",\n },\n ];\n return (\n \n \n
\n

Total fees

\n\n
\n {isLoading ? (\n \n ) : (\n

\n {formatTokenBalance(intent.fees?.total, {\n symbol: tokenSymbol,\n decimals: intent?.availableSources?.[0]?.token?.decimals,\n })}\n

\n )}\n \n

View Breakup

\n \n
\n
\n \n
\n {feeRows.map(({ key, label, value, description }) => {\n if (Number.parseFloat(value ?? \"0\") <= 0) return null;\n return (\n \n
\n
\n

{label}

\n \n \n \n
\n {isLoading ? (\n \n ) : (\n

\n {formatTokenBalance(value, {\n symbol: tokenSymbol,\n decimals: intent?.availableSources?.[0]?.token?.decimals,\n })}\n

\n )}\n
\n \n

{description}

\n \n
\n );\n })}\n
\n
\n
\n
\n );\n};\n\nexport default FeeBreakdown;\n", "type": "registry:component", "target": "components/transfer/components/fee-breakdown.tsx" }, { "path": "registry/nexus-elements/transfer/components/recipient-address.tsx", - "content": "\"use client\";\nimport { type FC, useState } from \"react\";\nimport { Input } from \"../../ui/input\";\nimport { Check, Edit } from \"lucide-react\";\nimport { Button } from \"../../ui/button\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { type Address } from \"viem\";\nimport { truncateAddress } from \"@avail-project/nexus-core\";\n\ninterface RecipientAddressProps {\n address?: Address;\n onChange: (address: string) => void;\n disabled?: boolean;\n}\n\nconst RecipientAddress: FC = ({\n address,\n onChange,\n disabled,\n}) => {\n const { nexusSDK } = useNexus();\n const [isEditing, setIsEditing] = useState(false);\n return (\n
\n {isEditing ? (\n
\n onChange(e.target.value)}\n className=\"w-full\"\n disabled={disabled}\n />\n {\n setIsEditing(false);\n }}\n disabled={disabled}\n >\n \n \n
\n ) : (\n
\n

Recipient Address

\n
\n {address && (\n

\n {truncateAddress(address, 6, 6)}\n

\n )}\n\n {\n setIsEditing(true);\n }}\n className=\"px-0 size-5\"\n disabled={disabled}\n >\n \n \n
\n
\n )}\n
\n );\n};\n\nexport default RecipientAddress;\n", + "content": "\"use client\";\nimport { type FC, useState } from \"react\";\nimport { Input } from \"../../ui/input\";\nimport { Check, Edit } from \"lucide-react\";\nimport { Button } from \"../../ui/button\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { type Address } from \"viem\";\nimport { truncateAddress } from \"@avail-project/nexus-sdk-v2/utils\";\n\ninterface RecipientAddressProps {\n address?: Address;\n onChange: (address: string) => void;\n disabled?: boolean;\n}\n\nconst RecipientAddress: FC = ({\n address,\n onChange,\n disabled,\n}) => {\n const { nexusSDK } = useNexus();\n const [isEditing, setIsEditing] = useState(false);\n return (\n
\n {isEditing ? (\n
\n onChange(e.target.value)}\n className=\"w-full\"\n disabled={disabled}\n />\n {\n setIsEditing(false);\n }}\n disabled={disabled}\n >\n \n \n
\n ) : (\n
\n

Recipient Address

\n
\n {address && (\n

\n {truncateAddress(address, 6, 6)}\n

\n )}\n\n {\n setIsEditing(true);\n }}\n className=\"px-0 size-5\"\n disabled={disabled}\n >\n \n \n
\n
\n )}\n
\n );\n};\n\nexport default RecipientAddress;\n", "type": "registry:component", "target": "components/transfer/components/recipient-address.tsx" }, { "path": "registry/nexus-elements/transfer/components/source-breakdown.tsx", - "content": "import {\n formatTokenBalance,\n type ReadableIntent,\n type SUPPORTED_TOKENS,\n type UserAsset,\n} from \"@avail-project/nexus-core\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"../../ui/accordion\";\nimport { Skeleton } from \"../../ui/skeleton\";\nimport { useMemo } from \"react\";\nimport { Checkbox } from \"../../ui/checkbox\";\nimport { cn } from \"@/lib/utils\";\n\ntype SourceCoverageState = \"healthy\" | \"warning\" | \"error\";\n\ninterface SourceBreakdownProps {\n intent?: ReadableIntent;\n tokenSymbol: SUPPORTED_TOKENS;\n isLoading?: boolean;\n requiredAmount?: string;\n availableSources: UserAsset[\"breakdown\"];\n selectedSourceChains: number[];\n onToggleSourceChain: (chainId: number) => void;\n onSourceMenuOpenChange?: (open: boolean) => void;\n isSourceSelectionInsufficient?: boolean;\n sourceCoverageState?: SourceCoverageState;\n sourceCoveragePercent?: number;\n missingToProceed?: string;\n missingToSafety?: string;\n selectedTotal?: string;\n requiredTotal?: string;\n requiredSafetyTotal?: string;\n}\n\nconst SourceBreakdown = ({\n intent,\n tokenSymbol,\n isLoading = false,\n requiredAmount,\n availableSources,\n selectedSourceChains,\n onToggleSourceChain,\n onSourceMenuOpenChange,\n isSourceSelectionInsufficient = false,\n sourceCoverageState = \"healthy\",\n sourceCoveragePercent = 100,\n missingToProceed,\n selectedTotal,\n requiredTotal,\n requiredSafetyTotal,\n}: SourceBreakdownProps) => {\n const displayTokenSymbol = availableSources[0]?.symbol ?? tokenSymbol;\n const normalizedCoverage = Math.max(0, Math.min(100, sourceCoveragePercent));\n const progressRadius = 16;\n const progressCircumference = 2 * Math.PI * progressRadius;\n const progressOffset =\n progressCircumference - (normalizedCoverage / 100) * progressCircumference;\n const showCoverageFeedback = Boolean(\n selectedTotal && requiredTotal && requiredSafetyTotal,\n );\n const shouldShowProceedMessage =\n sourceCoverageState === \"error\" &&\n Number.parseFloat(missingToProceed ?? \"0\") > 0;\n\n const coverageToneClass =\n sourceCoverageState === \"error\"\n ? \"text-rose-500\"\n : sourceCoverageState === \"warning\"\n ? \"text-amber-500\"\n : \"text-emerald-500\";\n\n const coverageSurfaceClass =\n sourceCoverageState === \"error\"\n ? \"border-rose-500/30 bg-rose-500/10 text-rose-950 dark:text-rose-200\"\n : sourceCoverageState === \"warning\"\n ? \"border-amber-500/30 bg-amber-500/10 text-amber-950 dark:text-amber-200\"\n : \"border-emerald-500/30 bg-emerald-500/10 text-emerald-950 dark:text-emerald-200\";\n const selectedSourceSet = new Set(selectedSourceChains);\n const bulkActionLabel =\n selectedSourceChains.length > 1 ? \"Deselect all\" : \"Select all\";\n const isBulkActionDisabled = availableSources.length <= 1;\n const handleBulkSourceAction = () => {\n if (isBulkActionDisabled) return;\n\n if (bulkActionLabel === \"Select all\") {\n availableSources.forEach((source) => {\n const chainId = source.chain.id;\n if (!selectedSourceSet.has(chainId)) {\n onToggleSourceChain(chainId);\n }\n });\n return;\n }\n\n const chainToKeep =\n availableSources.find((source) => selectedSourceSet.has(source.chain.id))\n ?.chain.id ?? selectedSourceChains[0];\n\n if (typeof chainToKeep !== \"number\") return;\n\n selectedSourceChains.forEach((chainId) => {\n if (chainId !== chainToKeep) {\n onToggleSourceChain(chainId);\n }\n });\n };\n\n const spendOnSources = useMemo(() => {\n if (!intent || (intent?.sources?.length ?? 0) < 2)\n return `1 asset on 1 chain`;\n return `${intent?.sources?.length} Assets on ${intent?.sources?.length} chains`;\n }, [intent]);\n\n const amountSpend = useMemo(() => {\n const base = Number(requiredAmount ?? \"0\");\n if (!intent) return base;\n const fees = Number.parseFloat(intent?.fees?.total ?? \"0\");\n return base + fees;\n }, [requiredAmount, intent]);\n\n return (\n onSourceMenuOpenChange?.(value === \"sources\")}\n >\n \n
\n {isLoading ? (\n <>\n
\n

You Spend

\n \n
\n
\n \n
\n \n
\n
\n \n ) : (\n intent && (\n <>\n
\n

You Spend

\n

{spendOnSources}

\n
\n\n
\n

\n {formatTokenBalance(amountSpend, {\n symbol: displayTokenSymbol,\n decimals: intent?.token?.decimals,\n })}\n

\n\n \n

View Sources

\n \n
\n \n )\n )}\n
\n {!isLoading && (\n \n {showCoverageFeedback && (\n \n
\n
\n \n \n \n \n \n {Math.round(normalizedCoverage)}%\n \n
\n\n
\n

\n Available on selected chains:{\" \"}\n \n {selectedTotal} {displayTokenSymbol}\n \n

\n

\n Required for this transaction:{\" \"}\n \n {requiredSafetyTotal} {displayTokenSymbol}\n \n

\n {shouldShowProceedMessage && (\n

\n Need{\" \"}\n \n {missingToProceed} {displayTokenSymbol}\n {\" \"}\n more on selected chains to continue.\n

\n )}\n {!isSourceSelectionInsufficient &&\n sourceCoverageState === \"healthy\" && (\n

\n You're all set. We'll only use what's\n needed from these selected chains.\n

\n )}\n
\n
\n \n )}\n\n {availableSources.length === 0 ? (\n

\n No source balances available for this token.\n

\n ) : (\n
\n \n {bulkActionLabel}\n \n {availableSources.map((source) => {\n const chainId = source.chain.id;\n const isSelected = selectedSourceChains.includes(chainId);\n const isLastSelected = isSelected\n ? selectedSourceChains.length === 1\n : false;\n\n const willUseFromIntent = intent?.sources?.find(\n (s) => s.chainID === chainId,\n )?.amount;\n\n return (\n {\n if (isLastSelected) return;\n onToggleSourceChain(chainId);\n }}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (isLastSelected) return;\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onToggleSourceChain(chainId);\n }\n }}\n >\n
\n {\n if (isLastSelected) return;\n onToggleSourceChain(chainId);\n }}\n onClick={(e) => e.stopPropagation()}\n aria-label={`Select ${source.chain.name} as a source`}\n />\n \n

\n {source.chain.name}\n

\n
\n\n
\n

\n {formatTokenBalance(source.balance, {\n symbol: source.symbol,\n decimals: source.decimals,\n })}\n

\n {willUseFromIntent && (\n

\n Estimated to use:{\" \"}\n {formatTokenBalance(willUseFromIntent, {\n symbol: source.symbol,\n decimals: intent?.token?.decimals,\n })}\n

\n )}\n
\n
\n );\n })}\n \n )}\n\n {availableSources.length > 0 && (\n
\n

Keep at least 1 chain selected.

\n
\n )}\n
\n )}\n
\n \n );\n};\n\nexport default SourceBreakdown;\n", + "content": "import { type BridgeIntent, type ChainBalance } from \"@avail-project/nexus-sdk-v2\";\nimport { formatTokenBalance } from \"@avail-project/nexus-sdk-v2/utils\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"../../ui/accordion\";\nimport { Skeleton } from \"../../ui/skeleton\";\nimport { useMemo } from \"react\";\nimport { Checkbox } from \"../../ui/checkbox\";\nimport { cn } from \"@/lib/utils\";\n\ntype SourceCoverageState = \"healthy\" | \"warning\" | \"error\";\n\ninterface SourceBreakdownProps {\n intent?: BridgeIntent;\n tokenSymbol?: string;\n isLoading?: boolean;\n requiredAmount?: string;\n // v2: ChainBalance replaces UserAssetDatum[\"breakdown\"] items\n availableSources: ChainBalance[];\n selectedSourceChains: number[];\n onToggleSourceChain: (chainId: number) => void;\n onSourceMenuOpenChange?: (open: boolean) => void;\n isSourceSelectionInsufficient?: boolean;\n sourceCoverageState?: SourceCoverageState;\n sourceCoveragePercent?: number;\n missingToProceed?: string;\n missingToSafety?: string;\n selectedTotal?: string;\n requiredTotal?: string;\n requiredSafetyTotal?: string;\n}\n\nconst SourceBreakdown = ({\n intent,\n tokenSymbol,\n isLoading = false,\n requiredAmount,\n availableSources,\n selectedSourceChains,\n onToggleSourceChain,\n onSourceMenuOpenChange,\n isSourceSelectionInsufficient = false,\n sourceCoverageState = \"healthy\",\n sourceCoveragePercent = 100,\n missingToProceed,\n selectedTotal,\n requiredTotal,\n requiredSafetyTotal,\n}: SourceBreakdownProps) => {\n const displayTokenSymbol = tokenSymbol ?? availableSources[0]?.chain?.name;\n const normalizedCoverage = Math.max(0, Math.min(100, sourceCoveragePercent));\n const progressRadius = 16;\n const progressCircumference = 2 * Math.PI * progressRadius;\n const progressOffset =\n progressCircumference - (normalizedCoverage / 100) * progressCircumference;\n const showCoverageFeedback = Boolean(\n selectedTotal && requiredTotal && requiredSafetyTotal,\n );\n const shouldShowProceedMessage =\n sourceCoverageState === \"error\" &&\n Number.parseFloat(missingToProceed ?? \"0\") > 0;\n\n const coverageToneClass =\n sourceCoverageState === \"error\"\n ? \"text-rose-500\"\n : sourceCoverageState === \"warning\"\n ? \"text-amber-500\"\n : \"text-emerald-500\";\n\n const coverageSurfaceClass =\n sourceCoverageState === \"error\"\n ? \"border-rose-500/30 bg-rose-500/10 text-rose-950 dark:text-rose-200\"\n : sourceCoverageState === \"warning\"\n ? \"border-amber-500/30 bg-amber-500/10 text-amber-950 dark:text-amber-200\"\n : \"border-emerald-500/30 bg-emerald-500/10 text-emerald-950 dark:text-emerald-200\";\n const selectedSourceSet = new Set(selectedSourceChains);\n const bulkActionLabel =\n selectedSourceChains.length > 1 ? \"Deselect all\" : \"Select all\";\n const isBulkActionDisabled = availableSources.length <= 1;\n const handleBulkSourceAction = () => {\n if (isBulkActionDisabled) return;\n\n if (bulkActionLabel === \"Select all\") {\n availableSources.forEach((source) => {\n const chainId = source.chain.id;\n if (!selectedSourceSet.has(chainId)) {\n onToggleSourceChain(chainId);\n }\n });\n return;\n }\n\n const chainToKeep =\n availableSources.find((source) => selectedSourceSet.has(source.chain.id))\n ?.chain.id ?? selectedSourceChains[0];\n\n if (typeof chainToKeep !== \"number\") return;\n\n selectedSourceChains.forEach((chainId) => {\n if (chainId !== chainToKeep) {\n onToggleSourceChain(chainId);\n }\n });\n };\n\n const spendOnSources = useMemo(() => {\n if (!intent || (intent?.availableSources?.length ?? 0) < 2)\n return `1 asset on 1 chain`;\n return `${intent?.availableSources?.length} Assets on ${intent?.availableSources?.length} chains`;\n }, [intent]);\n\n const amountSpend = useMemo(() => {\n const base = Number(requiredAmount ?? \"0\");\n if (!intent) return base;\n const fees = Number.parseFloat(intent?.fees?.total ?? \"0\");\n return base + fees;\n }, [requiredAmount, intent]);\n\n return (\n onSourceMenuOpenChange?.(value === \"sources\")}\n >\n \n
\n {isLoading ? (\n <>\n
\n

You Spend

\n \n
\n
\n \n
\n \n
\n
\n \n ) : (\n intent && (\n <>\n
\n

You Spend

\n

{spendOnSources}

\n
\n\n
\n

\n {formatTokenBalance(amountSpend, {\n symbol: displayTokenSymbol,\n // v2: BridgeIntent.availableSources replaces allSources\n decimals: intent?.availableSources?.[0]?.token?.decimals,\n })}\n

\n\n \n

View Sources

\n \n
\n \n )\n )}\n
\n {!isLoading && (\n \n {showCoverageFeedback && (\n \n
\n
\n \n \n \n \n \n {Math.round(normalizedCoverage)}%\n \n
\n\n
\n

\n Available on selected chains:{\" \"}\n \n {selectedTotal} {displayTokenSymbol}\n \n

\n

\n Required for this transaction:{\" \"}\n \n {requiredSafetyTotal} {displayTokenSymbol}\n \n

\n {shouldShowProceedMessage && (\n

\n Need{\" \"}\n \n {missingToProceed} {displayTokenSymbol}\n {\" \"}\n more on selected chains to continue.\n

\n )}\n {!isSourceSelectionInsufficient &&\n sourceCoverageState === \"healthy\" && (\n

\n You're all set. We'll only use what's\n needed from these selected chains.\n

\n )}\n
\n
\n \n )}\n\n {availableSources.length === 0 ? (\n

\n No source balances available for this token.\n

\n ) : (\n
\n \n {bulkActionLabel}\n \n {availableSources.map((source) => {\n const chainId = source.chain.id;\n const isSelected = selectedSourceChains.includes(chainId);\n const isLastSelected = isSelected\n ? selectedSourceChains.length === 1\n : false;\n\n const willUseFromIntent = intent?.availableSources?.find(\n (s: any) => s.chain.id === chainId,\n )?.amount;\n\n return (\n {\n if (isLastSelected) return;\n onToggleSourceChain(chainId);\n }}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (isLastSelected) return;\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onToggleSourceChain(chainId);\n }\n }}\n >\n
\n {\n if (isLastSelected) return;\n onToggleSourceChain(chainId);\n }}\n onClick={(e) => e.stopPropagation()}\n aria-label={`Select ${source.chain.name} as a source`}\n />\n \n

\n {source.chain.name}\n

\n
\n\n
\n

\n {formatTokenBalance(source.balance, {\n symbol: tokenSymbol ?? source.chain?.name,\n decimals: source.decimals,\n })}\n

\n {willUseFromIntent && (\n

\n Estimated to use:{\" \"}\n {formatTokenBalance(willUseFromIntent, {\n symbol: tokenSymbol ?? source.chain?.name,\n // v2: BridgeIntent.availableSources replaces allSources\n decimals:\n intent?.availableSources?.[0]?.token?.decimals,\n })}\n

\n )}\n
\n
\n );\n })}\n \n )}\n\n {availableSources.length > 0 && (\n
\n

Keep at least 1 chain selected.

\n
\n )}\n
\n )}\n
\n \n );\n};\n\nexport default SourceBreakdown;\n", "type": "registry:component", "target": "components/transfer/components/source-breakdown.tsx" }, { "path": "registry/nexus-elements/transfer/components/token-select.tsx", - "content": "import {\n type SUPPORTED_CHAINS_IDS,\n type SUPPORTED_TOKENS,\n} from \"@avail-project/nexus-core\";\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../../ui/select\";\nimport { Label } from \"../../ui/label\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { useMemo } from \"react\";\n\ninterface TokenSelectProps {\n selectedToken?: SUPPORTED_TOKENS;\n selectedChain: SUPPORTED_CHAINS_IDS;\n handleTokenSelect: (token: SUPPORTED_TOKENS) => void;\n isTestnet?: boolean;\n disabled?: boolean;\n label?: string;\n}\n\nconst TokenSelect = ({\n selectedToken,\n selectedChain,\n handleTokenSelect,\n isTestnet = false,\n disabled = false,\n label,\n}: TokenSelectProps) => {\n const { supportedChainsAndTokens } = useNexus();\n const tokenData = useMemo(() => {\n return supportedChainsAndTokens\n ?.filter((chain) => chain.id === selectedChain)\n .flatMap((chain) => chain.tokens);\n }, [selectedChain, supportedChainsAndTokens]);\n\n const selectedTokenData = tokenData?.find((token) => {\n return token.symbol === selectedToken;\n });\n\n return (\n \n !disabled && handleTokenSelect(value as SUPPORTED_TOKENS)\n }\n >\n
\n {label && }\n \n \n {selectedChain && selectedTokenData && (\n
\n \n {selectedToken}\n
\n )}\n
\n \n
\n\n \n \n {tokenData?.map((token) => (\n \n
\n \n
\n \n {isTestnet ? `${token.symbol} (Testnet)` : token.symbol}\n \n
\n
\n
\n ))}\n
\n
\n \n );\n};\n\nexport default TokenSelect;\n", + "content": "// v2: SUPPORTED_CHAINS_IDS, SUPPORTED_TOKENS removed — use plain string/number\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../../ui/select\";\nimport { Label } from \"../../ui/label\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { useMemo } from \"react\";\n\ninterface TokenSelectProps {\n selectedToken?: string;\n selectedChain: number;\n handleTokenSelect: (token: string) => void;\n isTestnet?: boolean;\n disabled?: boolean;\n label?: string;\n}\n\nconst TokenSelect = ({\n selectedToken,\n selectedChain,\n handleTokenSelect,\n isTestnet = false,\n disabled = false,\n label,\n}: TokenSelectProps) => {\n const { supportedChainsAndTokens } = useNexus();\n const tokenData = useMemo(() => {\n return supportedChainsAndTokens\n ?.filter((chain) => chain.id === selectedChain)\n .flatMap((chain) => chain.tokens);\n }, [selectedChain, supportedChainsAndTokens]);\n\n const selectedTokenData = tokenData?.find((token) => {\n return token.symbol === selectedToken;\n });\n\n return (\n \n !disabled && handleTokenSelect(value)\n }\n >\n
\n {label && }\n \n \n {selectedChain && selectedTokenData && (\n
\n \n {selectedToken}\n
\n )}\n
\n \n
\n\n \n \n {tokenData?.map((token) => (\n \n
\n \n
\n \n {isTestnet ? `${token.symbol} (Testnet)` : token.symbol}\n \n
\n
\n
\n ))}\n
\n
\n \n );\n};\n\nexport default TokenSelect;\n", "type": "registry:component", "target": "components/transfer/components/token-select.tsx" }, { "path": "registry/nexus-elements/transfer/components/transaction-progress.tsx", - "content": "import { Check, Circle, LoaderPinwheel, SquareArrowOutUpRight } from \"lucide-react\";\nimport { type FC, memo, useMemo } from \"react\";\nimport {\n type BridgeStepType,\n type SwapStepType,\n} from \"@avail-project/nexus-core\";\nimport { Button } from \"../../ui/button\";\n\ntype ProgressStep = BridgeStepType | SwapStepType;\n\ninterface TransactionProgressProps {\n timer: number;\n steps: Array<{ id: number; completed: boolean; step: ProgressStep }>;\n viewIntentUrl?: string;\n operationType?: string;\n completed?: boolean;\n}\n\nexport const getOperationText = (type: string) => {\n switch (type) {\n case \"transfer\":\n return \"Transferring\";\n case \"swap\":\n return \"Swapping\";\n default:\n return \"Processing\";\n }\n};\n\ntype DisplayStep = { id: string; label: string; completed: boolean };\n\nconst StepList: FC<{ steps: DisplayStep[]; currentIndex: number }> = memo(\n ({ steps, currentIndex }) => {\n return (\n
\n {steps.map((s, idx) => {\n const isCompleted = !!s.completed;\n const isCurrent = currentIndex === -1 ? false : idx === currentIndex;\n\n let rightIcon = ;\n if (isCompleted) {\n rightIcon = ;\n } else if (isCurrent) {\n rightIcon = (\n \n );\n }\n\n return (\n \n
\n {s.label}\n
\n {rightIcon}\n
\n );\n })}\n \n );\n }\n);\nStepList.displayName = \"StepList\";\n\nconst TransactionProgress: FC = ({\n timer,\n steps,\n viewIntentUrl,\n operationType = \"transfer\",\n completed = false,\n}) => {\n const totalSteps = Array.isArray(steps) ? steps.length : 0;\n const completedSteps = Array.isArray(steps)\n ? steps.reduce((acc, s) => acc + (s?.completed ? 1 : 0), 0)\n : 0;\n const rawPercent = totalSteps > 0 ? completedSteps / totalSteps : 0;\n const percent = completed ? 1 : rawPercent;\n const allCompleted = completed || percent >= 1;\n const opText = getOperationText(operationType);\n const headerText = allCompleted\n ? `${opText} Completed`\n : `${opText} In Progress...`;\n const ctaText = allCompleted ? `View Explorer` : \"View Intent\";\n\n const { effectiveSteps, currentIndex } = useMemo(() => {\n const milestones = [\n \"Intent verified\",\n \"Collected on sources\",\n \"Filled on destination\",\n ];\n const thresholds = milestones.map(\n (_, idx) => (idx + 1) / milestones.length\n );\n const displaySteps: DisplayStep[] = milestones.map((label, idx) => ({\n id: `M${idx}`,\n label,\n completed: idx === 0 ? timer > 0 : percent >= thresholds[idx],\n }));\n const current = displaySteps.findIndex((st) => !st.completed);\n return { effectiveSteps: displaySteps, currentIndex: current };\n }, [percent, timer, completed]);\n\n return (\n
\n
\n {allCompleted ? (\n \n ) : (\n \n )}\n

{headerText}

\n
\n \n {Math.floor(timer)}\n \n \n .\n \n \n {String(Math.floor((timer % 1) * 1000)).padStart(3, \"0\")}s\n \n
\n
\n\n \n\n {viewIntentUrl && (\n \n )}\n
\n );\n};\n\nexport default TransactionProgress;\n", + "content": "import { Check, Circle, LoaderPinwheel, SquareArrowOutUpRight } from \"lucide-react\";\nimport { type FC, memo, useMemo } from \"react\";\nimport { Button } from \"../../ui/button\";\n\ntype ProgressStep = { type?: string; typeID?: string; [key: string]: unknown };\n\ninterface TransactionProgressProps {\n timer: number;\n steps: Array<{ id: number; completed: boolean; step: ProgressStep }>;\n viewIntentUrl?: string;\n operationType?: string;\n completed?: boolean;\n}\n\nexport const getOperationText = (type: string) => {\n switch (type) {\n case \"transfer\":\n return \"Transferring\";\n case \"swap\":\n return \"Swapping\";\n default:\n return \"Processing\";\n }\n};\n\ntype DisplayStep = { id: string; label: string; completed: boolean };\n\nconst StepList: FC<{ steps: DisplayStep[]; currentIndex: number }> = memo(\n ({ steps, currentIndex }) => {\n return (\n
\n {steps.map((s, idx) => {\n const isCompleted = !!s.completed;\n const isCurrent = currentIndex === -1 ? false : idx === currentIndex;\n\n let rightIcon = ;\n if (isCompleted) {\n rightIcon = ;\n } else if (isCurrent) {\n rightIcon = (\n \n );\n }\n\n return (\n \n
\n {s.label}\n
\n {rightIcon}\n
\n );\n })}\n \n );\n }\n);\nStepList.displayName = \"StepList\";\n\nconst TransactionProgress: FC = ({\n timer,\n steps,\n viewIntentUrl,\n operationType = \"transfer\",\n completed = false,\n}) => {\n const totalSteps = Array.isArray(steps) ? steps.length : 0;\n const completedSteps = Array.isArray(steps)\n ? steps.reduce((acc, s) => acc + (s?.completed ? 1 : 0), 0)\n : 0;\n const rawPercent = totalSteps > 0 ? completedSteps / totalSteps : 0;\n const percent = completed ? 1 : rawPercent;\n const allCompleted = completed || percent >= 1;\n const opText = getOperationText(operationType);\n const headerText = allCompleted\n ? `${opText} Completed`\n : `${opText} In Progress...`;\n const ctaText = allCompleted ? `View Explorer` : \"View Intent\";\n\n const { effectiveSteps, currentIndex } = useMemo(() => {\n const milestones = [\n \"Intent verified\",\n \"Collected on sources\",\n \"Filled on destination\",\n ];\n const thresholds = milestones.map(\n (_, idx) => (idx + 1) / milestones.length\n );\n const displaySteps: DisplayStep[] = milestones.map((label, idx) => ({\n id: `M${idx}`,\n label,\n completed: idx === 0 ? timer > 0 : percent >= thresholds[idx],\n }));\n const current = displaySteps.findIndex((st) => !st.completed);\n return { effectiveSteps: displaySteps, currentIndex: current };\n }, [percent, timer, completed]);\n\n return (\n
\n
\n {allCompleted ? (\n \n ) : (\n \n )}\n

{headerText}

\n
\n \n {Math.floor(timer)}\n \n \n .\n \n \n {String(Math.floor((timer % 1) * 1000)).padStart(3, \"0\")}s\n \n
\n
\n\n \n\n {viewIntentUrl && (\n \n )}\n
\n );\n};\n\nexport default TransactionProgress;\n", "type": "registry:component", "target": "components/transfer/components/transaction-progress.tsx" }, { "path": "registry/nexus-elements/transfer/hooks/useTransfer.ts", - "content": "import {\n type NexusNetwork,\n NexusSDK,\n type OnAllowanceHookData,\n type OnIntentHookData,\n type SUPPORTED_CHAINS_IDS,\n type SUPPORTED_TOKENS,\n type UserAsset,\n} from \"@avail-project/nexus-core\";\nimport { useCallback, type RefObject } from \"react\";\nimport { type Address } from \"viem\";\nimport {\n type TransactionFlowExecuteParams,\n type TransactionFlowInputs,\n type TransactionFlowPrefill,\n useTransactionFlow,\n} from \"../../common\";\nimport { notifyIntentHistoryRefresh } from \"../../view-history/history-events\";\n\nexport type FastTransferState = TransactionFlowInputs;\n\ninterface UseTransferProps {\n network: NexusNetwork;\n nexusSDK: NexusSDK | null;\n intent: RefObject;\n allowance: RefObject;\n bridgableBalance: UserAsset[] | null;\n prefill?: {\n token: SUPPORTED_TOKENS;\n chainId: SUPPORTED_CHAINS_IDS;\n amount?: string;\n recipient?: Address;\n };\n onComplete?: (explorerUrl?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n maxAmount?: string | number;\n isSourceMenuOpen?: boolean;\n}\n\nconst useTransfer = ({\n network,\n nexusSDK,\n intent,\n bridgableBalance,\n prefill,\n onComplete,\n onStart,\n onError,\n allowance,\n fetchBalance,\n maxAmount,\n isSourceMenuOpen = false,\n}: UseTransferProps) => {\n const executeTransaction = useCallback(\n async ({\n token,\n amount,\n toChainId,\n recipient,\n sourceChains,\n onEvent,\n }: TransactionFlowExecuteParams) => {\n if (!nexusSDK) return null;\n return nexusSDK.bridgeAndTransfer(\n {\n token,\n amount,\n toChainId,\n recipient,\n sourceChains,\n },\n { onEvent },\n );\n },\n [nexusSDK],\n );\n\n const flow = useTransactionFlow({\n type: \"transfer\",\n network,\n nexusSDK,\n intent,\n bridgableBalance,\n prefill: prefill as TransactionFlowPrefill | undefined,\n onComplete,\n onStart,\n onError,\n allowance,\n fetchBalance,\n maxAmount,\n isSourceMenuOpen,\n notifyHistoryRefresh: notifyIntentHistoryRefresh,\n executeTransaction,\n });\n\n return {\n ...flow,\n inputs: flow.inputs as FastTransferState,\n setInputs: flow.setInputs as (\n next: FastTransferState | Partial,\n ) => void,\n };\n};\n\nexport default useTransfer;\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type {\n NexusNetwork,\n OnAllowanceHookData,\n OnIntentHookData,\n TokenBalance,\n} from \"@avail-project/nexus-sdk-v2\";\nimport { useCallback, type RefObject } from \"react\";\nimport { type Address } from \"viem\";\nimport {\n type TransactionFlowExecuteParams,\n type TransactionFlowInputs,\n type TransactionFlowPrefill,\n useTransactionFlow,\n} from \"../../common\";\nimport { notifyIntentHistoryRefresh } from \"../../view-history/history-events\";\n\ntype NexusClient = ReturnType;\n\nexport type FastTransferState = TransactionFlowInputs;\n\ninterface UseTransferProps {\n network: NexusNetwork;\n nexusSDK: NexusClient | null;\n intent: RefObject;\n allowance: RefObject;\n bridgableBalance: TokenBalance[] | null;\n prefill?: {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n };\n onComplete?: (explorerUrl?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n maxAmount?: string | number;\n isSourceMenuOpen?: boolean;\n}\n\nconst useTransfer = ({\n network,\n nexusSDK,\n intent,\n bridgableBalance,\n prefill,\n onComplete,\n onStart,\n onError,\n allowance,\n fetchBalance,\n maxAmount,\n isSourceMenuOpen = false,\n}: UseTransferProps) => {\n const executeTransaction = useCallback(\n async ({\n token,\n amount,\n toChainId,\n recipient,\n sources,\n onEvent,\n }: TransactionFlowExecuteParams) => {\n if (!nexusSDK) return null;\n // v2 params: toTokenSymbol, toAmountRaw, sources (not token/amount/sourceChains)\n return nexusSDK.bridgeAndTransfer(\n {\n toTokenSymbol: token,\n toAmountRaw: amount,\n toChainId,\n recipient,\n sources,\n },\n {\n onEvent,\n hooks: {\n onIntent: (data) => {\n (intent as RefObject).current = data;\n },\n onAllowance: (data) => {\n (allowance as RefObject).current = data;\n },\n },\n },\n );\n },\n [nexusSDK, intent, allowance],\n );\n\n const flow = useTransactionFlow({\n type: \"transfer\",\n network,\n nexusSDK,\n intent,\n bridgableBalance,\n prefill: prefill as TransactionFlowPrefill | undefined,\n onComplete,\n onStart,\n onError,\n allowance,\n fetchBalance,\n maxAmount,\n isSourceMenuOpen,\n notifyHistoryRefresh: notifyIntentHistoryRefresh,\n executeTransaction,\n });\n\n return {\n ...flow,\n inputs: flow.inputs as FastTransferState,\n setInputs: flow.setInputs as (\n next: FastTransferState | Partial,\n ) => void,\n };\n};\n\nexport default useTransfer;\n", "type": "registry:component", "target": "components/transfer/hooks/useTransfer.ts" }, { "path": "registry/nexus-elements/transfer/transfer.tsx", - "content": "\"use client\";\nimport { type FC, useEffect, useState } from \"react\";\nimport { Card, CardContent } from \"../ui/card\";\nimport ChainSelect from \"./components/chain-select\";\nimport TokenSelect from \"./components/token-select\";\nimport { Button } from \"../ui/button\";\nimport { LoaderPinwheel, X } from \"lucide-react\";\nimport { useNexus } from \"../nexus/NexusProvider\";\nimport AmountInput from \"./components/amount-input\";\nimport FeeBreakdown from \"./components/fee-breakdown\";\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from \"../ui/dialog\";\nimport TransactionProgress from \"./components/transaction-progress\";\nimport SourceBreakdown from \"./components/source-breakdown\";\nimport {\n type SUPPORTED_CHAINS_IDS,\n type SUPPORTED_TOKENS,\n} from \"@avail-project/nexus-core\";\nimport { type Address } from \"viem\";\nimport { Skeleton } from \"../ui/skeleton\";\nimport RecipientAddress from \"./components/recipient-address\";\nimport useTransfer from \"./hooks/useTransfer\";\nimport AllowanceModal from \"./components/allowance-modal\";\nimport ViewHistory from \"../view-history/view-history\";\n\ninterface FastTransferProps {\n maxAmount?: string | number;\n prefill?: {\n token: SUPPORTED_TOKENS;\n chainId: SUPPORTED_CHAINS_IDS;\n amount?: string;\n recipient?: Address;\n };\n onComplete?: (explorerUrl?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n}\n\nconst FastTransfer: FC = ({\n maxAmount,\n onComplete,\n onStart,\n onError,\n prefill,\n}) => {\n const [isSourceMenuOpen, setIsSourceMenuOpen] = useState(false);\n const {\n nexusSDK,\n intent,\n bridgableBalance,\n fetchBridgableBalance,\n allowance,\n network,\n } = useNexus();\n\n const {\n inputs,\n setInputs,\n timer,\n loading,\n refreshing,\n isDialogOpen,\n txError,\n setTxError,\n handleTransaction,\n reset,\n filteredBridgableBalance,\n startTransaction,\n setIsDialogOpen,\n commitAmount,\n lastExplorerUrl,\n steps,\n status,\n availableSources,\n selectedSourceChains,\n toggleSourceChain,\n isSourceSelectionInsufficient,\n isSourceSelectionReadyForAccept,\n sourceCoverageState,\n sourceCoveragePercent,\n missingToProceed,\n missingToSafety,\n selectedTotal,\n requiredTotal,\n requiredSafetyTotal,\n maxAvailableAmount,\n isInputsValid,\n } = useTransfer({\n prefill,\n network: network ?? \"mainnet\",\n nexusSDK,\n intent,\n bridgableBalance,\n onComplete,\n onStart,\n onError,\n allowance,\n fetchBalance: fetchBridgableBalance,\n maxAmount,\n isSourceMenuOpen,\n });\n\n useEffect(() => {\n if (!intent.current?.intent) {\n setIsSourceMenuOpen(false);\n }\n }, [intent.current?.intent]);\n\n return (\n \n \n \n \n setInputs({\n ...inputs,\n chain,\n })\n }\n label=\"To\"\n disabled={!!prefill?.chainId}\n />\n setInputs({ ...inputs, token })}\n disabled={!!prefill?.token}\n />\n setInputs({ ...inputs, amount })}\n bridgableBalance={filteredBridgableBalance}\n onCommit={() => void commitAmount()}\n disabled={refreshing || !!prefill?.amount}\n inputs={inputs}\n maxAmount={maxAmount}\n maxAvailableAmount={maxAvailableAmount}\n />\n \n setInputs({ ...inputs, recipient: address as `0x${string}` })\n }\n disabled={!!prefill?.recipient}\n />\n {intent?.current?.intent && (\n <>\n \n
\n

Receipient Receives

\n
\n {refreshing ? (\n \n ) : (\n

\n {`${inputs?.amount} ${\n inputs?.token === \"USDM\"\n ? \"USDM\"\n : filteredBridgableBalance?.symbol\n }`}\n

\n )}\n {refreshing ? (\n \n ) : (\n

\n on {intent?.current?.intent?.destination?.chainName}\n

\n )}\n
\n
\n \n \n )}\n\n {!intent.current && (\n \n {loading ? (\n \n ) : (\n \"Transfer to recipient\"\n )}\n \n )}\n\n {\n if (loading) return;\n setIsDialogOpen(open);\n }}\n >\n {intent.current && !isDialogOpen && (\n
\n \n \n \n {refreshing ? \"Refreshing...\" : \"Accept\"}\n \n \n
\n )}\n\n \n \n Transaction Progress\n \n {allowance.current ? (\n \n ) : (\n \n )}\n \n \n\n {txError && (\n
\n {txError}\n {\n reset();\n setTxError(null);\n }}\n className=\"text-destructive-foreground/80 hover:text-destructive-foreground focus:outline-none\"\n aria-label=\"Dismiss error\"\n >\n \n \n
\n )}\n
\n
\n );\n};\n\nexport default FastTransfer;\n", + "content": "\"use client\";\nimport { type FC, useEffect, useState } from \"react\";\nimport { Card, CardContent } from \"../ui/card\";\nimport ChainSelect from \"./components/chain-select\";\nimport TokenSelect from \"./components/token-select\";\nimport { Button } from \"../ui/button\";\nimport { LoaderPinwheel, X } from \"lucide-react\";\nimport { useNexus } from \"../nexus/NexusProvider\";\nimport AmountInput from \"./components/amount-input\";\nimport FeeBreakdown from \"./components/fee-breakdown\";\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from \"../ui/dialog\";\nimport TransactionProgress from \"./components/transaction-progress\";\nimport SourceBreakdown from \"./components/source-breakdown\";\nimport { type Address } from \"viem\";\nimport { Skeleton } from \"../ui/skeleton\";\nimport RecipientAddress from \"./components/recipient-address\";\nimport useTransfer from \"./hooks/useTransfer\";\nimport AllowanceModal from \"./components/allowance-modal\";\nimport ViewHistory from \"../view-history/view-history\";\n\ninterface FastTransferProps {\n maxAmount?: string | number;\n prefill?: {\n token: string; // v2: was SUPPORTED_TOKENS\n chainId: number; // v2: was SUPPORTED_CHAINS_IDS\n amount?: string;\n recipient?: Address;\n };\n onComplete?: (explorerUrl?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n}\n\nconst FastTransfer: FC = ({\n maxAmount,\n onComplete,\n onStart,\n onError,\n prefill,\n}) => {\n const [isSourceMenuOpen, setIsSourceMenuOpen] = useState(false);\n const {\n nexusSDK,\n intent,\n bridgableBalance,\n fetchBridgableBalance,\n allowance,\n network,\n } = useNexus();\n\n const {\n inputs,\n setInputs,\n timer,\n loading,\n refreshing,\n isDialogOpen,\n txError,\n setTxError,\n handleTransaction,\n reset,\n filteredBridgableBalance,\n startTransaction,\n setIsDialogOpen,\n commitAmount,\n lastExplorerUrl,\n steps,\n status,\n availableSources,\n selectedSourceChains,\n toggleSourceChain,\n isSourceSelectionInsufficient,\n isSourceSelectionReadyForAccept,\n sourceCoverageState,\n sourceCoveragePercent,\n missingToProceed,\n missingToSafety,\n selectedTotal,\n requiredTotal,\n requiredSafetyTotal,\n maxAvailableAmount,\n isInputsValid,\n } = useTransfer({\n prefill,\n network: network ?? \"mainnet\",\n nexusSDK,\n intent,\n bridgableBalance,\n onComplete,\n onStart,\n onError,\n allowance,\n fetchBalance: fetchBridgableBalance,\n maxAmount,\n isSourceMenuOpen,\n });\n\n useEffect(() => {\n if (!intent.current?.intent) {\n setIsSourceMenuOpen(false);\n }\n }, [intent.current?.intent]);\n\n return (\n \n \n \n \n setInputs({\n ...inputs,\n chain,\n })\n }\n label=\"To\"\n disabled={!!prefill?.chainId}\n />\n setInputs({ ...inputs, token })}\n disabled={!!prefill?.token}\n />\n setInputs({ ...inputs, amount })}\n bridgableBalance={filteredBridgableBalance}\n onCommit={() => void commitAmount()}\n disabled={refreshing || !!prefill?.amount}\n inputs={inputs}\n maxAmount={maxAmount}\n maxAvailableAmount={maxAvailableAmount}\n />\n \n setInputs({ ...inputs, recipient: address as `0x${string}` })\n }\n disabled={!!prefill?.recipient}\n />\n {intent?.current?.intent && (\n <>\n \n
\n

Receipient Receives

\n
\n {refreshing ? (\n \n ) : (\n

\n {`${inputs?.amount} ${\n inputs?.token === \"USDM\"\n ? \"USDM\"\n : filteredBridgableBalance?.symbol\n }`}\n

\n )}\n {refreshing ? (\n \n ) : (\n

\n on{\" \"}\n {\n (\n intent?.current?.intent?.destination as {\n chain?: { name?: string };\n }\n )?.chain?.name\n }\n

\n )}\n
\n
\n \n \n )}\n\n {!intent.current && (\n \n {loading ? (\n \n ) : (\n \"Transfer to recipient\"\n )}\n \n )}\n\n {\n if (loading) return;\n setIsDialogOpen(open);\n }}\n >\n {intent.current && !isDialogOpen && (\n
\n \n \n \n {refreshing ? \"Refreshing...\" : \"Accept\"}\n \n \n
\n )}\n\n \n \n Transaction Progress\n \n {allowance.current ? (\n \n ) : (\n \n )}\n \n \n\n {txError && (\n
\n {txError}\n {\n reset();\n setTxError(null);\n }}\n className=\"text-destructive-foreground/80 hover:text-destructive-foreground focus:outline-none\"\n aria-label=\"Dismiss error\"\n >\n \n \n
\n )}\n
\n
\n );\n};\n\nexport default FastTransfer;\n", "type": "registry:component", "target": "components/transfer/transfer.tsx" }, @@ -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-sdk-v2\";\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.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.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.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 // v2: RFF_FEE_EXPIRED was removed; use string key for forward compat\n [\"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 );\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 as unknown as { data?: { context?: unknown } })?.data?.context,\n details: (err as unknown as { data?: { details?: unknown } })?.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" }, @@ -141,13 +141,13 @@ }, { "path": "registry/nexus-elements/common/hooks/useTransactionExecution.ts", - "content": "import {\n type BridgeStepType,\n NEXUS_EVENTS,\n type NexusSDK,\n type OnAllowanceHookData,\n type OnIntentHookData,\n} from \"@avail-project/nexus-core\";\nimport {\n type Dispatch,\n type RefObject,\n type SetStateAction,\n useCallback,\n useRef,\n} from \"react\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport {\n type SourceSelectionValidation,\n type TransactionFlowEvent,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n} from \"../types/transaction-flow\";\n\ninterface NexusErrorInfo {\n code: string;\n message: string;\n context?: unknown;\n details?: unknown;\n}\n\ntype NexusErrorHandler = (error: unknown) => NexusErrorInfo;\n\ninterface UseTransactionExecutionProps {\n operationName: \"bridge\" | \"transfer\";\n nexusSDK: NexusSDK | null;\n intent: RefObject;\n allowance: RefObject;\n inputs: TransactionFlowInputs;\n configuredMaxAmount?: string;\n allAvailableSourceChainIds: number[];\n sourceChainsForSdk?: number[];\n sourceSelectionKey: string;\n sourceSelection: SourceSelectionValidation;\n loading: boolean;\n txError: string | null;\n areInputsValid: boolean;\n executeTransaction: TransactionFlowExecutor;\n getMaxForCurrentSelection: () => Promise;\n onStepsList: (steps: BridgeStepType[]) => void;\n onStepComplete: (step: BridgeStepType) => void;\n resetSteps: () => void;\n setStatus: (status: TransactionStatus) => void;\n resetInputs: () => void;\n setRefreshing: Dispatch>;\n setIsDialogOpen: Dispatch>;\n setTxError: Dispatch>;\n setLastExplorerUrl: Dispatch>;\n setSelectedSourceChains: Dispatch>;\n setAppliedSourceSelectionKey: Dispatch>;\n stopwatch: {\n start: () => void;\n stop: () => void;\n reset: () => void;\n };\n handleNexusError: NexusErrorHandler;\n onStart?: () => void;\n onComplete?: (explorerUrl?: string) => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n notifyHistoryRefresh?: () => void;\n}\n\nexport function useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n}: UseTransactionExecutionProps) {\n const commitLockRef = useRef(false);\n const runIdRef = useRef(0);\n\n const refreshIntent = async (options?: { reportError?: boolean }) => {\n if (!intent.current) return false;\n const activeRunId = runIdRef.current;\n setRefreshing(true);\n try {\n const updated = await intent.current.refresh(sourceChainsForSdk);\n if (activeRunId !== runIdRef.current) return false;\n if (updated) {\n intent.current.intent = updated;\n }\n setAppliedSourceSelectionKey(sourceSelectionKey);\n return true;\n } catch (error) {\n if (activeRunId !== runIdRef.current) return false;\n console.error(\"Transaction failed:\", error);\n if (options?.reportError) {\n const message = \"Unable to refresh source selection. Please try again.\";\n setTxError(message);\n onError?.(message);\n }\n return false;\n } finally {\n if (activeRunId === runIdRef.current) {\n setRefreshing(false);\n }\n }\n };\n\n const onSuccess = async (explorerUrl?: string) => {\n stopwatch.stop();\n setStatus(\"success\");\n onComplete?.(explorerUrl);\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n await fetchBalance();\n notifyHistoryRefresh?.();\n };\n\n const handleTransaction = async () => {\n if (commitLockRef.current) return;\n commitLockRef.current = true;\n const currentRunId = ++runIdRef.current;\n let didEnterExecutingState = false;\n const cleanupSupersededExecution = () => {\n if (!didEnterExecutingState) return;\n setRefreshing(false);\n setIsDialogOpen(false);\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n setStatus(\"idle\");\n };\n\n try {\n if (\n !inputs?.amount ||\n !inputs?.recipient ||\n !inputs?.chain ||\n !inputs?.token\n ) {\n console.error(\"Missing required inputs\");\n return;\n }\n if (!nexusSDK) {\n const message = \"Nexus SDK not initialized\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n const parsedAmount = Number(inputs.amount);\n if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) {\n const message = \"Enter a valid amount greater than 0.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n const amountBigInt = nexusSDK.convertTokenReadableAmountToBigInt(\n inputs.amount,\n inputs.token,\n inputs.chain,\n );\n\n if (configuredMaxAmount) {\n const configuredMaxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n configuredMaxAmount,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > configuredMaxRaw) {\n const message = `Amount exceeds maximum limit of ${configuredMaxAmount} ${inputs.token}.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n }\n\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (currentRunId !== runIdRef.current) return;\n if (!maxForCurrentSelection) {\n const message = `Unable to determine max ${operationName} amount for selected sources. Please try again.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n const maxForSelectionRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxForCurrentSelection,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > maxForSelectionRaw) {\n const message = `Selected sources can provide up to ${maxForCurrentSelection} ${inputs.token}. Reduce amount or enable more sources.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n setStatus(\"executing\");\n didEnterExecutingState = true;\n setTxError(null);\n onStart?.();\n setLastExplorerUrl(\"\");\n setAppliedSourceSelectionKey(sourceSelectionKey);\n\n const onEvent = (event: TransactionFlowEvent) => {\n if (currentRunId !== runIdRef.current) return;\n if (event.name === NEXUS_EVENTS.STEPS_LIST) {\n const list = Array.isArray(event.args) ? event.args : [];\n onStepsList(list as BridgeStepType[]);\n }\n if (event.name === NEXUS_EVENTS.STEP_COMPLETE) {\n if (\n !Array.isArray(event.args) &&\n \"type\" in event.args &&\n event.args.type === \"INTENT_HASH_SIGNED\"\n ) {\n stopwatch.start();\n }\n if (!Array.isArray(event.args)) {\n onStepComplete(event.args as BridgeStepType);\n }\n }\n };\n\n const transactionResult = await executeTransaction({\n token: inputs.token,\n amount: amountBigInt,\n toChainId: inputs.chain,\n recipient: inputs.recipient,\n sourceChains: sourceChainsForSdk,\n onEvent,\n });\n\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution();\n return;\n }\n if (!transactionResult) {\n throw new Error(\"Transaction rejected by user\");\n }\n setLastExplorerUrl(transactionResult.explorerUrl);\n await onSuccess(transactionResult.explorerUrl);\n } catch (error) {\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution();\n return;\n }\n const { message, code, context, details } = handleNexusError(error);\n console.error(`Fast ${operationName} transaction failed:`, {\n code,\n message,\n context,\n details,\n });\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n setTxError(message);\n onError?.(message);\n setIsDialogOpen(false);\n setSelectedSourceChains(null);\n setRefreshing(false);\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n void fetchBalance();\n setStatus(\"error\");\n } finally {\n commitLockRef.current = false;\n }\n };\n\n const reset = () => {\n runIdRef.current += 1;\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setStatus(\"idle\");\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n };\n\n const startTransaction = () => {\n if (!intent.current) return;\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (sourceSelection.isBelowRequired && inputs?.token) {\n const message = `Selected sources are not enough. Add ${sourceSelection.missingToProceed} ${inputs.token} more to make this transaction.`;\n setTxError(message);\n onError?.(message);\n return;\n }\n void (async () => {\n const refreshed = await refreshIntent({ reportError: true });\n if (!refreshed || !intent.current) return;\n intent.current.allow();\n setIsDialogOpen(true);\n setTxError(null);\n })();\n };\n\n const commitAmount = async () => {\n if (intent.current || loading || txError || !areInputsValid) return;\n await handleTransaction();\n };\n\n const invalidatePendingExecution = useCallback(() => {\n runIdRef.current += 1;\n if (intent.current) {\n intent.current.deny();\n intent.current = null;\n }\n setRefreshing(false);\n setAppliedSourceSelectionKey(\"ALL\");\n }, [intent, setAppliedSourceSelectionKey, setRefreshing]);\n\n return {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n };\n}\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type { OnAllowanceHookData, OnIntentHookData } from \"@avail-project/nexus-sdk-v2\";\nimport {\n type Dispatch,\n type RefObject,\n type SetStateAction,\n useCallback,\n useRef,\n} from \"react\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport {\n type SourceSelectionValidation,\n type TransactionFlowEvent,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n} from \"../types/transaction-flow\";\n\ntype NexusClient = ReturnType;\n\ninterface NexusErrorInfo {\n code: string;\n message: string;\n context?: unknown;\n details?: unknown;\n}\n\ntype NexusErrorHandler = (error: unknown) => NexusErrorInfo;\n\n// v2 plan_progress step types for bridge\nconst BRIDGE_STEP_INTENT_SIGNED = \"request_signing\";\n\ninterface UseTransactionExecutionProps {\n operationName: \"bridge\" | \"transfer\";\n nexusSDK: NexusClient | null;\n intent: RefObject;\n allowance: RefObject;\n inputs: TransactionFlowInputs;\n configuredMaxAmount?: string;\n allAvailableSourceChainIds: number[];\n sourceChainsForSdk?: number[];\n sourceSelectionKey: string;\n sourceSelection: SourceSelectionValidation;\n loading: boolean;\n txError: string | null;\n areInputsValid: boolean;\n executeTransaction: TransactionFlowExecutor;\n getMaxForCurrentSelection: () => Promise;\n onStepsList: (steps: { typeID?: string; type?: string; [key: string]: unknown }[]) => void;\n onStepComplete: (step: { typeID?: string; type?: string; [key: string]: unknown }) => void;\n resetSteps: () => void;\n setStatus: (status: TransactionStatus) => void;\n resetInputs: () => void;\n setRefreshing: Dispatch>;\n setIsDialogOpen: Dispatch>;\n setTxError: Dispatch>;\n setLastExplorerUrl: Dispatch>;\n setSelectedSourceChains: Dispatch>;\n setAppliedSourceSelectionKey: Dispatch>;\n stopwatch: {\n start: () => void;\n stop: () => void;\n reset: () => void;\n };\n handleNexusError: NexusErrorHandler;\n onStart?: () => void;\n onComplete?: (explorerUrl?: string) => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n notifyHistoryRefresh?: () => void;\n}\n\nexport function useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n}: UseTransactionExecutionProps) {\n const commitLockRef = useRef(false);\n const runIdRef = useRef(0);\n\n const refreshIntent = async (options?: { reportError?: boolean }) => {\n if (!intent.current) return false;\n const activeRunId = runIdRef.current;\n setRefreshing(true);\n try {\n const updated = await intent.current.refresh(sourceChainsForSdk);\n if (activeRunId !== runIdRef.current) return false;\n if (updated) {\n intent.current.intent = updated;\n }\n setAppliedSourceSelectionKey(sourceSelectionKey);\n return true;\n } catch (error) {\n if (activeRunId !== runIdRef.current) return false;\n console.error(\"Transaction failed:\", error);\n if (options?.reportError) {\n const message = \"Unable to refresh source selection. Please try again.\";\n setTxError(message);\n onError?.(message);\n }\n return false;\n } finally {\n if (activeRunId === runIdRef.current) {\n setRefreshing(false);\n }\n }\n };\n\n const onSuccess = async (explorerUrl?: string) => {\n stopwatch.stop();\n setStatus(\"success\");\n onComplete?.(explorerUrl);\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n await fetchBalance();\n notifyHistoryRefresh?.();\n };\n\n const handleTransaction = async () => {\n if (commitLockRef.current) return;\n commitLockRef.current = true;\n const currentRunId = ++runIdRef.current;\n let didEnterExecutingState = false;\n // Declared here (outside try/catch) so both the event handler and the catch block\n // can read/write it — prevents the catch from clobbering event-driven completions\n let completedFromEvent = false;\n const cleanupSupersededExecution = () => {\n if (!didEnterExecutingState) return;\n // Don't tear down the dialog if an event already handled success/failure —\n // resetInputs() inside onSuccess triggers invalidatePendingExecution which\n // increments runIdRef, making this branch fire spuriously.\n if (completedFromEvent) return;\n setRefreshing(false);\n setIsDialogOpen(false);\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n setStatus(\"idle\");\n };\n\n\n try {\n if (\n !inputs?.amount ||\n !inputs?.recipient ||\n !inputs?.chain ||\n !inputs?.token\n ) {\n console.error(\"Missing required inputs\");\n return;\n }\n if (!nexusSDK) {\n const message = \"Nexus SDK not initialized\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n const parsedAmount = Number(inputs.amount);\n if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) {\n const message = \"Enter a valid amount greater than 0.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n // v2: convertTokenReadableAmountToBigInt(amount, tokenSymbol, chainId)\n const amountBigInt = nexusSDK.convertTokenReadableAmountToBigInt(\n inputs.amount,\n inputs.token,\n inputs.chain,\n );\n\n if (configuredMaxAmount) {\n const configuredMaxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n configuredMaxAmount,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > configuredMaxRaw) {\n const message = `Amount exceeds maximum limit of ${configuredMaxAmount} ${inputs.token}.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n }\n\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (currentRunId !== runIdRef.current) return;\n if (!maxForCurrentSelection) {\n const message = `Unable to determine max ${operationName} amount for selected sources. Please try again.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n const maxForSelectionRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxForCurrentSelection,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > maxForSelectionRaw) {\n const message = `Selected sources can provide up to ${maxForCurrentSelection} ${inputs.token}. Reduce amount or enable more sources.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n setStatus(\"executing\");\n didEnterExecutingState = true;\n setTxError(null);\n onStart?.();\n setLastExplorerUrl(\"\");\n setAppliedSourceSelectionKey(sourceSelectionKey);\n\n // Terminal step types — when state:\"completed\" fires on these, the operation is done\n const TERMINAL_STEP_TYPES = new Set([\n \"bridge_fill\", // bridge & transfer final fill\n \"destination_swap\", // swap final step\n ]);\n\n // v2 onEvent uses typed discriminated union: { type, ... }\n const onEvent = (event: TransactionFlowEvent) => {\n if (currentRunId !== runIdRef.current) return;\n\n if (event.type === \"plan_preview\") {\n // Seed UI with the step list from the plan\n type StepShape = { typeID?: string; type?: string; [key: string]: unknown };\n const steps = ((event as { type: string; plan: { steps: StepShape[] } }).plan?.steps ?? []) as StepShape[];\n onStepsList(steps);\n }\n\n if (event.type === \"plan_progress\") {\n const progressEvent = event as {\n type: string;\n stepType: string;\n state: string;\n step: { typeID?: string; type?: string; [key: string]: unknown };\n error?: string;\n };\n\n // Always mark step as complete/updated in UI\n onStepComplete(progressEvent.step);\n\n const isTerminal = TERMINAL_STEP_TYPES.has(progressEvent.stepType);\n\n if (progressEvent.state === \"failed\") {\n // Any step failure → abort\n if (!completedFromEvent) {\n completedFromEvent = true;\n const errorMessage = progressEvent.error ?? \"Transaction failed\";\n stopwatch.stop();\n setTxError(errorMessage);\n onError?.(errorMessage);\n setStatus(\"error\");\n }\n return;\n }\n\n if (isTerminal && progressEvent.state === \"completed\") {\n // Terminal step completed → success\n if (!completedFromEvent) {\n completedFromEvent = true;\n stopwatch.stop();\n // explorerUrl is on the event itself, not the step object\n const explorerUrl = (event as { explorerUrl?: string }).explorerUrl;\n if (explorerUrl) setLastExplorerUrl(explorerUrl);\n void onSuccess(explorerUrl);\n }\n }\n }\n\n if (event.type === \"status\") {\n const statusEvent = event as { type: string; status: string };\n if (statusEvent.status === \"completed\" && !completedFromEvent) {\n completedFromEvent = true;\n stopwatch.stop();\n void onSuccess(undefined);\n }\n }\n };\n\n const transactionResult = await executeTransaction({\n token: inputs.token,\n amount: amountBigInt,\n toChainId: inputs.chain,\n recipient: inputs.recipient,\n sources: sourceChainsForSdk,\n onEvent,\n });\n\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution(); // no-op when completedFromEvent=true\n if (!completedFromEvent) return; // only bail if not already completed\n // else fall through — still want to capture explorerUrl from the result\n }\n if (!transactionResult) {\n if (!completedFromEvent) {\n throw new Error(\"Transaction rejected by user\");\n }\n // Already handled via events\n return;\n }\n\n // SDK promise resolved — use result for explorerUrl if event-driven success didn't set it\n if (!completedFromEvent) {\n // Fallback: SDK resolved but we never got a terminal event (e.g. single-step flows)\n setLastExplorerUrl(transactionResult.explorerUrl ?? \"\");\n await onSuccess(transactionResult.explorerUrl);\n } else {\n // Event-driven success already ran — capture the explorerUrl from the resolved result\n if (transactionResult.explorerUrl) {\n setLastExplorerUrl(transactionResult.explorerUrl);\n }\n }\n\n } catch (error) {\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution();\n return;\n }\n // If event-driven success/failure already handled this transaction, ignore SDK-level errors\n // (the SDK may throw or return oddly after a successful fill event)\n if (completedFromEvent) return;\n const { message, code, context, details } = handleNexusError(error);\n console.error(`Fast ${operationName} transaction failed:`, {\n code,\n message,\n context,\n details,\n });\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n setTxError(message);\n onError?.(message);\n setIsDialogOpen(false);\n setSelectedSourceChains(null);\n setRefreshing(false);\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n void fetchBalance();\n setStatus(\"error\");\n } finally {\n commitLockRef.current = false;\n }\n };\n\n const reset = () => {\n runIdRef.current += 1;\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setStatus(\"idle\");\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n };\n\n const startTransaction = () => {\n if (!intent.current) return;\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (sourceSelection.isBelowRequired && inputs?.token) {\n const message = `Selected sources are not enough. Add ${sourceSelection.missingToProceed} ${inputs.token} more to make this transaction.`;\n setTxError(message);\n onError?.(message);\n return;\n }\n void (async () => {\n const refreshed = await refreshIntent({ reportError: true });\n if (!refreshed || !intent.current) return;\n intent.current.allow();\n setIsDialogOpen(true);\n // Start the stopwatch AFTER the dialog opens so the isDialogOpen effect\n // does not immediately reset it (the effect only resets when dialog is closed)\n stopwatch.reset();\n stopwatch.start();\n setTxError(null);\n })();\n };\n\n const commitAmount = async () => {\n if (intent.current || loading || txError || !areInputsValid) return;\n await handleTransaction();\n };\n\n const invalidatePendingExecution = useCallback(() => {\n runIdRef.current += 1;\n if (intent.current) {\n intent.current.deny();\n intent.current = null;\n }\n setRefreshing(false);\n setAppliedSourceSelectionKey(\"ALL\");\n }, [intent, setAppliedSourceSelectionKey, setRefreshing]);\n\n return {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n };\n}\n", "type": "registry:component", "target": "components/common/hooks/useTransactionExecution.ts" }, { "path": "registry/nexus-elements/common/hooks/useTransactionFlow.ts", - "content": "import {\n type BridgeStepType,\n type NexusNetwork,\n NexusSDK,\n type OnAllowanceHookData,\n type OnIntentHookData,\n parseUnits,\n type UserAsset,\n} from \"@avail-project/nexus-core\";\nimport {\n useEffect,\n useMemo,\n useCallback,\n useRef,\n useState,\n useReducer,\n type RefObject,\n} from \"react\";\nimport { type Address, isAddress } from \"viem\";\nimport { useNexusError } from \"./useNexusError\";\nimport { useTransactionExecution } from \"./useTransactionExecution\";\nimport { usePolling } from \"./usePolling\";\nimport { useStopwatch } from \"./useStopwatch\";\nimport { useDebouncedCallback } from \"./useDebouncedCallback\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport { useTransactionSteps } from \"../tx/useTransactionSteps\";\nimport {\n type SourceCoverageState,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n type TransactionFlowPrefill,\n type TransactionFlowType,\n} from \"../types/transaction-flow\";\nimport {\n MAX_AMOUNT_DEBOUNCE_MS,\n buildInitialInputs,\n clampAmountToMax,\n formatAmountForDisplay,\n getCoverageDecimals,\n normalizeMaxAmount,\n} from \"../utils/transaction-flow\";\n\ninterface BaseTransactionFlowProps {\n type: TransactionFlowType;\n network: NexusNetwork;\n nexusSDK: NexusSDK | null;\n intent: RefObject;\n allowance: RefObject;\n bridgableBalance: UserAsset[] | null;\n prefill?: TransactionFlowPrefill;\n onComplete?: (explorerUrl?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n maxAmount?: string | number;\n isSourceMenuOpen?: boolean;\n notifyHistoryRefresh?: () => void;\n executeTransaction: TransactionFlowExecutor;\n}\n\nexport interface UseTransactionFlowProps extends BaseTransactionFlowProps {\n connectedAddress?: Address;\n}\n\ntype State = {\n inputs: TransactionFlowInputs;\n status: TransactionStatus;\n};\n\ntype Action =\n | { type: \"setInputs\"; payload: Partial }\n | { type: \"resetInputs\" }\n | { type: \"setStatus\"; payload: TransactionStatus };\n\nexport function useTransactionFlow(props: UseTransactionFlowProps) {\n const {\n type,\n network,\n nexusSDK,\n intent,\n bridgableBalance,\n prefill,\n onComplete,\n onStart,\n onError,\n fetchBalance,\n allowance,\n maxAmount,\n isSourceMenuOpen = false,\n notifyHistoryRefresh,\n executeTransaction,\n } = props;\n\n const connectedAddress = props.connectedAddress;\n const operationName = type === \"bridge\" ? \"bridge\" : \"transfer\";\n const handleNexusError = useNexusError();\n const initialState: State = {\n inputs: buildInitialInputs({ type, network, connectedAddress, prefill }),\n status: \"idle\",\n };\n\n function reducer(state: State, action: Action): State {\n switch (action.type) {\n case \"setInputs\":\n return { ...state, inputs: { ...state.inputs, ...action.payload } };\n case \"resetInputs\":\n return {\n ...state,\n inputs: buildInitialInputs({\n type,\n network,\n connectedAddress,\n prefill,\n }),\n };\n case \"setStatus\":\n return { ...state, status: action.payload };\n default:\n return state;\n }\n }\n\n const [state, dispatch] = useReducer(reducer, initialState);\n const inputs = state.inputs;\n const setInputs = (\n next: TransactionFlowInputs | Partial,\n ) => {\n dispatch({\n type: \"setInputs\",\n payload: next as Partial,\n });\n };\n\n const loading = state.status === \"executing\";\n const [refreshing, setRefreshing] = useState(false);\n const [isDialogOpen, setIsDialogOpen] = useState(false);\n const [txError, setTxError] = useState(null);\n const [lastExplorerUrl, setLastExplorerUrl] = useState(\"\");\n const previousConnectedAddressRef = useRef
(\n connectedAddress,\n );\n const maxAmountRequestIdRef = useRef(0);\n const [selectedSourceChains, setSelectedSourceChains] = useState<\n number[] | null\n >(null);\n const [selectedSourcesMaxAmount, setSelectedSourcesMaxAmount] = useState<\n string | null\n >(null);\n const [appliedSourceSelectionKey, setAppliedSourceSelectionKey] =\n useState(\"ALL\");\n const {\n steps,\n onStepsList,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n const configuredMaxAmount = useMemo(\n () => normalizeMaxAmount(maxAmount),\n [maxAmount],\n );\n\n const areInputsValid = useMemo(() => {\n const hasToken = inputs?.token !== undefined && inputs?.token !== null;\n const hasChain = inputs?.chain !== undefined && inputs?.chain !== null;\n const hasAmount = Boolean(inputs?.amount) && Number(inputs?.amount) > 0;\n const hasValidRecipient =\n Boolean(inputs?.recipient) && isAddress(inputs.recipient as string);\n return hasToken && hasChain && hasAmount && hasValidRecipient;\n }, [inputs]);\n\n const filteredBridgableBalance = useMemo(() => {\n return bridgableBalance?.find((bal) =>\n inputs?.token === \"USDM\"\n ? bal?.symbol === \"USDC\"\n : bal?.symbol === inputs?.token,\n );\n }, [bridgableBalance, inputs?.token]);\n\n const availableSources = useMemo(() => {\n const breakdown = filteredBridgableBalance?.breakdown ?? [];\n const destinationChainId = inputs?.chain;\n const nonZero = breakdown.filter((source) => {\n if (Number.parseFloat(source.balance ?? \"0\") <= 0) return false;\n if (typeof destinationChainId === \"number\") {\n return source.chain.id !== destinationChainId;\n }\n return true;\n });\n const decimals = filteredBridgableBalance?.decimals;\n if (!nexusSDK || typeof decimals !== \"number\") {\n return nonZero.sort(\n (a, b) => Number.parseFloat(b.balance) - Number.parseFloat(a.balance),\n );\n }\n return nonZero.sort((a, b) => {\n try {\n const aRaw = parseUnits(a.balance ?? \"0\", decimals);\n const bRaw = parseUnits(b.balance ?? \"0\", decimals);\n if (aRaw === bRaw) return 0;\n return aRaw > bRaw ? -1 : 1;\n } catch {\n return Number.parseFloat(b.balance) - Number.parseFloat(a.balance);\n }\n });\n }, [\n inputs?.chain,\n filteredBridgableBalance?.breakdown,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n ]);\n\n const allAvailableSourceChainIds = useMemo(\n () => availableSources.map((source) => source.chain.id),\n [availableSources],\n );\n\n const effectiveSelectedSourceChains = useMemo(() => {\n if (selectedSourceChains && selectedSourceChains.length > 0) {\n const availableSet = new Set(allAvailableSourceChainIds);\n const filteredSelection = selectedSourceChains.filter((id) =>\n availableSet.has(id),\n );\n if (filteredSelection.length > 0) {\n return filteredSelection;\n }\n }\n return allAvailableSourceChainIds;\n }, [selectedSourceChains, allAvailableSourceChainIds]);\n\n const sourceChainsForSdk =\n effectiveSelectedSourceChains.length > 0\n ? effectiveSelectedSourceChains\n : undefined;\n\n const sourceSelectionKey = useMemo(() => {\n if (allAvailableSourceChainIds.length === 0) return \"NONE\";\n if (!selectedSourceChains || selectedSourceChains.length === 0) {\n return \"ALL\";\n }\n return [...effectiveSelectedSourceChains].sort((a, b) => a - b).join(\"|\");\n }, [\n allAvailableSourceChainIds.length,\n effectiveSelectedSourceChains,\n selectedSourceChains,\n ]);\n const hasPendingSourceSelectionChanges =\n sourceSelectionKey !== appliedSourceSelectionKey;\n const intentSourceSpendAmount = intent.current?.intent?.sourcesTotal;\n\n const getMaxForCurrentSelection = useCallback(async () => {\n if (!nexusSDK || !inputs?.token || !inputs?.chain) return undefined;\n const maxBalAvailable = await nexusSDK.calculateMaxForBridge({\n token: inputs.token,\n toChainId: inputs.chain,\n recipient: inputs.recipient,\n sourceChains: sourceChainsForSdk,\n });\n if (!maxBalAvailable?.amount) return \"0\";\n return clampAmountToMax({\n amount: maxBalAvailable.amount,\n maxAmount: configuredMaxAmount,\n nexusSDK,\n token: inputs.token,\n chainId: inputs.chain,\n });\n }, [\n configuredMaxAmount,\n inputs?.chain,\n inputs?.recipient,\n inputs?.token,\n nexusSDK,\n sourceChainsForSdk,\n ]);\n\n const toggleSourceChain = useCallback(\n (chainId: number) => {\n setSelectedSourceChains((prev) => {\n if (allAvailableSourceChainIds.length === 0) return prev;\n const current =\n prev && prev.length > 0 ? prev : allAvailableSourceChainIds;\n const next = current.includes(chainId)\n ? current.filter((id) => id !== chainId)\n : [...current, chainId];\n if (next.length === 0) {\n return current;\n }\n const isAllSelected =\n next.length === allAvailableSourceChainIds.length &&\n allAvailableSourceChainIds.every((id) => next.includes(id));\n return isAllSelected ? null : next;\n });\n },\n [allAvailableSourceChainIds],\n );\n\n const sourceSelection = useMemo(() => {\n const amount =\n intentSourceSpendAmount?.trim() ?? inputs?.amount?.trim() ?? \"\";\n const decimals = getCoverageDecimals({\n type,\n token: inputs?.token,\n chainId: inputs?.chain,\n fallback: filteredBridgableBalance?.decimals,\n });\n const selectedChainSet = new Set(effectiveSelectedSourceChains);\n const selectedTotalRaw =\n !nexusSDK || typeof decimals !== \"number\"\n ? BigInt(0)\n : availableSources.reduce((sum, source) => {\n if (!selectedChainSet.has(source.chain.id)) return sum;\n try {\n return sum + parseUnits(source.balance ?? \"0\", decimals);\n } catch {\n return sum;\n }\n }, BigInt(0));\n const selectedTotal =\n !nexusSDK || typeof decimals !== \"number\"\n ? \"0\"\n : formatAmountForDisplay(selectedTotalRaw, decimals, nexusSDK);\n const baseSelection = {\n selectedTotal,\n requiredTotal: amount || \"0\",\n requiredSafetyTotal: amount || \"0\",\n missingToProceed: \"0\",\n missingToSafety: \"0\",\n coverageState: \"healthy\" as SourceCoverageState,\n coverageToSafetyPercent: 100,\n isBelowRequired: false,\n isBelowSafetyBuffer: false,\n };\n\n if (!nexusSDK || !inputs?.token || !inputs?.chain || !amount) {\n return baseSelection;\n }\n\n try {\n const requiredRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n inputs.token,\n inputs.chain,\n );\n if (requiredRaw <= BigInt(0)) {\n return baseSelection;\n }\n\n const missingToProceedRaw =\n selectedTotalRaw >= requiredRaw\n ? BigInt(0)\n : requiredRaw - selectedTotalRaw;\n const missingToSafetyRaw = missingToProceedRaw;\n\n const coverageState: SourceCoverageState =\n selectedTotalRaw < requiredRaw ? \"error\" : \"healthy\";\n\n const coverageBasisPoints =\n requiredRaw === BigInt(0)\n ? 10_000\n : selectedTotalRaw >= requiredRaw\n ? 10_000\n : Number((selectedTotalRaw * BigInt(10_000)) / requiredRaw);\n\n return {\n selectedTotal,\n requiredTotal: amount,\n requiredSafetyTotal: amount,\n missingToProceed: formatAmountForDisplay(\n missingToProceedRaw,\n decimals,\n nexusSDK,\n ),\n missingToSafety: formatAmountForDisplay(\n missingToSafetyRaw,\n decimals,\n nexusSDK,\n ),\n coverageState,\n coverageToSafetyPercent: coverageBasisPoints / 100,\n isBelowRequired: coverageState === \"error\",\n isBelowSafetyBuffer: coverageState === \"error\",\n };\n } catch {\n return baseSelection;\n }\n }, [\n type,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n inputs?.chain,\n inputs?.amount,\n inputs?.token,\n intentSourceSpendAmount,\n availableSources,\n effectiveSelectedSourceChains,\n ]);\n\n const stopwatch = useStopwatch({ intervalMs: 100 });\n const setStatus = useCallback(\n (status: TransactionStatus) =>\n dispatch({ type: \"setStatus\", payload: status }),\n [],\n );\n\n const resetInputs = useCallback(() => {\n dispatch({ type: \"resetInputs\" });\n }, []);\n\n const {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n } = useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n });\n\n usePolling(\n Boolean(intent.current) &&\n !isDialogOpen &&\n !isSourceMenuOpen &&\n !hasPendingSourceSelectionChanges,\n async () => {\n await refreshIntent();\n },\n 15000,\n );\n\n const debouncedRefreshMaxForSelection = useDebouncedCallback(\n async (requestId: number) => {\n try {\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (requestId !== maxAmountRequestIdRef.current) return;\n setSelectedSourcesMaxAmount(maxForCurrentSelection ?? \"0\");\n } catch (error) {\n if (requestId !== maxAmountRequestIdRef.current) return;\n console.error(\"Unable to calculate max for selected sources:\", error);\n setSelectedSourcesMaxAmount(\"0\");\n }\n },\n MAX_AMOUNT_DEBOUNCE_MS,\n );\n\n useEffect(() => {\n debouncedRefreshMaxForSelection.cancel();\n if (!nexusSDK || !inputs?.token || !inputs?.chain) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(null);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(\"0\");\n return;\n }\n const requestId = ++maxAmountRequestIdRef.current;\n debouncedRefreshMaxForSelection(requestId);\n }, [\n allAvailableSourceChainIds.length,\n configuredMaxAmount,\n debouncedRefreshMaxForSelection,\n inputs?.recipient,\n sourceSelectionKey,\n inputs?.chain,\n inputs?.token,\n nexusSDK,\n ]);\n\n useEffect(() => {\n if (type !== \"bridge\" || !connectedAddress) return;\n const previousConnectedAddress = previousConnectedAddressRef.current;\n if (!previousConnectedAddress) {\n previousConnectedAddressRef.current = connectedAddress;\n return;\n }\n if (connectedAddress === previousConnectedAddress) return;\n previousConnectedAddressRef.current = connectedAddress;\n if (prefill?.recipient) return;\n if (!inputs?.recipient || inputs.recipient === previousConnectedAddress) {\n dispatch({ type: \"setInputs\", payload: { recipient: connectedAddress } });\n }\n }, [type, connectedAddress, inputs?.recipient, prefill?.recipient]);\n\n useEffect(() => {\n invalidatePendingExecution();\n }, [inputs, invalidatePendingExecution]);\n\n useEffect(() => {\n setSelectedSourceChains(null);\n }, [inputs?.token]);\n\n useEffect(() => {\n if (isDialogOpen) return;\n stopwatch.stop();\n stopwatch.reset();\n if (state.status === \"success\" || state.status === \"error\") {\n resetSteps();\n setLastExplorerUrl(\"\");\n setStatus(\"idle\");\n }\n }, [isDialogOpen, resetSteps, setStatus, state.status, stopwatch]);\n\n useEffect(() => {\n if (txError) {\n setTxError(null);\n }\n }, [inputs, txError]);\n\n return {\n inputs,\n setInputs,\n timer: stopwatch.seconds,\n setIsDialogOpen,\n setTxError,\n loading,\n refreshing,\n isDialogOpen,\n txError,\n handleTransaction,\n reset,\n filteredBridgableBalance,\n startTransaction,\n commitAmount,\n lastExplorerUrl,\n steps,\n status: state.status,\n availableSources,\n selectedSourceChains: effectiveSelectedSourceChains,\n toggleSourceChain,\n isSourceSelectionInsufficient: sourceSelection.isBelowRequired,\n isSourceSelectionBelowSafetyBuffer: sourceSelection.isBelowSafetyBuffer,\n isSourceSelectionReadyForAccept:\n sourceSelection.coverageState === \"healthy\",\n sourceCoverageState: sourceSelection.coverageState,\n sourceCoveragePercent: sourceSelection.coverageToSafetyPercent,\n missingToProceed: sourceSelection.missingToProceed,\n missingToSafety: sourceSelection.missingToSafety,\n selectedTotal: sourceSelection.selectedTotal,\n requiredTotal: sourceSelection.requiredTotal,\n requiredSafetyTotal: sourceSelection.requiredSafetyTotal,\n maxAvailableAmount: selectedSourcesMaxAmount ?? undefined,\n isInputsValid: areInputsValid,\n };\n}\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type {\n NexusNetwork,\n OnAllowanceHookData,\n OnIntentHookData,\n TokenBalance,\n ChainBalance,\n} from \"@avail-project/nexus-sdk-v2\";\nimport { parseUnits } from \"viem\";\nimport {\n useEffect,\n useMemo,\n useCallback,\n useRef,\n useState,\n useReducer,\n type RefObject,\n} from \"react\";\nimport { type Address, isAddress } from \"viem\";\nimport { useNexusError } from \"./useNexusError\";\nimport { useTransactionExecution } from \"./useTransactionExecution\";\nimport { usePolling } from \"./usePolling\";\nimport { useStopwatch } from \"./useStopwatch\";\nimport { useDebouncedCallback } from \"./useDebouncedCallback\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport { useTransactionSteps } from \"../tx/useTransactionSteps\";\nimport {\n type SourceCoverageState,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n type TransactionFlowPrefill,\n type TransactionFlowType,\n} from \"../types/transaction-flow\";\nimport {\n MAX_AMOUNT_DEBOUNCE_MS,\n buildInitialInputs,\n clampAmountToMax,\n formatAmountForDisplay,\n getCoverageDecimals,\n normalizeMaxAmount,\n} from \"../utils/transaction-flow\";\n\ntype NexusClient = ReturnType;\n\n// v2 uses a generic step shape; minimal type to satisfy getStepKey constraint\ntype BridgePlanStep = {\n typeID?: string;\n type?: string;\n [key: string]: unknown;\n};\n\ninterface BaseTransactionFlowProps {\n type: TransactionFlowType;\n network: NexusNetwork;\n nexusSDK: NexusClient | null;\n intent: RefObject;\n allowance: RefObject;\n bridgableBalance: TokenBalance[] | null;\n prefill?: TransactionFlowPrefill;\n onComplete?: (explorerUrl?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n maxAmount?: string | number;\n isSourceMenuOpen?: boolean;\n notifyHistoryRefresh?: () => void;\n executeTransaction: TransactionFlowExecutor;\n}\n\nexport interface UseTransactionFlowProps extends BaseTransactionFlowProps {\n connectedAddress?: Address;\n}\n\ntype State = {\n inputs: TransactionFlowInputs;\n status: TransactionStatus;\n};\n\ntype Action =\n | { type: \"setInputs\"; payload: Partial }\n | { type: \"resetInputs\" }\n | { type: \"setStatus\"; payload: TransactionStatus };\n\nexport function useTransactionFlow(props: UseTransactionFlowProps) {\n const {\n type,\n network,\n nexusSDK,\n intent,\n bridgableBalance,\n prefill,\n onComplete,\n onStart,\n onError,\n fetchBalance,\n allowance,\n maxAmount,\n isSourceMenuOpen = false,\n notifyHistoryRefresh,\n executeTransaction,\n } = props;\n\n const connectedAddress = props.connectedAddress;\n const operationName = type === \"bridge\" ? \"bridge\" : \"transfer\";\n const handleNexusError = useNexusError();\n const initialState: State = {\n inputs: buildInitialInputs({ type, network, connectedAddress, prefill }),\n status: \"idle\",\n };\n\n function reducer(state: State, action: Action): State {\n switch (action.type) {\n case \"setInputs\":\n return { ...state, inputs: { ...state.inputs, ...action.payload } };\n case \"resetInputs\":\n return {\n ...state,\n inputs: buildInitialInputs({\n type,\n network,\n connectedAddress,\n prefill,\n }),\n };\n case \"setStatus\":\n return { ...state, status: action.payload };\n default:\n return state;\n }\n }\n\n const [state, dispatch] = useReducer(reducer, initialState);\n const inputs = state.inputs;\n const setInputs = (\n next: TransactionFlowInputs | Partial,\n ) => {\n dispatch({\n type: \"setInputs\",\n payload: next as Partial,\n });\n };\n\n const loading = state.status === \"executing\";\n const [refreshing, setRefreshing] = useState(false);\n const [isDialogOpen, setIsDialogOpen] = useState(false);\n const [txError, setTxError] = useState(null);\n const [lastExplorerUrl, setLastExplorerUrl] = useState(\"\");\n const previousConnectedAddressRef = useRef
(\n connectedAddress,\n );\n const maxAmountRequestIdRef = useRef(0);\n const [selectedSourceChains, setSelectedSourceChains] = useState<\n number[] | null\n >(null);\n const [selectedSourcesMaxAmount, setSelectedSourcesMaxAmount] = useState<\n string | null\n >(null);\n const [appliedSourceSelectionKey, setAppliedSourceSelectionKey] =\n useState(\"ALL\");\n const {\n steps,\n onStepsList,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n const configuredMaxAmount = useMemo(\n () => normalizeMaxAmount(maxAmount),\n [maxAmount],\n );\n\n const areInputsValid = useMemo(() => {\n const hasToken = inputs?.token !== undefined && inputs?.token !== null;\n const hasChain = inputs?.chain !== undefined && inputs?.chain !== null;\n const hasAmount = Boolean(inputs?.amount) && Number(inputs?.amount) > 0;\n const hasValidRecipient =\n Boolean(inputs?.recipient) && isAddress(inputs.recipient as string);\n return hasToken && hasChain && hasAmount && hasValidRecipient;\n }, [inputs]);\n\n const filteredBridgableBalance = useMemo(() => {\n return bridgableBalance?.find((bal) =>\n inputs?.token === \"USDM\"\n ? bal?.symbol === \"USDC\"\n : bal?.symbol === inputs?.token,\n );\n }, [bridgableBalance, inputs?.token]);\n\n const availableSources = useMemo(() => {\n // v2: chainBalances replaces breakdown\n const chainBalances = filteredBridgableBalance?.chainBalances ?? [];\n const destinationChainId = inputs?.chain;\n const nonZero = chainBalances.filter((source: ChainBalance) => {\n if (Number.parseFloat(source.balance ?? \"0\") <= 0) return false;\n if (typeof destinationChainId === \"number\") {\n return source.chain.id !== destinationChainId;\n }\n return true;\n });\n const decimals = filteredBridgableBalance?.decimals;\n if (!nexusSDK || typeof decimals !== \"number\") {\n return nonZero.sort(\n (a, b) => Number.parseFloat(b.balance) - Number.parseFloat(a.balance),\n );\n }\n return nonZero.sort((a: ChainBalance, b: ChainBalance) => {\n try {\n const aRaw = parseUnits(a.balance ?? \"0\", decimals);\n const bRaw = parseUnits(b.balance ?? \"0\", decimals);\n if (aRaw === bRaw) return 0;\n return aRaw > bRaw ? -1 : 1;\n } catch {\n return Number.parseFloat(b.balance) - Number.parseFloat(a.balance);\n }\n });\n }, [\n inputs?.chain,\n filteredBridgableBalance?.chainBalances,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n ]);\n\n const allAvailableSourceChainIds = useMemo(\n () => availableSources.map((source: ChainBalance) => source.chain.id),\n [availableSources],\n );\n\n const effectiveSelectedSourceChains = useMemo(() => {\n if (selectedSourceChains && selectedSourceChains.length > 0) {\n const availableSet = new Set(allAvailableSourceChainIds);\n const filteredSelection = selectedSourceChains.filter((id: number) =>\n availableSet.has(id),\n );\n if (filteredSelection.length > 0) {\n return filteredSelection;\n }\n }\n return allAvailableSourceChainIds;\n }, [selectedSourceChains, allAvailableSourceChainIds]);\n\n const sourceChainsForSdk =\n effectiveSelectedSourceChains.length > 0\n ? effectiveSelectedSourceChains\n : undefined;\n\n const sourceSelectionKey = useMemo(() => {\n if (allAvailableSourceChainIds.length === 0) return \"NONE\";\n if (!selectedSourceChains || selectedSourceChains.length === 0) {\n return \"ALL\";\n }\n return [...effectiveSelectedSourceChains].sort((a: number, b: number) => a - b).join(\"|\");\n }, [\n allAvailableSourceChainIds.length,\n effectiveSelectedSourceChains,\n selectedSourceChains,\n ]);\n const hasPendingSourceSelectionChanges =\n sourceSelectionKey !== appliedSourceSelectionKey;\n const intentSourceSpendAmount = intent.current?.intent?.sourcesTotal;\n\n /**\n * v2: calculateMaxForBridge is removed. Use simulateBridge to get the max amount,\n * or fall back to summing available source balances directly.\n */\n const getMaxForCurrentSelection = useCallback(async () => {\n if (!nexusSDK || !inputs?.token || !inputs?.chain) return undefined;\n\n // Sum balances from selected sources as a direct proxy for max\n const decimals = filteredBridgableBalance?.decimals;\n if (typeof decimals !== \"number\") return \"0\";\n\n const selectedSet = new Set(\n sourceChainsForSdk ?? allAvailableSourceChainIds,\n );\n const totalRaw = availableSources.reduce((sum: bigint, source: ChainBalance) => {\n if (!selectedSet.has(source.chain.id)) return sum;\n try {\n return sum + parseUnits(source.balance ?? \"0\", decimals);\n } catch {\n return sum;\n }\n }, BigInt(0));\n\n const totalReadable = formatToBigIntReadable(totalRaw, decimals);\n if (!totalReadable) return \"0\";\n\n return clampAmountToMax({\n amount: totalReadable,\n maxAmount: configuredMaxAmount,\n nexusSDK,\n token: inputs.token,\n chainId: inputs.chain,\n });\n }, [\n configuredMaxAmount,\n inputs?.chain,\n inputs?.token,\n nexusSDK,\n sourceChainsForSdk,\n allAvailableSourceChainIds,\n availableSources,\n filteredBridgableBalance?.decimals,\n ]);\n\n const toggleSourceChain = useCallback(\n (chainId: number) => {\n setSelectedSourceChains((prev) => {\n if (allAvailableSourceChainIds.length === 0) return prev;\n const current =\n prev && prev.length > 0 ? prev : allAvailableSourceChainIds;\n const next = current.includes(chainId)\n ? current.filter((id: number) => id !== chainId)\n : [...current, chainId];\n if (next.length === 0) {\n return current;\n }\n const isAllSelected =\n next.length === allAvailableSourceChainIds.length &&\n allAvailableSourceChainIds.every((id) => next.includes(id));\n return isAllSelected ? null : next;\n });\n },\n [allAvailableSourceChainIds],\n );\n\n const sourceSelection = useMemo(() => {\n const amount =\n intentSourceSpendAmount?.trim() ?? inputs?.amount?.trim() ?? \"\";\n const decimals = getCoverageDecimals({\n type,\n token: inputs?.token,\n chainId: inputs?.chain,\n fallback: filteredBridgableBalance?.decimals,\n });\n const selectedChainSet = new Set(effectiveSelectedSourceChains);\n const selectedTotalRaw =\n !nexusSDK || typeof decimals !== \"number\"\n ? BigInt(0)\n : availableSources.reduce((sum: bigint, source: ChainBalance) => {\n if (!selectedChainSet.has(source.chain.id)) return sum;\n try {\n return sum + parseUnits(source.balance ?? \"0\", decimals);\n } catch {\n return sum;\n }\n }, BigInt(0));\n const selectedTotal =\n !nexusSDK || typeof decimals !== \"number\"\n ? \"0\"\n : formatAmountForDisplay(selectedTotalRaw, decimals, nexusSDK);\n const baseSelection = {\n selectedTotal,\n requiredTotal: amount || \"0\",\n requiredSafetyTotal: amount || \"0\",\n missingToProceed: \"0\",\n missingToSafety: \"0\",\n coverageState: \"healthy\" as SourceCoverageState,\n coverageToSafetyPercent: 100,\n isBelowRequired: false,\n isBelowSafetyBuffer: false,\n };\n\n if (!nexusSDK || !inputs?.token || !inputs?.chain || !amount) {\n return baseSelection;\n }\n\n try {\n // v2: convertTokenReadableAmountToBigInt(amount, tokenSymbol, chainId)\n const requiredRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n inputs.token,\n inputs.chain,\n );\n if (requiredRaw <= BigInt(0)) {\n return baseSelection;\n }\n\n const missingToProceedRaw =\n selectedTotalRaw >= requiredRaw\n ? BigInt(0)\n : requiredRaw - selectedTotalRaw;\n const missingToSafetyRaw = missingToProceedRaw;\n\n const coverageState: SourceCoverageState =\n selectedTotalRaw < requiredRaw ? \"error\" : \"healthy\";\n\n const coverageBasisPoints =\n requiredRaw === BigInt(0)\n ? 10_000\n : selectedTotalRaw >= requiredRaw\n ? 10_000\n : Number((selectedTotalRaw * BigInt(10_000)) / requiredRaw);\n\n return {\n selectedTotal,\n requiredTotal: amount,\n requiredSafetyTotal: amount,\n missingToProceed: formatAmountForDisplay(\n missingToProceedRaw,\n decimals,\n nexusSDK,\n ),\n missingToSafety: formatAmountForDisplay(\n missingToSafetyRaw,\n decimals,\n nexusSDK,\n ),\n coverageState,\n coverageToSafetyPercent: coverageBasisPoints / 100,\n isBelowRequired: coverageState === \"error\",\n isBelowSafetyBuffer: coverageState === \"error\",\n };\n } catch {\n return baseSelection;\n }\n }, [\n type,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n inputs?.chain,\n inputs?.amount,\n inputs?.token,\n intentSourceSpendAmount,\n availableSources,\n effectiveSelectedSourceChains,\n ]);\n\n const stopwatch = useStopwatch({ intervalMs: 100 });\n const setStatus = useCallback(\n (status: TransactionStatus) =>\n dispatch({ type: \"setStatus\", payload: status }),\n [],\n );\n\n const resetInputs = useCallback(() => {\n dispatch({ type: \"resetInputs\" });\n }, []);\n\n const {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n } = useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n });\n\n usePolling(\n Boolean(intent.current) &&\n !isDialogOpen &&\n !isSourceMenuOpen &&\n !hasPendingSourceSelectionChanges,\n async () => {\n await refreshIntent();\n },\n 15000,\n );\n\n const debouncedRefreshMaxForSelection = useDebouncedCallback(\n async (requestId: number) => {\n try {\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (requestId !== maxAmountRequestIdRef.current) return;\n setSelectedSourcesMaxAmount(maxForCurrentSelection ?? \"0\");\n } catch (error) {\n if (requestId !== maxAmountRequestIdRef.current) return;\n console.error(\"Unable to calculate max for selected sources:\", error);\n setSelectedSourcesMaxAmount(\"0\");\n }\n },\n MAX_AMOUNT_DEBOUNCE_MS,\n );\n\n useEffect(() => {\n debouncedRefreshMaxForSelection.cancel();\n if (!nexusSDK || !inputs?.token || !inputs?.chain) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(null);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(\"0\");\n return;\n }\n const requestId = ++maxAmountRequestIdRef.current;\n debouncedRefreshMaxForSelection(requestId);\n }, [\n allAvailableSourceChainIds.length,\n configuredMaxAmount,\n debouncedRefreshMaxForSelection,\n inputs?.recipient,\n sourceSelectionKey,\n inputs?.chain,\n inputs?.token,\n nexusSDK,\n ]);\n\n useEffect(() => {\n if (type !== \"bridge\" || !connectedAddress) return;\n const previousConnectedAddress = previousConnectedAddressRef.current;\n if (!previousConnectedAddress) {\n previousConnectedAddressRef.current = connectedAddress;\n return;\n }\n if (connectedAddress === previousConnectedAddress) return;\n previousConnectedAddressRef.current = connectedAddress;\n if (prefill?.recipient) return;\n if (!inputs?.recipient || inputs.recipient === previousConnectedAddress) {\n dispatch({ type: \"setInputs\", payload: { recipient: connectedAddress } });\n }\n }, [type, connectedAddress, inputs?.recipient, prefill?.recipient]);\n\n useEffect(() => {\n invalidatePendingExecution();\n }, [inputs, invalidatePendingExecution]);\n\n useEffect(() => {\n setSelectedSourceChains(null);\n }, [inputs?.token]);\n\n // Safety-net: stop the stopwatch as soon as status reaches a terminal state.\n // This ensures the timer freezes even if the onEvent closure's stopwatch.stop()\n // didn't fire (e.g. stale closure reference or SDK promise resolved oddly).\n useEffect(() => {\n if (state.status === \"success\" || state.status === \"error\") {\n stopwatch.stop();\n }\n }, [state.status, stopwatch]);\n\n useEffect(() => {\n if (isDialogOpen) return;\n stopwatch.stop();\n stopwatch.reset();\n if (state.status === \"success\" || state.status === \"error\") {\n resetSteps();\n setLastExplorerUrl(\"\");\n setStatus(\"idle\");\n }\n }, [isDialogOpen, resetSteps, setStatus, state.status, stopwatch]);\n\n useEffect(() => {\n if (txError) {\n setTxError(null);\n }\n }, [inputs, txError]);\n\n\n return {\n inputs,\n setInputs,\n timer: stopwatch.seconds,\n setIsDialogOpen,\n setTxError,\n loading,\n refreshing,\n isDialogOpen,\n txError,\n handleTransaction,\n reset,\n filteredBridgableBalance,\n startTransaction,\n commitAmount,\n lastExplorerUrl,\n steps,\n status: state.status,\n availableSources,\n selectedSourceChains: effectiveSelectedSourceChains,\n toggleSourceChain,\n isSourceSelectionInsufficient: sourceSelection.isBelowRequired,\n isSourceSelectionBelowSafetyBuffer: sourceSelection.isBelowSafetyBuffer,\n isSourceSelectionReadyForAccept:\n sourceSelection.coverageState === \"healthy\",\n sourceCoverageState: sourceSelection.coverageState,\n sourceCoveragePercent: sourceSelection.coverageToSafetyPercent,\n missingToProceed: sourceSelection.missingToProceed,\n missingToSafety: sourceSelection.missingToSafety,\n selectedTotal: sourceSelection.selectedTotal,\n requiredTotal: sourceSelection.requiredTotal,\n requiredSafetyTotal: sourceSelection.requiredSafetyTotal,\n maxAvailableAmount: selectedSourcesMaxAmount ?? undefined,\n isInputsValid: areInputsValid,\n };\n}\n\n/** Helper: format a bigint rawAmount with decimals into a readable decimal string. */\nfunction formatToBigIntReadable(raw: bigint, decimals: number): string {\n if (raw <= BigInt(0)) return \"0\";\n const divisor = BigInt(10 ** decimals);\n const whole = raw / divisor;\n const fraction = raw % divisor;\n if (fraction === BigInt(0)) return whole.toString();\n const fractionStr = fraction.toString().padStart(decimals, \"0\").replace(/0+$/, \"\");\n return `${whole}.${fractionStr}`;\n}\n", "type": "registry:component", "target": "components/common/hooks/useTransactionFlow.ts" }, @@ -159,7 +159,7 @@ }, { "path": "registry/nexus-elements/common/tx/steps.ts", - "content": "import type { SwapStepType } from \"@avail-project/nexus-core\";\nimport type { GenericStep } from \"./types\";\nimport { getStepKey } from \"./types\";\n\n/**\n * Predefined expected steps for swaps to seed UI before events arrive.\n * Kept here to avoid duplication across exact-in and exact-out hooks.\n */\nexport const SWAP_EXPECTED_STEPS: SwapStepType[] = [\n { type: \"SWAP_START\", typeID: \"SWAP_START\" } as SwapStepType,\n { type: \"DETERMINING_SWAP\", typeID: \"DETERMINING_SWAP\" } as SwapStepType,\n {\n type: \"CREATE_PERMIT_FOR_SOURCE_SWAP\",\n typeID:\n \"CREATE_PERMIT_FOR_SOURCE_SWAP\" as unknown as SwapStepType[\"typeID\"],\n } as SwapStepType,\n {\n type: \"SOURCE_SWAP_BATCH_TX\",\n typeID: \"SOURCE_SWAP_BATCH_TX\",\n } as SwapStepType,\n {\n type: \"SOURCE_SWAP_HASH\",\n typeID: \"SOURCE_SWAP_HASH\" as unknown as SwapStepType[\"typeID\"],\n } as SwapStepType,\n { type: \"RFF_ID\", typeID: \"RFF_ID\" } as SwapStepType,\n {\n type: \"DESTINATION_SWAP_BATCH_TX\",\n typeID: \"DESTINATION_SWAP_BATCH_TX\",\n } as SwapStepType,\n {\n type: \"DESTINATION_SWAP_HASH\",\n typeID: \"DESTINATION_SWAP_HASH\" as unknown as SwapStepType[\"typeID\"],\n } as SwapStepType,\n { type: \"SWAP_COMPLETE\", typeID: \"SWAP_COMPLETE\" } as SwapStepType,\n];\n\nexport function seedSteps(expected: T[]): Array> {\n return expected.map((st, index) => ({\n id: index,\n completed: false,\n step: st,\n }));\n}\n\nexport function computeAllCompleted(steps: Array>): boolean {\n return steps.length > 0 && steps.every((s) => s.completed);\n}\n\n/**\n * Replace the current list of steps with a new list, preserving completion\n * for any steps that were already marked completed (matched by key).\n */\nexport function mergeStepsList(\n prev: Array>,\n list: T[]\n): Array> {\n const completedKeys = new Set();\n for (const prevStep of prev) {\n if (prevStep.completed) {\n completedKeys.add(getStepKey(prevStep.step));\n }\n }\n const next: Array> = [];\n for (let index = 0; index < list.length; index++) {\n const step = list[index];\n const key = getStepKey(step);\n next.push({\n id: index,\n completed: completedKeys.has(key),\n step,\n });\n }\n return next;\n}\n\n/**\n * Mark a step complete in-place; if the step doesn't yet exist, append it.\n */\nexport function mergeStepComplete(\n prev: Array>,\n step: T\n): Array> {\n const key = getStepKey(step);\n const updated: Array> = [];\n let found = false;\n for (const s of prev) {\n if (getStepKey(s.step) === key) {\n updated.push({ ...s, completed: true, step: { ...s.step, ...step } });\n found = true;\n } else {\n updated.push(s);\n }\n }\n if (!found) {\n updated.push({\n id: updated.length,\n completed: true,\n step,\n });\n }\n return updated;\n}\n", + "content": "// v2: SwapStepType is no longer exported from the SDK — use a local step shape\n// that matches v2 SwapPlanStep discriminator pattern\nexport type SwapStepType = {\n typeID?: string;\n type?: string;\n [key: string]: unknown;\n};\n\nimport type { GenericStep } from \"./types\";\nimport { getStepKey } from \"./types\";\n\n/**\n * Predefined expected steps for swaps to seed UI before events arrive.\n * Uses v2 stepType names that match SwapPlanProgressEvent.stepType discriminators.\n */\nexport const SWAP_EXPECTED_STEPS: SwapStepType[] = [\n { type: \"source_swap\", typeID: \"source_swap\" },\n { type: \"eoa_to_ephemeral_transfer\", typeID: \"eoa_to_ephemeral_transfer\" },\n { type: \"bridge_deposit\", typeID: \"bridge_deposit\" },\n { type: \"bridge_intent_submission\", typeID: \"bridge_intent_submission\" },\n { type: \"bridge_fill\", typeID: \"bridge_fill\" },\n { type: \"destination_swap\", typeID: \"destination_swap\" },\n];\n\nexport function seedSteps(expected: T[]): Array> {\n return expected.map((st, index) => ({\n id: index,\n completed: false,\n step: st,\n }));\n}\n\nexport function computeAllCompleted(steps: Array>): boolean {\n return steps.length > 0 && steps.every((s) => s.completed);\n}\n\n/**\n * Replace the current list of steps with a new list, preserving completion\n * for any steps that were already marked completed (matched by key).\n */\nexport function mergeStepsList(\n prev: Array>,\n list: T[]\n): Array> {\n const completedKeys = new Set();\n for (const prevStep of prev) {\n if (prevStep.completed) {\n completedKeys.add(getStepKey(prevStep.step));\n }\n }\n const next: Array> = [];\n for (let index = 0; index < list.length; index++) {\n const step = list[index];\n const key = getStepKey(step);\n next.push({\n id: index,\n completed: completedKeys.has(key),\n step,\n });\n }\n return next;\n}\n\n/**\n * Mark a step complete in-place; if the step doesn't yet exist, append it.\n */\nexport function mergeStepComplete(\n prev: Array>,\n step: T\n): Array> {\n const key = getStepKey(step);\n const updated: Array> = [];\n let found = false;\n for (const s of prev) {\n if (getStepKey(s.step) === key) {\n updated.push({ ...s, completed: true, step: { ...s.step, ...step } });\n found = true;\n } else {\n updated.push(s);\n }\n }\n if (!found) {\n updated.push({\n id: updated.length,\n completed: true,\n step,\n });\n }\n return updated;\n}\n", "type": "registry:component", "target": "components/common/tx/steps.ts" }, @@ -177,25 +177,25 @@ }, { "path": "registry/nexus-elements/common/types/transaction-flow.ts", - "content": "import {\n type NexusSDK,\n type SUPPORTED_CHAINS_IDS,\n type SUPPORTED_TOKENS,\n} from \"@avail-project/nexus-core\";\nimport { type Address } from \"viem\";\n\nexport type TransactionFlowType = \"bridge\" | \"transfer\";\n\nexport interface TransactionFlowInputs {\n chain: SUPPORTED_CHAINS_IDS;\n token: SUPPORTED_TOKENS;\n amount?: string;\n recipient?: `0x${string}`;\n}\n\nexport interface TransactionFlowPrefill {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n}\n\ntype BridgeOptions = NonNullable[1]>;\n\nexport type TransactionFlowEvent =\n NonNullable extends (event: infer E) => void\n ? E\n : never;\n\nexport type TransactionFlowOnEvent = NonNullable;\n\nexport interface TransactionFlowExecuteParams {\n token: SUPPORTED_TOKENS;\n amount: bigint;\n toChainId: SUPPORTED_CHAINS_IDS;\n recipient: `0x${string}`;\n sourceChains?: number[];\n onEvent: TransactionFlowOnEvent;\n}\n\nexport type TransactionFlowExecutor = (\n params: TransactionFlowExecuteParams,\n) => Promise<{ explorerUrl: string } | null>;\n\nexport type SourceCoverageState = \"healthy\" | \"warning\" | \"error\";\n\nexport interface SourceSelectionValidation {\n coverageState: SourceCoverageState;\n isBelowRequired: boolean;\n missingToProceed: string;\n missingToSafety: string;\n}\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport { type Address } from \"viem\";\n\ntype NexusClient = ReturnType;\n\n// v2 uses string token symbols (toTokenSymbol) with number chain IDs\nexport type TransactionFlowType = \"bridge\" | \"transfer\";\n\nexport interface TransactionFlowInputs {\n chain: number;\n token: string;\n amount?: string;\n recipient?: `0x${string}`;\n}\n\nexport interface TransactionFlowPrefill {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n}\n\n// v2 bridge onEvent uses typed discriminated union, not NEXUS_EVENTS\nexport type TransactionFlowEvent =\n | { type: \"status\"; status: string }\n | { type: \"plan_preview\"; plan: { steps: unknown[] } }\n | { type: \"plan_confirmed\"; plan: { steps: unknown[] } }\n | { type: \"plan_progress\"; stepType: string; state: string; step: unknown };\n\nexport type TransactionFlowOnEvent = (event: TransactionFlowEvent) => void;\n\nexport interface TransactionFlowExecuteParams {\n token: string;\n amount: bigint;\n toChainId: number;\n recipient: `0x${string}`;\n sources?: number[];\n onEvent: TransactionFlowOnEvent;\n}\n\nexport type TransactionFlowExecutor = (\n params: TransactionFlowExecuteParams,\n) => Promise<{ explorerUrl: string } | null>;\n\nexport type SourceCoverageState = \"healthy\" | \"warning\" | \"error\";\n\nexport interface SourceSelectionValidation {\n coverageState: SourceCoverageState;\n isBelowRequired: boolean;\n missingToProceed: string;\n missingToSafety: string;\n}\n", "type": "registry:component", "target": "components/common/types/transaction-flow.ts" }, { "path": "registry/nexus-elements/common/utils/constant.ts", - "content": "import { SUPPORTED_CHAINS } from \"@avail-project/nexus-core\";\nimport { formatUnits, parseUnits } from \"viem\";\n\nexport const SHORT_CHAIN_NAME: Record = {\n [SUPPORTED_CHAINS.ETHEREUM]: \"Ethereum\",\n [SUPPORTED_CHAINS.BASE]: \"Base\",\n [SUPPORTED_CHAINS.ARBITRUM]: \"Arbitrum\",\n [SUPPORTED_CHAINS.OPTIMISM]: \"Optimism\",\n [SUPPORTED_CHAINS.POLYGON]: \"Polygon\",\n [SUPPORTED_CHAINS.AVALANCHE]: \"Avalanche\",\n [SUPPORTED_CHAINS.SCROLL]: \"Scroll\",\n [SUPPORTED_CHAINS.MEGAETH]: \"MegaETH\",\n [SUPPORTED_CHAINS.KAIA]: \"Kaia\",\n [SUPPORTED_CHAINS.BNB]: \"BNB\",\n [SUPPORTED_CHAINS.MONAD]: \"Monad\",\n [SUPPORTED_CHAINS.HYPEREVM]: \"HyperEVM\",\n [SUPPORTED_CHAINS.CITREA]: \"Citrea\",\n // [SUPPORTED_CHAINS.TRON]: \"Tron\",\n [SUPPORTED_CHAINS.SEPOLIA]: \"Sepolia\",\n [SUPPORTED_CHAINS.BASE_SEPOLIA]: \"Base Sepolia\",\n [SUPPORTED_CHAINS.ARBITRUM_SEPOLIA]: \"Arbitrum Sepolia\",\n [SUPPORTED_CHAINS.OPTIMISM_SEPOLIA]: \"Optimism Sepolia\",\n [SUPPORTED_CHAINS.POLYGON_AMOY]: \"Polygon Amoy\",\n [SUPPORTED_CHAINS.MONAD_TESTNET]: \"Monad Testnet\",\n // [SUPPORTED_CHAINS.TRON_SHASTA]: \"Tron Shasta\",\n} as const;\n\nconst DEFAULT_SAFETY_MARGIN = 0.01; // 1%\n\n/**\n * Compute an amount string for fraction buttons (25%, 50%, 75%, 100%).\n *\n * @param balanceStr - user's balance as a human decimal string (e.g. \"12.345\") OR as base-unit integer string if `balanceIsBaseUnits` true\n * @param fraction - fraction e.g. 0.25, 0.5, 0.75, 1\n * @param decimals - token decimals (6 for USDC/USDT, 18 for ETH)\n * @param safetyMargin - 0.01 for 1% default\n * @param balanceIsBaseUnits - if true, balanceStr is already base units integer string (wei / smallest unit)\n * @returns decimal string clipped to token decimals (rounded down)\n */\nexport function computeAmountFromFraction(\n balanceStr: string,\n fraction: number,\n decimals: number,\n safetyMargin = DEFAULT_SAFETY_MARGIN,\n balanceIsBaseUnits = false,\n): string {\n if (!balanceStr) return \"0\";\n\n // parse balance into base units (BigInt)\n const balanceUnits: bigint = balanceIsBaseUnits\n ? BigInt(balanceStr)\n : parseUnits(balanceStr, decimals);\n\n if (balanceUnits === BigInt(0)) return \"0\";\n\n // Use an integer precision multiplier to avoid FP issues\n const PREC = 1_000_000; // 1e6 precision for fraction & safety margin\n const safetyMul = BigInt(Math.max(0, Math.floor((1 - safetyMargin) * PREC))); // (1 - safetyMargin) * PREC\n const fractionMul = BigInt(Math.max(0, Math.floor(fraction * PREC))); // fraction * PREC\n\n // Apply safety margin: floor(balance * (1 - safetyMargin))\n const maxAfterSafety = (balanceUnits * safetyMul) / BigInt(PREC);\n\n // Apply fraction and floor: floor(maxAfterSafety * fraction)\n let desiredUnits = (maxAfterSafety * fractionMul) / BigInt(PREC);\n\n // Extra clamp just in case\n if (desiredUnits > balanceUnits) desiredUnits = balanceUnits;\n if (desiredUnits < BigInt(0)) desiredUnits = BigInt(0);\n\n // format back to human readable decimal string with token decimals (formatUnits truncates/keeps decimals)\n // formatUnits will produce exactly decimals digits if fractional part exists; we'll strip trailing zeros.\n const raw = formatUnits(desiredUnits, decimals);\n // strip trailing zeros and possible trailing dot\n return raw\n .replace(/(\\.\\d*?[1-9])0+$/u, \"$1\")\n .replace(/\\.0+$/u, \"\")\n .replace(/^\\.$/u, \"0\");\n}\n\nexport const usdFormatter = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n});\n\nconst usdFormatterPrecise = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 3,\n maximumFractionDigits: 3,\n});\n\n/**\n * Formats USD values for UI.\n * Values between 0 and 0.001 are shown as \"< $0.001\".\n * Values between 0.001 and 0.01 are shown with 3 decimals.\n */\nexport function formatUsdForDisplay(value: number): string {\n if (!Number.isFinite(value)) return usdFormatter.format(0);\n const absValue = Math.abs(value);\n\n if (absValue === 0) return usdFormatter.format(0);\n if (absValue < 0.001) return \"< $0.001\";\n if (absValue < 0.01) {\n return usdFormatterPrecise.format(value);\n }\n\n return usdFormatter.format(value);\n}\n", + "content": "import { formatUnits, parseUnits } from \"viem\";\n\n// v2: SUPPORTED_CHAINS removed — using literal EVM chain IDs\nexport const SHORT_CHAIN_NAME: Record = {\n 1: \"Ethereum\",\n 8453: \"Base\",\n 42161: \"Arbitrum\",\n 10: \"Optimism\",\n 137: \"Polygon\",\n 43114: \"Avalanche\",\n 534352: \"Scroll\",\n 6342: \"MegaETH\",\n 8217: \"Kaia\",\n 56: \"BNB\",\n 10143: \"Monad\",\n 999: \"HyperEVM\",\n 5115: \"Citrea\",\n 11155111: \"Sepolia\",\n 84532: \"Base Sepolia\",\n 421614: \"Arbitrum Sepolia\",\n 11155420: \"Optimism Sepolia\",\n 80002: \"Polygon Amoy\",\n} as const;\n\nconst DEFAULT_SAFETY_MARGIN = 0.01; // 1%\n\n/**\n * Compute an amount string for fraction buttons (25%, 50%, 75%, 100%).\n *\n * @param balanceStr - user's balance as a human decimal string (e.g. \"12.345\") OR as base-unit integer string if `balanceIsBaseUnits` true\n * @param fraction - fraction e.g. 0.25, 0.5, 0.75, 1\n * @param decimals - token decimals (6 for USDC/USDT, 18 for ETH)\n * @param safetyMargin - 0.01 for 1% default\n * @param balanceIsBaseUnits - if true, balanceStr is already base units integer string (wei / smallest unit)\n * @returns decimal string clipped to token decimals (rounded down)\n */\nexport function computeAmountFromFraction(\n balanceStr: string,\n fraction: number,\n decimals: number,\n safetyMargin = DEFAULT_SAFETY_MARGIN,\n balanceIsBaseUnits = false,\n): string {\n if (!balanceStr) return \"0\";\n\n // parse balance into base units (BigInt)\n const balanceUnits: bigint = balanceIsBaseUnits\n ? BigInt(balanceStr)\n : parseUnits(balanceStr, decimals);\n\n if (balanceUnits === BigInt(0)) return \"0\";\n\n // Use an integer precision multiplier to avoid FP issues\n const PREC = 1_000_000; // 1e6 precision for fraction & safety margin\n const safetyMul = BigInt(Math.max(0, Math.floor((1 - safetyMargin) * PREC))); // (1 - safetyMargin) * PREC\n const fractionMul = BigInt(Math.max(0, Math.floor(fraction * PREC))); // fraction * PREC\n\n // Apply safety margin: floor(balance * (1 - safetyMargin))\n const maxAfterSafety = (balanceUnits * safetyMul) / BigInt(PREC);\n\n // Apply fraction and floor: floor(maxAfterSafety * fraction)\n let desiredUnits = (maxAfterSafety * fractionMul) / BigInt(PREC);\n\n // Extra clamp just in case\n if (desiredUnits > balanceUnits) desiredUnits = balanceUnits;\n if (desiredUnits < BigInt(0)) desiredUnits = BigInt(0);\n\n // format back to human readable decimal string with token decimals (formatUnits truncates/keeps decimals)\n // formatUnits will produce exactly decimals digits if fractional part exists; we'll strip trailing zeros.\n const raw = formatUnits(desiredUnits, decimals);\n // strip trailing zeros and possible trailing dot\n return raw\n .replace(/(\\.\\d*?[1-9])0+$/u, \"$1\")\n .replace(/\\.0+$/u, \"\")\n .replace(/^\\.$/u, \"0\");\n}\n\nexport const usdFormatter = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n});\n\nconst usdFormatterPrecise = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 3,\n maximumFractionDigits: 3,\n});\n\n/**\n * Formats USD values for UI.\n * Values between 0 and 0.001 are shown as \"< $0.001\".\n * Values between 0.001 and 0.01 are shown with 3 decimals.\n */\nexport function formatUsdForDisplay(value: number): string {\n if (!Number.isFinite(value)) return usdFormatter.format(0);\n const absValue = Math.abs(value);\n\n if (absValue === 0) return usdFormatter.format(0);\n if (absValue < 0.001) return \"< $0.001\";\n if (absValue < 0.01) {\n return usdFormatterPrecise.format(value);\n }\n\n return usdFormatter.format(value);\n}\n", "type": "registry:component", "target": "components/common/utils/constant.ts" }, { "path": "registry/nexus-elements/common/utils/token-pricing.ts", - "content": "import type { SupportedChainsAndTokensResult } from \"@avail-project/nexus-core\";\n\nconst COINBASE_SPOT_API_BASE = \"https://api.coinbase.com/v2/prices\";\nconst COINBASE_EXCHANGE_RATES_API_BASE =\n \"https://api.coinbase.com/v2/exchange-rates\";\n\nexport const DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS = 4_000;\nexport const USD_PEGGED_FALLBACK_RATE = 1;\nexport const DEFAULT_USD_PEGGED_TOKEN_SYMBOLS = [\n \"USDT\",\n \"USDC\",\n \"USDS\",\n \"DAI\",\n \"USDM\",\n \"FDUSD\",\n \"BUSD\",\n \"TUSD\",\n \"PYUSD\",\n \"GUSD\",\n \"LUSD\",\n \"USDE\",\n \"USDP\",\n] as const;\n\ntype CoinbaseSpotPriceResponse = {\n data?: {\n amount?: string | number;\n };\n};\n\ntype CoinbaseExchangeRatesResponse = {\n data?: {\n rates?: Record;\n };\n};\n\ntype SupportedTokenMetadata = {\n symbol?: string;\n equivalentCurrency?: string;\n};\n\ntype SupportedChainMetadata = {\n tokens?: SupportedTokenMetadata[];\n};\n\nexport function normalizeTokenSymbol(tokenSymbol: string): string {\n return tokenSymbol.trim().toUpperCase();\n}\n\nexport function toFinitePositiveNumber(value: unknown): number | null {\n const parsed = Number.parseFloat(String(value));\n if (!Number.isFinite(parsed) || parsed <= 0) {\n return null;\n }\n return parsed;\n}\n\nexport function getCoinbaseSymbolCandidates(tokenSymbol: string): string[] {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return [];\n\n const baseSymbol = normalized.split(/[._-]/)[0] ?? normalized;\n const wrappedBase =\n baseSymbol.startsWith(\"W\") && baseSymbol.length > 3\n ? baseSymbol.slice(1)\n : null;\n\n return Array.from(\n new Set(\n [normalized, baseSymbol, wrappedBase].filter(\n (symbol): symbol is string => Boolean(symbol),\n ),\n ),\n );\n}\n\nexport function buildUsdPeggedSymbolSet(\n supportedChains: SupportedChainsAndTokensResult | null,\n baseSymbols: Iterable = DEFAULT_USD_PEGGED_TOKEN_SYMBOLS,\n): Set {\n const symbolSet = new Set(baseSymbols);\n\n for (const chain of (supportedChains ?? []) as SupportedChainMetadata[]) {\n for (const token of chain.tokens ?? []) {\n const symbol = normalizeTokenSymbol(token.symbol ?? \"\");\n const equivalent = normalizeTokenSymbol(token.equivalentCurrency ?? \"\");\n if (!symbol) continue;\n\n if (equivalent && symbolSet.has(equivalent)) {\n symbolSet.add(symbol);\n }\n }\n }\n\n return symbolSet;\n}\n\nasync function fetchJsonWithTimeout(\n url: string,\n requestTimeoutMs: number,\n): Promise {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);\n try {\n const response = await fetch(url, {\n signal: controller.signal,\n });\n if (!response.ok) return null;\n return (await response.json()) as T;\n } catch {\n return null;\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nexport async function fetchCoinbaseUsdRate(\n tokenSymbol: string,\n requestTimeoutMs = DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS,\n): Promise {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return null;\n\n for (const candidate of getCoinbaseSymbolCandidates(normalized)) {\n const spotBody = await fetchJsonWithTimeout(\n `${COINBASE_SPOT_API_BASE}/${encodeURIComponent(candidate)}-USD/spot`,\n requestTimeoutMs,\n );\n const spotAmount = toFinitePositiveNumber(spotBody?.data?.amount);\n if (spotAmount) return spotAmount;\n\n const exchangeRatesBody =\n await fetchJsonWithTimeout(\n `${COINBASE_EXCHANGE_RATES_API_BASE}?currency=${encodeURIComponent(candidate)}`,\n requestTimeoutMs,\n );\n const exchangeRatesAmount = toFinitePositiveNumber(\n exchangeRatesBody?.data?.rates?.USD,\n );\n if (exchangeRatesAmount) return exchangeRatesAmount;\n }\n\n return null;\n}\n", + "content": "// v2: getSupportedChains() return type is inferred directly; define a structural type\ntype SupportedChainsAndTokensResult = readonly {\n tokens?: { symbol?: string; equivalentCurrency?: string }[];\n [key: string]: unknown;\n}[];\n\nconst COINBASE_SPOT_API_BASE = \"https://api.coinbase.com/v2/prices\";\nconst COINBASE_EXCHANGE_RATES_API_BASE =\n \"https://api.coinbase.com/v2/exchange-rates\";\n\nexport const DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS = 4_000;\nexport const USD_PEGGED_FALLBACK_RATE = 1;\nexport const DEFAULT_USD_PEGGED_TOKEN_SYMBOLS = [\n \"USDT\",\n \"USDC\",\n \"USDS\",\n \"DAI\",\n \"USDM\",\n \"FDUSD\",\n \"BUSD\",\n \"TUSD\",\n \"PYUSD\",\n \"GUSD\",\n \"LUSD\",\n \"USDE\",\n \"USDP\",\n] as const;\n\ntype CoinbaseSpotPriceResponse = {\n data?: {\n amount?: string | number;\n };\n};\n\ntype CoinbaseExchangeRatesResponse = {\n data?: {\n rates?: Record;\n };\n};\n\ntype SupportedTokenMetadata = {\n symbol?: string;\n equivalentCurrency?: string;\n};\n\ntype SupportedChainMetadata = {\n tokens?: SupportedTokenMetadata[];\n};\n\nexport function normalizeTokenSymbol(tokenSymbol: string): string {\n return tokenSymbol.trim().toUpperCase();\n}\n\nexport function toFinitePositiveNumber(value: unknown): number | null {\n const parsed = Number.parseFloat(String(value));\n if (!Number.isFinite(parsed) || parsed <= 0) {\n return null;\n }\n return parsed;\n}\n\nexport function getCoinbaseSymbolCandidates(tokenSymbol: string): string[] {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return [];\n\n const baseSymbol = normalized.split(/[._-]/)[0] ?? normalized;\n const wrappedBase =\n baseSymbol.startsWith(\"W\") && baseSymbol.length > 3\n ? baseSymbol.slice(1)\n : null;\n\n return Array.from(\n new Set(\n [normalized, baseSymbol, wrappedBase].filter(\n (symbol): symbol is string => Boolean(symbol),\n ),\n ),\n );\n}\n\nexport function buildUsdPeggedSymbolSet(\n supportedChains: SupportedChainsAndTokensResult | null,\n baseSymbols: Iterable = DEFAULT_USD_PEGGED_TOKEN_SYMBOLS,\n): Set {\n const symbolSet = new Set(baseSymbols);\n\n for (const chain of (supportedChains ?? []) as SupportedChainMetadata[]) {\n for (const token of chain.tokens ?? []) {\n const symbol = normalizeTokenSymbol(token.symbol ?? \"\");\n const equivalent = normalizeTokenSymbol(token.equivalentCurrency ?? \"\");\n if (!symbol) continue;\n\n if (equivalent && symbolSet.has(equivalent)) {\n symbolSet.add(symbol);\n }\n }\n }\n\n return symbolSet;\n}\n\nasync function fetchJsonWithTimeout(\n url: string,\n requestTimeoutMs: number,\n): Promise {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);\n try {\n const response = await fetch(url, {\n signal: controller.signal,\n });\n if (!response.ok) return null;\n return (await response.json()) as T;\n } catch {\n return null;\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nexport async function fetchCoinbaseUsdRate(\n tokenSymbol: string,\n requestTimeoutMs = DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS,\n): Promise {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return null;\n\n for (const candidate of getCoinbaseSymbolCandidates(normalized)) {\n const spotBody = await fetchJsonWithTimeout(\n `${COINBASE_SPOT_API_BASE}/${encodeURIComponent(candidate)}-USD/spot`,\n requestTimeoutMs,\n );\n const spotAmount = toFinitePositiveNumber(spotBody?.data?.amount);\n if (spotAmount) return spotAmount;\n\n const exchangeRatesBody =\n await fetchJsonWithTimeout(\n `${COINBASE_EXCHANGE_RATES_API_BASE}?currency=${encodeURIComponent(candidate)}`,\n requestTimeoutMs,\n );\n const exchangeRatesAmount = toFinitePositiveNumber(\n exchangeRatesBody?.data?.rates?.USD,\n );\n if (exchangeRatesAmount) return exchangeRatesAmount;\n }\n\n return null;\n}\n", "type": "registry:component", "target": "components/common/utils/token-pricing.ts" }, { "path": "registry/nexus-elements/common/utils/transaction-flow.ts", - "content": "import {\n formatUnits,\n type NexusNetwork,\n NexusSDK,\n SUPPORTED_CHAINS,\n type SUPPORTED_CHAINS_IDS,\n type SUPPORTED_TOKENS,\n} from \"@avail-project/nexus-core\";\nimport { type Address } from \"viem\";\n\nconst MAX_AMOUNT_REGEX = /^\\d*\\.?\\d+$/;\n\nexport const MAX_AMOUNT_DEBOUNCE_MS = 300;\n\nexport const normalizeMaxAmount = (\n maxAmount?: string | number,\n): string | undefined => {\n if (maxAmount === undefined || maxAmount === null) return undefined;\n const value = String(maxAmount).trim();\n if (!value || value === \".\" || !MAX_AMOUNT_REGEX.test(value)) {\n return undefined;\n }\n const parsed = Number.parseFloat(value);\n if (!Number.isFinite(parsed) || parsed <= 0) return undefined;\n return value;\n};\n\nexport const clampAmountToMax = ({\n amount,\n maxAmount,\n nexusSDK,\n token,\n chainId,\n}: {\n amount: string;\n maxAmount?: string;\n nexusSDK: NexusSDK;\n token: SUPPORTED_TOKENS;\n chainId: SUPPORTED_CHAINS_IDS;\n}): string => {\n if (!maxAmount) return amount;\n try {\n const amountRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n token,\n chainId,\n );\n const maxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxAmount,\n token,\n chainId,\n );\n return amountRaw > maxRaw ? maxAmount : amount;\n } catch {\n return amount;\n }\n};\n\nexport const formatAmountForDisplay = (\n amount: bigint,\n decimals: number | undefined,\n nexusSDK: NexusSDK,\n): string => {\n if (typeof decimals !== \"number\") return amount.toString();\n const formatted = formatUnits(amount, decimals);\n if (!formatted.includes(\".\")) return formatted;\n const [whole, fraction] = formatted.split(\".\");\n const trimmedFraction = fraction.slice(0, 6).replace(/0+$/, \"\");\n if (!trimmedFraction && whole === \"0\" && amount > BigInt(0)) {\n return \"0.000001\";\n }\n return trimmedFraction ? `${whole}.${trimmedFraction}` : whole;\n};\n\nexport const buildInitialInputs = ({\n type,\n network,\n connectedAddress,\n prefill,\n}: {\n type: \"bridge\" | \"transfer\";\n network: NexusNetwork;\n connectedAddress?: Address;\n prefill?: {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n };\n}) => {\n return {\n chain:\n (prefill?.chainId as SUPPORTED_CHAINS_IDS) ??\n (network === \"testnet\"\n ? SUPPORTED_CHAINS.SEPOLIA\n : SUPPORTED_CHAINS.ETHEREUM),\n token: (prefill?.token as SUPPORTED_TOKENS) ?? \"USDC\",\n amount: prefill?.amount ?? undefined,\n recipient:\n (prefill?.recipient as `0x${string}`) ??\n (type === \"bridge\" ? connectedAddress : undefined),\n };\n};\n\nexport const getCoverageDecimals = ({\n type,\n token,\n chainId,\n fallback,\n}: {\n type: \"bridge\" | \"transfer\";\n token?: SUPPORTED_TOKENS;\n chainId?: SUPPORTED_CHAINS_IDS;\n fallback: number | undefined;\n}) => {\n if (token === \"USDM\") return 18;\n if (\n type === \"bridge\" &&\n token === \"USDC\" &&\n chainId === SUPPORTED_CHAINS.BNB\n ) {\n return 18;\n }\n return fallback;\n};\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type { NexusNetwork } from \"@avail-project/nexus-sdk-v2\";\nimport { formatUnits, type Address } from \"viem\";\n\ntype NexusClient = ReturnType;\n\nconst MAX_AMOUNT_REGEX = /^\\d*\\.?\\d+$/;\n\n// v2 chain IDs for defaults\nconst SEPOLIA_CHAIN_ID = 11155111;\nconst ETHEREUM_CHAIN_ID = 1;\n// v2: BNB chain ID for edge-case decimal override\nconst BNB_CHAIN_ID = 56;\n\nexport const MAX_AMOUNT_DEBOUNCE_MS = 300;\n\nexport const normalizeMaxAmount = (\n maxAmount?: string | number,\n): string | undefined => {\n if (maxAmount === undefined || maxAmount === null) return undefined;\n const value = String(maxAmount).trim();\n if (!value || value === \".\" || !MAX_AMOUNT_REGEX.test(value)) {\n return undefined;\n }\n const parsed = Number.parseFloat(value);\n if (!Number.isFinite(parsed) || parsed <= 0) return undefined;\n return value;\n};\n\nexport const clampAmountToMax = ({\n amount,\n maxAmount,\n nexusSDK,\n token,\n chainId,\n}: {\n amount: string;\n maxAmount?: string;\n nexusSDK: NexusClient;\n token: string;\n chainId: number;\n}): string => {\n if (!maxAmount) return amount;\n try {\n // v2: convertTokenReadableAmountToBigInt(amount, tokenSymbol, chainId)\n const amountRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n token,\n chainId,\n );\n const maxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxAmount,\n token,\n chainId,\n );\n return amountRaw > maxRaw ? maxAmount : amount;\n } catch {\n return amount;\n }\n};\n\nexport const formatAmountForDisplay = (\n amount: bigint,\n decimals: number | undefined,\n // nexusSDK kept for API compatibility but formatUnits is now imported directly\n _nexusSDK: NexusClient,\n): string => {\n if (typeof decimals !== \"number\") return amount.toString();\n const formatted = formatUnits(amount, decimals);\n if (!formatted.includes(\".\")) return formatted;\n const [whole, fraction] = formatted.split(\".\");\n const trimmedFraction = fraction.slice(0, 6).replace(/0+$/, \"\");\n if (!trimmedFraction && whole === \"0\" && amount > BigInt(0)) {\n return \"0.000001\";\n }\n return trimmedFraction ? `${whole}.${trimmedFraction}` : whole;\n};\n\nexport const buildInitialInputs = ({\n type,\n network,\n connectedAddress,\n prefill,\n}: {\n type: \"bridge\" | \"transfer\";\n network: NexusNetwork;\n connectedAddress?: Address;\n prefill?: {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n };\n}) => {\n return {\n // v2 uses plain number chain IDs and string token symbols\n chain:\n prefill?.chainId ??\n (network === \"testnet\" ? SEPOLIA_CHAIN_ID : ETHEREUM_CHAIN_ID),\n token: prefill?.token ?? \"USDC\",\n amount: prefill?.amount ?? undefined,\n recipient:\n (prefill?.recipient as `0x${string}`) ??\n (type === \"bridge\" ? connectedAddress : undefined),\n };\n};\n\nexport const getCoverageDecimals = ({\n type,\n token,\n chainId,\n fallback,\n}: {\n type: \"bridge\" | \"transfer\";\n token?: string;\n chainId?: number;\n fallback: number | undefined;\n}) => {\n if (token === \"USDM\") return 18;\n if (type === \"bridge\" && token === \"USDC\" && chainId === BNB_CHAIN_ID) {\n return 18;\n }\n return fallback;\n};\n", "type": "registry:component", "target": "components/common/utils/transaction-flow.ts" } diff --git a/public/r/unified-balance.json b/public/r/unified-balance.json index 1b2b3f9..0c88ab9 100644 --- a/public/r/unified-balance.json +++ b/public/r/unified-balance.json @@ -5,7 +5,7 @@ "title": "Unified Balance", "description": "A simple component built with Nexus to display unified balance", "dependencies": [ - "@avail-project/nexus-core@1.2.0", + "@avail-project/nexus-sdk-v2", "lucide-react" ], "registryDependencies": [ @@ -19,7 +19,7 @@ "files": [ { "path": "registry/nexus-elements/unified-balance/unified-balance.tsx", - "content": "\"use client\";\nimport React, { memo, useMemo } from \"react\";\nimport { useNexus } from \"../nexus/NexusProvider\";\nimport { Label } from \"../ui/label\";\nimport { DollarSign } from \"lucide-react\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"../ui/accordion\";\nimport { Separator } from \"../ui/separator\";\nimport { cn } from \"@/lib/utils\";\nimport { formatTokenBalance, type UserAsset } from \"@avail-project/nexus-core\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"../ui/tabs\";\n\nconst BalanceBreakdown = ({\n className,\n totalFiat,\n tokens,\n}: {\n totalFiat: string;\n tokens: UserAsset[];\n className?: string;\n}) => {\n return (\n
\n
\n \n\n \n
\n \n {tokens.map((token) => {\n const positiveBreakdown = token.breakdown.filter(\n (chain) => Number.parseFloat(chain.balance) > 0,\n );\n const chainsCount = positiveBreakdown.length;\n const chainsLabel =\n chainsCount > 1 ? `${chainsCount} chains` : `${chainsCount} chain`;\n return (\n \n \n
\n
\n
\n {token.icon && (\n \n )}\n
\n
\n

\n {token.symbol}\n

\n

\n {chainsLabel}\n

\n
\n
\n
\n
\n

\n {formatTokenBalance(token.balance, {\n symbol: token.symbol,\n decimals: token.decimals,\n })}\n

\n

\n ${token.balanceInFiat.toFixed(2)}\n

\n
\n
\n
\n \n \n
\n {positiveBreakdown.map((chain, index) => (\n \n
\n
\n
\n \n
\n \n {chain.chain.name}\n \n
\n
\n

\n {formatTokenBalance(chain.balance, {\n symbol: chain.symbol,\n decimals: chain.decimals,\n })}\n

\n

\n ${chain.balanceInFiat.toFixed(2)}\n

\n
\n
\n {index < positiveBreakdown.length - 1 && (\n \n )}\n
\n ))}\n
\n
\n \n );\n })}\n
\n
\n );\n};\n\nconst UnifiedBalance = ({ className }: { className?: string }) => {\n const { bridgableBalance, swapBalance, nexusSDK } = useNexus();\n\n const totalFiat = useMemo(() => {\n if (!bridgableBalance) return \"0.00\";\n\n return bridgableBalance\n .reduce((acc, fiat) => acc + fiat.balanceInFiat, 0)\n .toFixed(2);\n }, [bridgableBalance]);\n\n const swapTotalFiat = useMemo(() => {\n if (!swapBalance) return \"0.00\";\n return swapBalance\n .reduce((acc, fiat) => acc + fiat.balanceInFiat, 0)\n .toFixed(2);\n }, [swapBalance]);\n\n const tokens = useMemo(\n () =>\n bridgableBalance?.filter(\n (token) => Number.parseFloat(token.balance) > 0,\n ) ?? [],\n [bridgableBalance],\n );\n\n const swapTokens = useMemo(\n () =>\n swapBalance?.filter((token) => Number.parseFloat(token.balance) > 0) ??\n [],\n [swapBalance],\n );\n\n if (!swapBalance) {\n return (\n \n );\n }\n\n return (\n \n \n \n

Bridgeable Balance

\n
\n \n

Swappable Balance

\n
\n
\n \n \n \n \n \n \n \n );\n};\nUnifiedBalance.displayName = \"UnifiedBalance\";\nexport default memo(UnifiedBalance);\n", + "content": "\"use client\";\nimport React, { memo, useMemo } from \"react\";\nimport { useNexus } from \"../nexus/NexusProvider\";\nimport { Label } from \"../ui/label\";\nimport { DollarSign } from \"lucide-react\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"../ui/accordion\";\nimport { Separator } from \"../ui/separator\";\nimport { cn } from \"@/lib/utils\";\nimport { type TokenBalance, type ChainBalance } from \"@avail-project/nexus-sdk-v2\";\nimport { formatTokenBalance } from \"@avail-project/nexus-sdk-v2/utils\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"../ui/tabs\";\n\nconst BalanceBreakdown = ({\n className,\n totalFiat,\n tokens,\n}: {\n totalFiat: string;\n tokens: TokenBalance[];\n className?: string;\n}) => {\n return (\n
\n
\n \n\n \n
\n \n {tokens.map((token) => {\n // v2: chainBalances replaces breakdown\n const positiveBreakdown = token.chainBalances.filter((chain: ChainBalance) => Number.parseFloat(chain.balance) > 0,\n );\n const chainsCount = positiveBreakdown.length;\n const chainsLabel =\n chainsCount > 1 ? `${chainsCount} chains` : `${chainsCount} chain`;\n return (\n \n \n
\n
\n
\n {/* v2: logo replaces icon */}\n {token.logo && (\n \n )}\n
\n
\n

\n {token.symbol}\n

\n

\n {chainsLabel}\n

\n
\n
\n
\n
\n

\n {formatTokenBalance(token.balance, {\n symbol: token.symbol,\n decimals: token.decimals,\n })}\n

\n

\n {/* v2: value is a string USD amount */}\n ${Number.parseFloat(token.value ?? \"0\").toFixed(2)}\n

\n
\n
\n
\n \n \n
\n {positiveBreakdown.map((chain: ChainBalance, index: number) => (\n \n
\n
\n
\n \n
\n \n {chain.chain.name}\n \n
\n
\n

\n {formatTokenBalance(chain.balance, {\n symbol: chain.symbol,\n decimals: chain.decimals,\n })}\n

\n

\n ${Number.parseFloat(chain.value ?? \"0\").toFixed(2)}\n

\n
\n
\n {index < positiveBreakdown.length - 1 && (\n \n )}\n
\n ))}\n
\n
\n \n );\n })}\n
\n
\n );\n};\n\nconst UnifiedBalance = ({ className }: { className?: string }) => {\n const { bridgableBalance, swapBalance, nexusSDK } = useNexus();\n\n const totalFiat = useMemo(() => {\n if (!bridgableBalance) return \"0.00\";\n\n return bridgableBalance\n .reduce((acc: number, fiat: TokenBalance) => acc + Number.parseFloat(fiat.value ?? \"0\"), 0)\n .toFixed(2);\n }, [bridgableBalance]);\n\n const swapTotalFiat = useMemo(() => {\n if (!swapBalance) return \"0.00\";\n return swapBalance\n .reduce((acc: number, fiat: TokenBalance) => acc + Number.parseFloat(fiat.value ?? \"0\"), 0)\n .toFixed(2);\n }, [swapBalance]);\n\n const tokens = useMemo(\n () =>\n bridgableBalance?.filter((token: TokenBalance) => Number.parseFloat(token.balance) > 0,\n ) ?? [],\n [bridgableBalance],\n );\n\n const swapTokens = useMemo(\n () =>\n swapBalance?.filter((token: TokenBalance) => Number.parseFloat(token.balance) > 0) ??\n [],\n [swapBalance],\n );\n\n if (!swapBalance) {\n return (\n \n );\n }\n\n return (\n \n \n \n

Bridgeable Balance

\n
\n \n

Swappable Balance

\n
\n
\n \n \n \n \n \n \n \n );\n};\nUnifiedBalance.displayName = \"UnifiedBalance\";\nexport default memo(UnifiedBalance);\n", "type": "registry:component", "target": "components/unified-balance/unified-balance.tsx" } diff --git a/public/r/view-history.json b/public/r/view-history.json index a0b138a..aa3fe4c 100644 --- a/public/r/view-history.json +++ b/public/r/view-history.json @@ -5,7 +5,7 @@ "title": "View History", "description": "A simple component built with Nexus to display view history", "dependencies": [ - "@avail-project/nexus-core@1.2.0", + "@avail-project/nexus-sdk-v2", "lucide-react" ], "registryDependencies": [ @@ -26,13 +26,13 @@ }, { "path": "registry/nexus-elements/view-history/hooks/useViewHistory.ts", - "content": "import { type RFF } from \"@avail-project/nexus-core\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport { INTENT_HISTORY_REFRESH_EVENT } from \"../history-events\";\n\nconst ITEMS_PER_PAGE = 10;\n\nfunction formatExpiryDate(timestamp: number) {\n const date = new Date(timestamp * 1000);\n const formatted = date.toLocaleString(\"en-US\", {\n month: \"short\",\n day: \"2-digit\",\n year: \"numeric\",\n });\n return formatted.replace(\" \", \", \");\n}\n\nconst useViewHistory = () => {\n const { nexusSDK } = useNexus();\n const [history, setHistory] = useState(null);\n const [loadError, setLoadError] = useState(null);\n const [displayedHistory, setDisplayedHistory] = useState([]);\n const [page, setPage] = useState(0);\n const [hasMore, setHasMore] = useState(true);\n const [isLoadingMore, setIsLoadingMore] = useState(false);\n const [sentinelNode, setSentinelNode] = useState(null);\n\n const observerTarget = useCallback((node: HTMLDivElement | null) => {\n setSentinelNode(node);\n }, []);\n\n const fetchIntentHistory = useCallback(async () => {\n if (!nexusSDK) return;\n try {\n const nextHistory = (await nexusSDK.getMyIntents()) ?? [];\n setLoadError(null);\n setHistory(nextHistory);\n const firstPage = nextHistory.slice(0, ITEMS_PER_PAGE);\n setDisplayedHistory(firstPage);\n setPage(0);\n setHasMore(nextHistory.length > ITEMS_PER_PAGE);\n } catch (error) {\n console.error(\"Error fetching intent history:\", error);\n setLoadError(\"Please check your wallet connection and try again.\");\n setHistory([]);\n setDisplayedHistory([]);\n setPage(0);\n setHasMore(false);\n }\n }, [nexusSDK]);\n\n useEffect(() => {\n void fetchIntentHistory();\n }, [fetchIntentHistory]);\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n\n const handleRefresh = () => {\n void fetchIntentHistory();\n };\n\n window.addEventListener(INTENT_HISTORY_REFRESH_EVENT, handleRefresh);\n return () => {\n window.removeEventListener(INTENT_HISTORY_REFRESH_EVENT, handleRefresh);\n };\n }, [fetchIntentHistory]);\n\n const loadMore = useCallback(() => {\n if (!history || isLoadingMore || !hasMore) return;\n setIsLoadingMore(true);\n\n setTimeout(() => {\n const nextPage = page + 1;\n const startIndex = nextPage * ITEMS_PER_PAGE;\n const endIndex = startIndex + ITEMS_PER_PAGE;\n const newItems = history.slice(startIndex, endIndex);\n\n if (newItems.length > 0) {\n setDisplayedHistory((prev) => [...prev, ...newItems]);\n setPage(nextPage);\n setHasMore(endIndex < history.length);\n } else {\n setHasMore(false);\n }\n\n setIsLoadingMore(false);\n }, 300);\n }, [history, page, isLoadingMore, hasMore]);\n\n useEffect(() => {\n if (!sentinelNode) {\n return;\n }\n\n const rootElement = sentinelNode.parentElement;\n\n const observer = new IntersectionObserver(\n (entries) => {\n if (entries[0].isIntersecting && hasMore && !isLoadingMore) {\n loadMore();\n }\n },\n { threshold: 0.1, root: rootElement ?? null }\n );\n\n observer.observe(sentinelNode);\n\n return () => {\n observer.disconnect();\n };\n }, [sentinelNode, loadMore, hasMore, isLoadingMore, displayedHistory.length]);\n\n const getStatus = (pastIntent: RFF) => {\n if (pastIntent?.fulfilled) {\n return \"Fulfilled\";\n } else if (pastIntent?.deposited) {\n return \"Deposited\";\n } else if (pastIntent?.refunded) {\n return \"Refunded\";\n } else {\n return \"Failed\";\n }\n };\n\n return {\n history,\n loadError,\n displayedHistory,\n page,\n hasMore,\n isLoadingMore,\n getStatus,\n observerTarget,\n refreshHistory: fetchIntentHistory,\n ITEMS_PER_PAGE,\n formatExpiryDate,\n };\n};\n\nexport default useViewHistory;\n", + "content": "import { type IntentRecord } from \"@avail-project/nexus-sdk-v2\";\nimport { useNexus } from \"../../nexus/NexusProvider\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport { INTENT_HISTORY_REFRESH_EVENT } from \"../history-events\";\n\nconst ITEMS_PER_PAGE = 10;\n\nfunction formatExpiryDate(timestamp: number) {\n const date = new Date(timestamp * 1000);\n const formatted = date.toLocaleString(\"en-US\", {\n month: \"short\",\n day: \"2-digit\",\n year: \"numeric\",\n });\n return formatted.replace(\" \", \", \");\n}\n\nconst useViewHistory = () => {\n const { nexusSDK } = useNexus();\n const [history, setHistory] = useState(null);\n const [loadError, setLoadError] = useState(null);\n const [displayedHistory, setDisplayedHistory] = useState([]);\n const [page, setPage] = useState(0);\n const [hasMore, setHasMore] = useState(true);\n const [isLoadingMore, setIsLoadingMore] = useState(false);\n const [sentinelNode, setSentinelNode] = useState(null);\n\n const observerTarget = useCallback((node: HTMLDivElement | null) => {\n setSentinelNode(node);\n }, []);\n\n const fetchIntentHistory = useCallback(async () => {\n if (!nexusSDK) return;\n try {\n // v2: listIntents() replaces getMyIntents(); returns { intents, total }\n const { intents: nextHistory } = await nexusSDK.listIntents({ page: 1 });\n setLoadError(null);\n setHistory(nextHistory);\n const firstPage = nextHistory.slice(0, ITEMS_PER_PAGE);\n setDisplayedHistory(firstPage);\n setPage(0);\n setHasMore(nextHistory.length > ITEMS_PER_PAGE);\n } catch (error) {\n console.error(\"Error fetching intent history:\", error);\n setLoadError(\"Please check your wallet connection and try again.\");\n setHistory([]);\n setDisplayedHistory([]);\n setPage(0);\n setHasMore(false);\n }\n }, [nexusSDK]);\n\n useEffect(() => {\n void fetchIntentHistory();\n }, [fetchIntentHistory]);\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n\n const handleRefresh = () => {\n void fetchIntentHistory();\n };\n\n window.addEventListener(INTENT_HISTORY_REFRESH_EVENT, handleRefresh);\n return () => {\n window.removeEventListener(INTENT_HISTORY_REFRESH_EVENT, handleRefresh);\n };\n }, [fetchIntentHistory]);\n\n const loadMore = useCallback(() => {\n if (!history || isLoadingMore || !hasMore) return;\n setIsLoadingMore(true);\n\n setTimeout(() => {\n const nextPage = page + 1;\n const startIndex = nextPage * ITEMS_PER_PAGE;\n const endIndex = startIndex + ITEMS_PER_PAGE;\n const newItems = history.slice(startIndex, endIndex);\n\n if (newItems.length > 0) {\n setDisplayedHistory((prev) => [...prev, ...newItems]);\n setPage(nextPage);\n setHasMore(endIndex < history.length);\n } else {\n setHasMore(false);\n }\n\n setIsLoadingMore(false);\n }, 300);\n }, [history, page, isLoadingMore, hasMore]);\n\n useEffect(() => {\n if (!sentinelNode) {\n return;\n }\n\n const rootElement = sentinelNode.parentElement;\n\n const observer = new IntersectionObserver(\n (entries) => {\n if (entries[0].isIntersecting && hasMore && !isLoadingMore) {\n loadMore();\n }\n },\n { threshold: 0.1, root: rootElement ?? null }\n );\n\n observer.observe(sentinelNode);\n\n return () => {\n observer.disconnect();\n };\n }, [sentinelNode, loadMore, hasMore, isLoadingMore, displayedHistory.length]);\n\n const getStatus = (pastIntent: IntentRecord) => {\n // v2: status is a string field, not boolean flags\n const s = pastIntent?.status;\n if (s === \"fulfilled\") return \"Fulfilled\";\n if (s === \"deposited\") return \"Deposited\";\n if (s === \"expired\") return \"Expired\";\n return \"Created\";\n };\n\n return {\n history,\n loadError,\n displayedHistory,\n page,\n hasMore,\n isLoadingMore,\n getStatus,\n observerTarget,\n refreshHistory: fetchIntentHistory,\n ITEMS_PER_PAGE,\n formatExpiryDate,\n };\n};\n\nexport default useViewHistory;\n", "type": "registry:component", "target": "components/view-history/hooks/useViewHistory.ts" }, { "path": "registry/nexus-elements/view-history/view-history.tsx", - "content": "\"use client\";\n\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTrigger,\n DialogTitle,\n} from \"@/registry/nexus-elements/ui/dialog\";\nimport { Clock, LoaderPinwheel, SquareArrowOutUpRight } from \"lucide-react\";\nimport { TOKEN_METADATA, type RFF } from \"@avail-project/nexus-core\";\nimport { cn } from \"@/lib/utils\";\nimport { Badge } from \"@/registry/nexus-elements/ui/badge\";\nimport { Button } from \"@/registry/nexus-elements/ui/button\";\nimport { Card } from \"@/registry/nexus-elements/ui/card\";\nimport { Separator } from \"@/registry/nexus-elements/ui/separator\";\nimport useViewHistory from \"./hooks/useViewHistory\";\nimport { useEffect, useState } from \"react\";\n\nconst TOKEN_ICON_FALLBACKS: Record = {\n USDM:\n \"https://raw.githubusercontent.com/availproject/nexus-assets/main/tokens/usdm/logo.png\",\n};\n\nfunction resolveTokenMetadata(symbol?: string) {\n const normalized = symbol?.trim() ?? \"\";\n if (!normalized) {\n return { icon: \"\", name: \"token\" };\n }\n\n const upper = normalized.toUpperCase();\n const normalizedMetadata =\n TOKEN_METADATA[normalized as keyof typeof TOKEN_METADATA];\n const upperMetadata = TOKEN_METADATA[upper as keyof typeof TOKEN_METADATA];\n\n const icon =\n normalizedMetadata?.icon ||\n upperMetadata?.icon ||\n TOKEN_ICON_FALLBACKS[normalized] ||\n TOKEN_ICON_FALLBACKS[upper] ||\n \"\";\n const name = normalizedMetadata?.name || upperMetadata?.name || upper;\n\n return { icon, name };\n}\n\nconst SourceChains = ({ sources }: { sources: RFF[\"sources\"] }) => {\n const sourceList = sources ?? [];\n return (\n
\n {sourceList.map((source, index) => (\n 0 && \"-ml-2\"\n )}\n style={{ zIndex: sourceList.length - index }}\n >\n \n
\n ))}\n \n );\n};\n\nconst StatusBadge = ({ status }: { status: string }) => {\n const getVariant = (status: string) => {\n if (status === \"Fulfilled\") {\n return \"default\";\n } else if (status === \"Deposited\") {\n return \"secondary\";\n } else if (status === \"Refunded\") {\n return \"outline\";\n } else if (status === \"Failed\") {\n return \"destructive\";\n } else {\n return \"default\";\n }\n };\n\n return (\n \n

{status}

\n
\n );\n};\n\nconst DestinationToken = ({\n destination,\n}: {\n destination: RFF[\"destinations\"];\n}) => {\n return (\n
\n {destination.map((dest, index) => {\n const tokenMeta = resolveTokenMetadata(dest.token.symbol);\n return (\n 0 && \"-ml-2\"\n )}\n style={{ zIndex: destination.length - index }}\n >\n \n
\n );\n })}\n \n );\n};\n\nconst ViewHistory = ({\n viewAsModal = true,\n className,\n}: {\n viewAsModal?: boolean;\n className?: string;\n}) => {\n const [isOpen, setIsOpen] = useState(false);\n const {\n history,\n loadError,\n displayedHistory,\n hasMore,\n isLoadingMore,\n getStatus,\n observerTarget,\n refreshHistory,\n ITEMS_PER_PAGE,\n formatExpiryDate,\n } = useViewHistory();\n\n useEffect(() => {\n if (!viewAsModal || !isOpen) return;\n void refreshHistory();\n }, [isOpen, refreshHistory, viewAsModal]);\n\n const renderHistoryContent = () => {\n if (displayedHistory.length > 0) {\n return (\n <>\n {displayedHistory?.map((pastIntent) => (\n \n
\n
\n \n
\n

\n {pastIntent?.destinations\n .map((d) => d?.token?.symbol)\n .join(\", \")}\n

\n

\n Intent #{pastIntent?.id}\n

\n
\n
\n \n
\n\n \n\n
\n
\n \n
\n
\n \n
\n
\n
\n \n
\n
\n\n
\n
\n

Expiry

\n

\n {formatExpiryDate(pastIntent?.expiry)}\n

\n
\n \n \n \n
\n
\n \n ))}\n\n {hasMore && (\n
\n {isLoadingMore && (\n
\n \n Loading more...\n
\n )}\n
\n )}\n\n {!hasMore && displayedHistory?.length > ITEMS_PER_PAGE && (\n
\n

\n No more transactions to load\n

\n
\n )}\n \n );\n }\n\n if (history === null) {\n return (\n
\n
\n
\n \n
\n
\n

Loading your history

\n

\n Fetching your past transactions...\n

\n
\n
\n );\n }\n\n if (loadError) {\n return (\n
\n \n
\n

Unable to load history

\n

{loadError}

\n
\n {\n void refreshHistory();\n }}\n >\n Retry\n \n
\n );\n }\n\n return (\n
\n \n
\n

No history yet

\n

\n Your transaction history will appear here\n

\n
\n
\n );\n };\n\n if (!viewAsModal) {\n return (\n
\n {renderHistoryContent()}\n
\n );\n }\n\n return (\n \n \n \n \n \n \n \n \n \n Transaction History\n \n \n
\n {renderHistoryContent()}\n
\n
\n
\n );\n};\n\nexport default ViewHistory;\n", + "content": "\"use client\";\n\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTrigger,\n DialogTitle,\n} from \"@/registry/nexus-elements/ui/dialog\";\nimport { Clock, LoaderPinwheel, SquareArrowOutUpRight } from \"lucide-react\";\nimport { type IntentRecord } from \"@avail-project/nexus-sdk-v2\";\nimport { cn } from \"@/lib/utils\";\nimport { Badge } from \"@/registry/nexus-elements/ui/badge\";\nimport { Button } from \"@/registry/nexus-elements/ui/button\";\nimport { Card } from \"@/registry/nexus-elements/ui/card\";\nimport { Separator } from \"@/registry/nexus-elements/ui/separator\";\nimport useViewHistory from \"./hooks/useViewHistory\";\nimport { useEffect, useState } from \"react\";\n\n// v2: IntentRecord.sources[].chain.logo directly available\nconst SourceChains = ({ sources }: { sources: IntentRecord[\"sources\"] }) => {\n const sourceList = sources ?? [];\n return (\n
\n {sourceList.map((source, index) => (\n 0 && \"-ml-2\"\n )}\n style={{ zIndex: sourceList.length - index }}\n >\n \n
\n ))}\n
\n );\n};\n\nconst StatusBadge = ({ status }: { status: string }) => {\n const getVariant = (status: string) => {\n if (status === \"Fulfilled\") {\n return \"default\";\n } else if (status === \"Deposited\") {\n return \"secondary\";\n } else if (status === \"Refunded\") {\n return \"outline\";\n } else if (status === \"Failed\") {\n return \"destructive\";\n } else {\n return \"default\";\n }\n };\n\n return (\n \n

{status}

\n
\n );\n};\n\n// v2: IntentRecord.destinations[].token.logo is directly available\nconst DestinationToken = ({\n destination,\n}: {\n destination: IntentRecord[\"destinations\"];\n}) => {\n const destList = destination ?? [];\n return (\n
\n {destList.map((dest, index) => {\n return (\n 0 && \"-ml-2\"\n )}\n style={{ zIndex: destList.length - index }}\n >\n \n
\n );\n })}\n
\n );\n};\n\nconst ViewHistory = ({\n viewAsModal = true,\n className,\n}: {\n viewAsModal?: boolean;\n className?: string;\n}) => {\n const [isOpen, setIsOpen] = useState(false);\n const {\n history,\n loadError,\n displayedHistory,\n hasMore,\n isLoadingMore,\n getStatus,\n observerTarget,\n refreshHistory,\n ITEMS_PER_PAGE,\n formatExpiryDate,\n } = useViewHistory();\n\n useEffect(() => {\n if (!viewAsModal || !isOpen) return;\n void refreshHistory();\n }, [isOpen, refreshHistory, viewAsModal]);\n\n const renderHistoryContent = () => {\n if (displayedHistory.length > 0) {\n return (\n <>\n {displayedHistory?.map((pastIntent) => (\n \n
\n
\n \n
\n

\n {(pastIntent?.destinations ?? [])\n .map((d) => d?.token?.symbol)\n .join(\", \")}\n

\n

\n Intent #{pastIntent?.requestHash?.slice(0, 10)}...\n

\n
\n
\n \n
\n\n \n\n
\n
\n \n
\n
\n \n
\n
\n
\n \n
\n
\n\n
\n
\n

Expiry

\n

\n {formatExpiryDate(pastIntent?.expiry)}\n

\n
\n \n \n \n
\n
\n \n ))}\n\n {hasMore && (\n
\n {isLoadingMore && (\n
\n \n Loading more...\n
\n )}\n
\n )}\n\n {!hasMore && displayedHistory?.length > ITEMS_PER_PAGE && (\n
\n

\n No more transactions to load\n

\n
\n )}\n \n );\n }\n\n if (history === null) {\n return (\n
\n
\n
\n \n
\n
\n

Loading your history

\n

\n Fetching your past transactions...\n

\n
\n
\n );\n }\n\n if (loadError) {\n return (\n
\n \n
\n

Unable to load history

\n

{loadError}

\n
\n {\n void refreshHistory();\n }}\n >\n Retry\n \n
\n );\n }\n\n return (\n
\n \n
\n

No history yet

\n

\n Your transaction history will appear here\n

\n
\n
\n );\n };\n\n if (!viewAsModal) {\n return (\n
\n {renderHistoryContent()}\n
\n );\n }\n\n return (\n \n \n \n \n \n \n \n \n \n Transaction History\n \n \n
\n {renderHistoryContent()}\n
\n
\n
\n );\n};\n\nexport default ViewHistory;\n", "type": "registry:component", "target": "components/view-history/view-history.tsx" }, @@ -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-sdk-v2\";\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.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.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.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 // v2: RFF_FEE_EXPIRED was removed; use string key for forward compat\n [\"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 );\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 as unknown as { data?: { context?: unknown } })?.data?.context,\n details: (err as unknown as { data?: { details?: unknown } })?.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" }, @@ -92,13 +92,13 @@ }, { "path": "registry/nexus-elements/common/hooks/useTransactionExecution.ts", - "content": "import {\n type BridgeStepType,\n NEXUS_EVENTS,\n type NexusSDK,\n type OnAllowanceHookData,\n type OnIntentHookData,\n} from \"@avail-project/nexus-core\";\nimport {\n type Dispatch,\n type RefObject,\n type SetStateAction,\n useCallback,\n useRef,\n} from \"react\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport {\n type SourceSelectionValidation,\n type TransactionFlowEvent,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n} from \"../types/transaction-flow\";\n\ninterface NexusErrorInfo {\n code: string;\n message: string;\n context?: unknown;\n details?: unknown;\n}\n\ntype NexusErrorHandler = (error: unknown) => NexusErrorInfo;\n\ninterface UseTransactionExecutionProps {\n operationName: \"bridge\" | \"transfer\";\n nexusSDK: NexusSDK | null;\n intent: RefObject;\n allowance: RefObject;\n inputs: TransactionFlowInputs;\n configuredMaxAmount?: string;\n allAvailableSourceChainIds: number[];\n sourceChainsForSdk?: number[];\n sourceSelectionKey: string;\n sourceSelection: SourceSelectionValidation;\n loading: boolean;\n txError: string | null;\n areInputsValid: boolean;\n executeTransaction: TransactionFlowExecutor;\n getMaxForCurrentSelection: () => Promise;\n onStepsList: (steps: BridgeStepType[]) => void;\n onStepComplete: (step: BridgeStepType) => void;\n resetSteps: () => void;\n setStatus: (status: TransactionStatus) => void;\n resetInputs: () => void;\n setRefreshing: Dispatch>;\n setIsDialogOpen: Dispatch>;\n setTxError: Dispatch>;\n setLastExplorerUrl: Dispatch>;\n setSelectedSourceChains: Dispatch>;\n setAppliedSourceSelectionKey: Dispatch>;\n stopwatch: {\n start: () => void;\n stop: () => void;\n reset: () => void;\n };\n handleNexusError: NexusErrorHandler;\n onStart?: () => void;\n onComplete?: (explorerUrl?: string) => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n notifyHistoryRefresh?: () => void;\n}\n\nexport function useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n}: UseTransactionExecutionProps) {\n const commitLockRef = useRef(false);\n const runIdRef = useRef(0);\n\n const refreshIntent = async (options?: { reportError?: boolean }) => {\n if (!intent.current) return false;\n const activeRunId = runIdRef.current;\n setRefreshing(true);\n try {\n const updated = await intent.current.refresh(sourceChainsForSdk);\n if (activeRunId !== runIdRef.current) return false;\n if (updated) {\n intent.current.intent = updated;\n }\n setAppliedSourceSelectionKey(sourceSelectionKey);\n return true;\n } catch (error) {\n if (activeRunId !== runIdRef.current) return false;\n console.error(\"Transaction failed:\", error);\n if (options?.reportError) {\n const message = \"Unable to refresh source selection. Please try again.\";\n setTxError(message);\n onError?.(message);\n }\n return false;\n } finally {\n if (activeRunId === runIdRef.current) {\n setRefreshing(false);\n }\n }\n };\n\n const onSuccess = async (explorerUrl?: string) => {\n stopwatch.stop();\n setStatus(\"success\");\n onComplete?.(explorerUrl);\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n await fetchBalance();\n notifyHistoryRefresh?.();\n };\n\n const handleTransaction = async () => {\n if (commitLockRef.current) return;\n commitLockRef.current = true;\n const currentRunId = ++runIdRef.current;\n let didEnterExecutingState = false;\n const cleanupSupersededExecution = () => {\n if (!didEnterExecutingState) return;\n setRefreshing(false);\n setIsDialogOpen(false);\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n setStatus(\"idle\");\n };\n\n try {\n if (\n !inputs?.amount ||\n !inputs?.recipient ||\n !inputs?.chain ||\n !inputs?.token\n ) {\n console.error(\"Missing required inputs\");\n return;\n }\n if (!nexusSDK) {\n const message = \"Nexus SDK not initialized\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n const parsedAmount = Number(inputs.amount);\n if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) {\n const message = \"Enter a valid amount greater than 0.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n const amountBigInt = nexusSDK.convertTokenReadableAmountToBigInt(\n inputs.amount,\n inputs.token,\n inputs.chain,\n );\n\n if (configuredMaxAmount) {\n const configuredMaxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n configuredMaxAmount,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > configuredMaxRaw) {\n const message = `Amount exceeds maximum limit of ${configuredMaxAmount} ${inputs.token}.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n }\n\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (currentRunId !== runIdRef.current) return;\n if (!maxForCurrentSelection) {\n const message = `Unable to determine max ${operationName} amount for selected sources. Please try again.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n const maxForSelectionRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxForCurrentSelection,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > maxForSelectionRaw) {\n const message = `Selected sources can provide up to ${maxForCurrentSelection} ${inputs.token}. Reduce amount or enable more sources.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n setStatus(\"executing\");\n didEnterExecutingState = true;\n setTxError(null);\n onStart?.();\n setLastExplorerUrl(\"\");\n setAppliedSourceSelectionKey(sourceSelectionKey);\n\n const onEvent = (event: TransactionFlowEvent) => {\n if (currentRunId !== runIdRef.current) return;\n if (event.name === NEXUS_EVENTS.STEPS_LIST) {\n const list = Array.isArray(event.args) ? event.args : [];\n onStepsList(list as BridgeStepType[]);\n }\n if (event.name === NEXUS_EVENTS.STEP_COMPLETE) {\n if (\n !Array.isArray(event.args) &&\n \"type\" in event.args &&\n event.args.type === \"INTENT_HASH_SIGNED\"\n ) {\n stopwatch.start();\n }\n if (!Array.isArray(event.args)) {\n onStepComplete(event.args as BridgeStepType);\n }\n }\n };\n\n const transactionResult = await executeTransaction({\n token: inputs.token,\n amount: amountBigInt,\n toChainId: inputs.chain,\n recipient: inputs.recipient,\n sourceChains: sourceChainsForSdk,\n onEvent,\n });\n\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution();\n return;\n }\n if (!transactionResult) {\n throw new Error(\"Transaction rejected by user\");\n }\n setLastExplorerUrl(transactionResult.explorerUrl);\n await onSuccess(transactionResult.explorerUrl);\n } catch (error) {\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution();\n return;\n }\n const { message, code, context, details } = handleNexusError(error);\n console.error(`Fast ${operationName} transaction failed:`, {\n code,\n message,\n context,\n details,\n });\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n setTxError(message);\n onError?.(message);\n setIsDialogOpen(false);\n setSelectedSourceChains(null);\n setRefreshing(false);\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n void fetchBalance();\n setStatus(\"error\");\n } finally {\n commitLockRef.current = false;\n }\n };\n\n const reset = () => {\n runIdRef.current += 1;\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setStatus(\"idle\");\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n };\n\n const startTransaction = () => {\n if (!intent.current) return;\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (sourceSelection.isBelowRequired && inputs?.token) {\n const message = `Selected sources are not enough. Add ${sourceSelection.missingToProceed} ${inputs.token} more to make this transaction.`;\n setTxError(message);\n onError?.(message);\n return;\n }\n void (async () => {\n const refreshed = await refreshIntent({ reportError: true });\n if (!refreshed || !intent.current) return;\n intent.current.allow();\n setIsDialogOpen(true);\n setTxError(null);\n })();\n };\n\n const commitAmount = async () => {\n if (intent.current || loading || txError || !areInputsValid) return;\n await handleTransaction();\n };\n\n const invalidatePendingExecution = useCallback(() => {\n runIdRef.current += 1;\n if (intent.current) {\n intent.current.deny();\n intent.current = null;\n }\n setRefreshing(false);\n setAppliedSourceSelectionKey(\"ALL\");\n }, [intent, setAppliedSourceSelectionKey, setRefreshing]);\n\n return {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n };\n}\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type { OnAllowanceHookData, OnIntentHookData } from \"@avail-project/nexus-sdk-v2\";\nimport {\n type Dispatch,\n type RefObject,\n type SetStateAction,\n useCallback,\n useRef,\n} from \"react\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport {\n type SourceSelectionValidation,\n type TransactionFlowEvent,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n} from \"../types/transaction-flow\";\n\ntype NexusClient = ReturnType;\n\ninterface NexusErrorInfo {\n code: string;\n message: string;\n context?: unknown;\n details?: unknown;\n}\n\ntype NexusErrorHandler = (error: unknown) => NexusErrorInfo;\n\n// v2 plan_progress step types for bridge\nconst BRIDGE_STEP_INTENT_SIGNED = \"request_signing\";\n\ninterface UseTransactionExecutionProps {\n operationName: \"bridge\" | \"transfer\";\n nexusSDK: NexusClient | null;\n intent: RefObject;\n allowance: RefObject;\n inputs: TransactionFlowInputs;\n configuredMaxAmount?: string;\n allAvailableSourceChainIds: number[];\n sourceChainsForSdk?: number[];\n sourceSelectionKey: string;\n sourceSelection: SourceSelectionValidation;\n loading: boolean;\n txError: string | null;\n areInputsValid: boolean;\n executeTransaction: TransactionFlowExecutor;\n getMaxForCurrentSelection: () => Promise;\n onStepsList: (steps: { typeID?: string; type?: string; [key: string]: unknown }[]) => void;\n onStepComplete: (step: { typeID?: string; type?: string; [key: string]: unknown }) => void;\n resetSteps: () => void;\n setStatus: (status: TransactionStatus) => void;\n resetInputs: () => void;\n setRefreshing: Dispatch>;\n setIsDialogOpen: Dispatch>;\n setTxError: Dispatch>;\n setLastExplorerUrl: Dispatch>;\n setSelectedSourceChains: Dispatch>;\n setAppliedSourceSelectionKey: Dispatch>;\n stopwatch: {\n start: () => void;\n stop: () => void;\n reset: () => void;\n };\n handleNexusError: NexusErrorHandler;\n onStart?: () => void;\n onComplete?: (explorerUrl?: string) => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n notifyHistoryRefresh?: () => void;\n}\n\nexport function useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n}: UseTransactionExecutionProps) {\n const commitLockRef = useRef(false);\n const runIdRef = useRef(0);\n\n const refreshIntent = async (options?: { reportError?: boolean }) => {\n if (!intent.current) return false;\n const activeRunId = runIdRef.current;\n setRefreshing(true);\n try {\n const updated = await intent.current.refresh(sourceChainsForSdk);\n if (activeRunId !== runIdRef.current) return false;\n if (updated) {\n intent.current.intent = updated;\n }\n setAppliedSourceSelectionKey(sourceSelectionKey);\n return true;\n } catch (error) {\n if (activeRunId !== runIdRef.current) return false;\n console.error(\"Transaction failed:\", error);\n if (options?.reportError) {\n const message = \"Unable to refresh source selection. Please try again.\";\n setTxError(message);\n onError?.(message);\n }\n return false;\n } finally {\n if (activeRunId === runIdRef.current) {\n setRefreshing(false);\n }\n }\n };\n\n const onSuccess = async (explorerUrl?: string) => {\n stopwatch.stop();\n setStatus(\"success\");\n onComplete?.(explorerUrl);\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n await fetchBalance();\n notifyHistoryRefresh?.();\n };\n\n const handleTransaction = async () => {\n if (commitLockRef.current) return;\n commitLockRef.current = true;\n const currentRunId = ++runIdRef.current;\n let didEnterExecutingState = false;\n // Declared here (outside try/catch) so both the event handler and the catch block\n // can read/write it — prevents the catch from clobbering event-driven completions\n let completedFromEvent = false;\n const cleanupSupersededExecution = () => {\n if (!didEnterExecutingState) return;\n // Don't tear down the dialog if an event already handled success/failure —\n // resetInputs() inside onSuccess triggers invalidatePendingExecution which\n // increments runIdRef, making this branch fire spuriously.\n if (completedFromEvent) return;\n setRefreshing(false);\n setIsDialogOpen(false);\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n setStatus(\"idle\");\n };\n\n\n try {\n if (\n !inputs?.amount ||\n !inputs?.recipient ||\n !inputs?.chain ||\n !inputs?.token\n ) {\n console.error(\"Missing required inputs\");\n return;\n }\n if (!nexusSDK) {\n const message = \"Nexus SDK not initialized\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n const parsedAmount = Number(inputs.amount);\n if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) {\n const message = \"Enter a valid amount greater than 0.\";\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n // v2: convertTokenReadableAmountToBigInt(amount, tokenSymbol, chainId)\n const amountBigInt = nexusSDK.convertTokenReadableAmountToBigInt(\n inputs.amount,\n inputs.token,\n inputs.chain,\n );\n\n if (configuredMaxAmount) {\n const configuredMaxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n configuredMaxAmount,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > configuredMaxRaw) {\n const message = `Amount exceeds maximum limit of ${configuredMaxAmount} ${inputs.token}.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n }\n\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (currentRunId !== runIdRef.current) return;\n if (!maxForCurrentSelection) {\n const message = `Unable to determine max ${operationName} amount for selected sources. Please try again.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n const maxForSelectionRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxForCurrentSelection,\n inputs.token,\n inputs.chain,\n );\n if (amountBigInt > maxForSelectionRaw) {\n const message = `Selected sources can provide up to ${maxForCurrentSelection} ${inputs.token}. Reduce amount or enable more sources.`;\n setTxError(message);\n onError?.(message);\n setStatus(\"error\");\n return;\n }\n\n setStatus(\"executing\");\n didEnterExecutingState = true;\n setTxError(null);\n onStart?.();\n setLastExplorerUrl(\"\");\n setAppliedSourceSelectionKey(sourceSelectionKey);\n\n // Terminal step types — when state:\"completed\" fires on these, the operation is done\n const TERMINAL_STEP_TYPES = new Set([\n \"bridge_fill\", // bridge & transfer final fill\n \"destination_swap\", // swap final step\n ]);\n\n // v2 onEvent uses typed discriminated union: { type, ... }\n const onEvent = (event: TransactionFlowEvent) => {\n if (currentRunId !== runIdRef.current) return;\n\n if (event.type === \"plan_preview\") {\n // Seed UI with the step list from the plan\n type StepShape = { typeID?: string; type?: string; [key: string]: unknown };\n const steps = ((event as { type: string; plan: { steps: StepShape[] } }).plan?.steps ?? []) as StepShape[];\n onStepsList(steps);\n }\n\n if (event.type === \"plan_progress\") {\n const progressEvent = event as {\n type: string;\n stepType: string;\n state: string;\n step: { typeID?: string; type?: string; [key: string]: unknown };\n error?: string;\n };\n\n // Always mark step as complete/updated in UI\n onStepComplete(progressEvent.step);\n\n const isTerminal = TERMINAL_STEP_TYPES.has(progressEvent.stepType);\n\n if (progressEvent.state === \"failed\") {\n // Any step failure → abort\n if (!completedFromEvent) {\n completedFromEvent = true;\n const errorMessage = progressEvent.error ?? \"Transaction failed\";\n stopwatch.stop();\n setTxError(errorMessage);\n onError?.(errorMessage);\n setStatus(\"error\");\n }\n return;\n }\n\n if (isTerminal && progressEvent.state === \"completed\") {\n // Terminal step completed → success\n if (!completedFromEvent) {\n completedFromEvent = true;\n stopwatch.stop();\n // explorerUrl is on the event itself, not the step object\n const explorerUrl = (event as { explorerUrl?: string }).explorerUrl;\n if (explorerUrl) setLastExplorerUrl(explorerUrl);\n void onSuccess(explorerUrl);\n }\n }\n }\n\n if (event.type === \"status\") {\n const statusEvent = event as { type: string; status: string };\n if (statusEvent.status === \"completed\" && !completedFromEvent) {\n completedFromEvent = true;\n stopwatch.stop();\n void onSuccess(undefined);\n }\n }\n };\n\n const transactionResult = await executeTransaction({\n token: inputs.token,\n amount: amountBigInt,\n toChainId: inputs.chain,\n recipient: inputs.recipient,\n sources: sourceChainsForSdk,\n onEvent,\n });\n\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution(); // no-op when completedFromEvent=true\n if (!completedFromEvent) return; // only bail if not already completed\n // else fall through — still want to capture explorerUrl from the result\n }\n if (!transactionResult) {\n if (!completedFromEvent) {\n throw new Error(\"Transaction rejected by user\");\n }\n // Already handled via events\n return;\n }\n\n // SDK promise resolved — use result for explorerUrl if event-driven success didn't set it\n if (!completedFromEvent) {\n // Fallback: SDK resolved but we never got a terminal event (e.g. single-step flows)\n setLastExplorerUrl(transactionResult.explorerUrl ?? \"\");\n await onSuccess(transactionResult.explorerUrl);\n } else {\n // Event-driven success already ran — capture the explorerUrl from the resolved result\n if (transactionResult.explorerUrl) {\n setLastExplorerUrl(transactionResult.explorerUrl);\n }\n }\n\n } catch (error) {\n if (currentRunId !== runIdRef.current) {\n cleanupSupersededExecution();\n return;\n }\n // If event-driven success/failure already handled this transaction, ignore SDK-level errors\n // (the SDK may throw or return oddly after a successful fill event)\n if (completedFromEvent) return;\n const { message, code, context, details } = handleNexusError(error);\n console.error(`Fast ${operationName} transaction failed:`, {\n code,\n message,\n context,\n details,\n });\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n setTxError(message);\n onError?.(message);\n setIsDialogOpen(false);\n setSelectedSourceChains(null);\n setRefreshing(false);\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n void fetchBalance();\n setStatus(\"error\");\n } finally {\n commitLockRef.current = false;\n }\n };\n\n const reset = () => {\n runIdRef.current += 1;\n intent.current?.deny();\n intent.current = null;\n allowance.current = null;\n resetInputs();\n setStatus(\"idle\");\n setRefreshing(false);\n setSelectedSourceChains(null);\n setAppliedSourceSelectionKey(\"ALL\");\n setLastExplorerUrl(\"\");\n stopwatch.stop();\n stopwatch.reset();\n resetSteps();\n };\n\n const startTransaction = () => {\n if (!intent.current) return;\n if (allAvailableSourceChainIds.length === 0) {\n const message =\n \"No eligible source chains available for the selected token and destination.\";\n setTxError(message);\n onError?.(message);\n return;\n }\n if (sourceSelection.isBelowRequired && inputs?.token) {\n const message = `Selected sources are not enough. Add ${sourceSelection.missingToProceed} ${inputs.token} more to make this transaction.`;\n setTxError(message);\n onError?.(message);\n return;\n }\n void (async () => {\n const refreshed = await refreshIntent({ reportError: true });\n if (!refreshed || !intent.current) return;\n intent.current.allow();\n setIsDialogOpen(true);\n // Start the stopwatch AFTER the dialog opens so the isDialogOpen effect\n // does not immediately reset it (the effect only resets when dialog is closed)\n stopwatch.reset();\n stopwatch.start();\n setTxError(null);\n })();\n };\n\n const commitAmount = async () => {\n if (intent.current || loading || txError || !areInputsValid) return;\n await handleTransaction();\n };\n\n const invalidatePendingExecution = useCallback(() => {\n runIdRef.current += 1;\n if (intent.current) {\n intent.current.deny();\n intent.current = null;\n }\n setRefreshing(false);\n setAppliedSourceSelectionKey(\"ALL\");\n }, [intent, setAppliedSourceSelectionKey, setRefreshing]);\n\n return {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n };\n}\n", "type": "registry:component", "target": "components/common/hooks/useTransactionExecution.ts" }, { "path": "registry/nexus-elements/common/hooks/useTransactionFlow.ts", - "content": "import {\n type BridgeStepType,\n type NexusNetwork,\n NexusSDK,\n type OnAllowanceHookData,\n type OnIntentHookData,\n parseUnits,\n type UserAsset,\n} from \"@avail-project/nexus-core\";\nimport {\n useEffect,\n useMemo,\n useCallback,\n useRef,\n useState,\n useReducer,\n type RefObject,\n} from \"react\";\nimport { type Address, isAddress } from \"viem\";\nimport { useNexusError } from \"./useNexusError\";\nimport { useTransactionExecution } from \"./useTransactionExecution\";\nimport { usePolling } from \"./usePolling\";\nimport { useStopwatch } from \"./useStopwatch\";\nimport { useDebouncedCallback } from \"./useDebouncedCallback\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport { useTransactionSteps } from \"../tx/useTransactionSteps\";\nimport {\n type SourceCoverageState,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n type TransactionFlowPrefill,\n type TransactionFlowType,\n} from \"../types/transaction-flow\";\nimport {\n MAX_AMOUNT_DEBOUNCE_MS,\n buildInitialInputs,\n clampAmountToMax,\n formatAmountForDisplay,\n getCoverageDecimals,\n normalizeMaxAmount,\n} from \"../utils/transaction-flow\";\n\ninterface BaseTransactionFlowProps {\n type: TransactionFlowType;\n network: NexusNetwork;\n nexusSDK: NexusSDK | null;\n intent: RefObject;\n allowance: RefObject;\n bridgableBalance: UserAsset[] | null;\n prefill?: TransactionFlowPrefill;\n onComplete?: (explorerUrl?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n maxAmount?: string | number;\n isSourceMenuOpen?: boolean;\n notifyHistoryRefresh?: () => void;\n executeTransaction: TransactionFlowExecutor;\n}\n\nexport interface UseTransactionFlowProps extends BaseTransactionFlowProps {\n connectedAddress?: Address;\n}\n\ntype State = {\n inputs: TransactionFlowInputs;\n status: TransactionStatus;\n};\n\ntype Action =\n | { type: \"setInputs\"; payload: Partial }\n | { type: \"resetInputs\" }\n | { type: \"setStatus\"; payload: TransactionStatus };\n\nexport function useTransactionFlow(props: UseTransactionFlowProps) {\n const {\n type,\n network,\n nexusSDK,\n intent,\n bridgableBalance,\n prefill,\n onComplete,\n onStart,\n onError,\n fetchBalance,\n allowance,\n maxAmount,\n isSourceMenuOpen = false,\n notifyHistoryRefresh,\n executeTransaction,\n } = props;\n\n const connectedAddress = props.connectedAddress;\n const operationName = type === \"bridge\" ? \"bridge\" : \"transfer\";\n const handleNexusError = useNexusError();\n const initialState: State = {\n inputs: buildInitialInputs({ type, network, connectedAddress, prefill }),\n status: \"idle\",\n };\n\n function reducer(state: State, action: Action): State {\n switch (action.type) {\n case \"setInputs\":\n return { ...state, inputs: { ...state.inputs, ...action.payload } };\n case \"resetInputs\":\n return {\n ...state,\n inputs: buildInitialInputs({\n type,\n network,\n connectedAddress,\n prefill,\n }),\n };\n case \"setStatus\":\n return { ...state, status: action.payload };\n default:\n return state;\n }\n }\n\n const [state, dispatch] = useReducer(reducer, initialState);\n const inputs = state.inputs;\n const setInputs = (\n next: TransactionFlowInputs | Partial,\n ) => {\n dispatch({\n type: \"setInputs\",\n payload: next as Partial,\n });\n };\n\n const loading = state.status === \"executing\";\n const [refreshing, setRefreshing] = useState(false);\n const [isDialogOpen, setIsDialogOpen] = useState(false);\n const [txError, setTxError] = useState(null);\n const [lastExplorerUrl, setLastExplorerUrl] = useState(\"\");\n const previousConnectedAddressRef = useRef
(\n connectedAddress,\n );\n const maxAmountRequestIdRef = useRef(0);\n const [selectedSourceChains, setSelectedSourceChains] = useState<\n number[] | null\n >(null);\n const [selectedSourcesMaxAmount, setSelectedSourcesMaxAmount] = useState<\n string | null\n >(null);\n const [appliedSourceSelectionKey, setAppliedSourceSelectionKey] =\n useState(\"ALL\");\n const {\n steps,\n onStepsList,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n const configuredMaxAmount = useMemo(\n () => normalizeMaxAmount(maxAmount),\n [maxAmount],\n );\n\n const areInputsValid = useMemo(() => {\n const hasToken = inputs?.token !== undefined && inputs?.token !== null;\n const hasChain = inputs?.chain !== undefined && inputs?.chain !== null;\n const hasAmount = Boolean(inputs?.amount) && Number(inputs?.amount) > 0;\n const hasValidRecipient =\n Boolean(inputs?.recipient) && isAddress(inputs.recipient as string);\n return hasToken && hasChain && hasAmount && hasValidRecipient;\n }, [inputs]);\n\n const filteredBridgableBalance = useMemo(() => {\n return bridgableBalance?.find((bal) =>\n inputs?.token === \"USDM\"\n ? bal?.symbol === \"USDC\"\n : bal?.symbol === inputs?.token,\n );\n }, [bridgableBalance, inputs?.token]);\n\n const availableSources = useMemo(() => {\n const breakdown = filteredBridgableBalance?.breakdown ?? [];\n const destinationChainId = inputs?.chain;\n const nonZero = breakdown.filter((source) => {\n if (Number.parseFloat(source.balance ?? \"0\") <= 0) return false;\n if (typeof destinationChainId === \"number\") {\n return source.chain.id !== destinationChainId;\n }\n return true;\n });\n const decimals = filteredBridgableBalance?.decimals;\n if (!nexusSDK || typeof decimals !== \"number\") {\n return nonZero.sort(\n (a, b) => Number.parseFloat(b.balance) - Number.parseFloat(a.balance),\n );\n }\n return nonZero.sort((a, b) => {\n try {\n const aRaw = parseUnits(a.balance ?? \"0\", decimals);\n const bRaw = parseUnits(b.balance ?? \"0\", decimals);\n if (aRaw === bRaw) return 0;\n return aRaw > bRaw ? -1 : 1;\n } catch {\n return Number.parseFloat(b.balance) - Number.parseFloat(a.balance);\n }\n });\n }, [\n inputs?.chain,\n filteredBridgableBalance?.breakdown,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n ]);\n\n const allAvailableSourceChainIds = useMemo(\n () => availableSources.map((source) => source.chain.id),\n [availableSources],\n );\n\n const effectiveSelectedSourceChains = useMemo(() => {\n if (selectedSourceChains && selectedSourceChains.length > 0) {\n const availableSet = new Set(allAvailableSourceChainIds);\n const filteredSelection = selectedSourceChains.filter((id) =>\n availableSet.has(id),\n );\n if (filteredSelection.length > 0) {\n return filteredSelection;\n }\n }\n return allAvailableSourceChainIds;\n }, [selectedSourceChains, allAvailableSourceChainIds]);\n\n const sourceChainsForSdk =\n effectiveSelectedSourceChains.length > 0\n ? effectiveSelectedSourceChains\n : undefined;\n\n const sourceSelectionKey = useMemo(() => {\n if (allAvailableSourceChainIds.length === 0) return \"NONE\";\n if (!selectedSourceChains || selectedSourceChains.length === 0) {\n return \"ALL\";\n }\n return [...effectiveSelectedSourceChains].sort((a, b) => a - b).join(\"|\");\n }, [\n allAvailableSourceChainIds.length,\n effectiveSelectedSourceChains,\n selectedSourceChains,\n ]);\n const hasPendingSourceSelectionChanges =\n sourceSelectionKey !== appliedSourceSelectionKey;\n const intentSourceSpendAmount = intent.current?.intent?.sourcesTotal;\n\n const getMaxForCurrentSelection = useCallback(async () => {\n if (!nexusSDK || !inputs?.token || !inputs?.chain) return undefined;\n const maxBalAvailable = await nexusSDK.calculateMaxForBridge({\n token: inputs.token,\n toChainId: inputs.chain,\n recipient: inputs.recipient,\n sourceChains: sourceChainsForSdk,\n });\n if (!maxBalAvailable?.amount) return \"0\";\n return clampAmountToMax({\n amount: maxBalAvailable.amount,\n maxAmount: configuredMaxAmount,\n nexusSDK,\n token: inputs.token,\n chainId: inputs.chain,\n });\n }, [\n configuredMaxAmount,\n inputs?.chain,\n inputs?.recipient,\n inputs?.token,\n nexusSDK,\n sourceChainsForSdk,\n ]);\n\n const toggleSourceChain = useCallback(\n (chainId: number) => {\n setSelectedSourceChains((prev) => {\n if (allAvailableSourceChainIds.length === 0) return prev;\n const current =\n prev && prev.length > 0 ? prev : allAvailableSourceChainIds;\n const next = current.includes(chainId)\n ? current.filter((id) => id !== chainId)\n : [...current, chainId];\n if (next.length === 0) {\n return current;\n }\n const isAllSelected =\n next.length === allAvailableSourceChainIds.length &&\n allAvailableSourceChainIds.every((id) => next.includes(id));\n return isAllSelected ? null : next;\n });\n },\n [allAvailableSourceChainIds],\n );\n\n const sourceSelection = useMemo(() => {\n const amount =\n intentSourceSpendAmount?.trim() ?? inputs?.amount?.trim() ?? \"\";\n const decimals = getCoverageDecimals({\n type,\n token: inputs?.token,\n chainId: inputs?.chain,\n fallback: filteredBridgableBalance?.decimals,\n });\n const selectedChainSet = new Set(effectiveSelectedSourceChains);\n const selectedTotalRaw =\n !nexusSDK || typeof decimals !== \"number\"\n ? BigInt(0)\n : availableSources.reduce((sum, source) => {\n if (!selectedChainSet.has(source.chain.id)) return sum;\n try {\n return sum + parseUnits(source.balance ?? \"0\", decimals);\n } catch {\n return sum;\n }\n }, BigInt(0));\n const selectedTotal =\n !nexusSDK || typeof decimals !== \"number\"\n ? \"0\"\n : formatAmountForDisplay(selectedTotalRaw, decimals, nexusSDK);\n const baseSelection = {\n selectedTotal,\n requiredTotal: amount || \"0\",\n requiredSafetyTotal: amount || \"0\",\n missingToProceed: \"0\",\n missingToSafety: \"0\",\n coverageState: \"healthy\" as SourceCoverageState,\n coverageToSafetyPercent: 100,\n isBelowRequired: false,\n isBelowSafetyBuffer: false,\n };\n\n if (!nexusSDK || !inputs?.token || !inputs?.chain || !amount) {\n return baseSelection;\n }\n\n try {\n const requiredRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n inputs.token,\n inputs.chain,\n );\n if (requiredRaw <= BigInt(0)) {\n return baseSelection;\n }\n\n const missingToProceedRaw =\n selectedTotalRaw >= requiredRaw\n ? BigInt(0)\n : requiredRaw - selectedTotalRaw;\n const missingToSafetyRaw = missingToProceedRaw;\n\n const coverageState: SourceCoverageState =\n selectedTotalRaw < requiredRaw ? \"error\" : \"healthy\";\n\n const coverageBasisPoints =\n requiredRaw === BigInt(0)\n ? 10_000\n : selectedTotalRaw >= requiredRaw\n ? 10_000\n : Number((selectedTotalRaw * BigInt(10_000)) / requiredRaw);\n\n return {\n selectedTotal,\n requiredTotal: amount,\n requiredSafetyTotal: amount,\n missingToProceed: formatAmountForDisplay(\n missingToProceedRaw,\n decimals,\n nexusSDK,\n ),\n missingToSafety: formatAmountForDisplay(\n missingToSafetyRaw,\n decimals,\n nexusSDK,\n ),\n coverageState,\n coverageToSafetyPercent: coverageBasisPoints / 100,\n isBelowRequired: coverageState === \"error\",\n isBelowSafetyBuffer: coverageState === \"error\",\n };\n } catch {\n return baseSelection;\n }\n }, [\n type,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n inputs?.chain,\n inputs?.amount,\n inputs?.token,\n intentSourceSpendAmount,\n availableSources,\n effectiveSelectedSourceChains,\n ]);\n\n const stopwatch = useStopwatch({ intervalMs: 100 });\n const setStatus = useCallback(\n (status: TransactionStatus) =>\n dispatch({ type: \"setStatus\", payload: status }),\n [],\n );\n\n const resetInputs = useCallback(() => {\n dispatch({ type: \"resetInputs\" });\n }, []);\n\n const {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n } = useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n });\n\n usePolling(\n Boolean(intent.current) &&\n !isDialogOpen &&\n !isSourceMenuOpen &&\n !hasPendingSourceSelectionChanges,\n async () => {\n await refreshIntent();\n },\n 15000,\n );\n\n const debouncedRefreshMaxForSelection = useDebouncedCallback(\n async (requestId: number) => {\n try {\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (requestId !== maxAmountRequestIdRef.current) return;\n setSelectedSourcesMaxAmount(maxForCurrentSelection ?? \"0\");\n } catch (error) {\n if (requestId !== maxAmountRequestIdRef.current) return;\n console.error(\"Unable to calculate max for selected sources:\", error);\n setSelectedSourcesMaxAmount(\"0\");\n }\n },\n MAX_AMOUNT_DEBOUNCE_MS,\n );\n\n useEffect(() => {\n debouncedRefreshMaxForSelection.cancel();\n if (!nexusSDK || !inputs?.token || !inputs?.chain) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(null);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(\"0\");\n return;\n }\n const requestId = ++maxAmountRequestIdRef.current;\n debouncedRefreshMaxForSelection(requestId);\n }, [\n allAvailableSourceChainIds.length,\n configuredMaxAmount,\n debouncedRefreshMaxForSelection,\n inputs?.recipient,\n sourceSelectionKey,\n inputs?.chain,\n inputs?.token,\n nexusSDK,\n ]);\n\n useEffect(() => {\n if (type !== \"bridge\" || !connectedAddress) return;\n const previousConnectedAddress = previousConnectedAddressRef.current;\n if (!previousConnectedAddress) {\n previousConnectedAddressRef.current = connectedAddress;\n return;\n }\n if (connectedAddress === previousConnectedAddress) return;\n previousConnectedAddressRef.current = connectedAddress;\n if (prefill?.recipient) return;\n if (!inputs?.recipient || inputs.recipient === previousConnectedAddress) {\n dispatch({ type: \"setInputs\", payload: { recipient: connectedAddress } });\n }\n }, [type, connectedAddress, inputs?.recipient, prefill?.recipient]);\n\n useEffect(() => {\n invalidatePendingExecution();\n }, [inputs, invalidatePendingExecution]);\n\n useEffect(() => {\n setSelectedSourceChains(null);\n }, [inputs?.token]);\n\n useEffect(() => {\n if (isDialogOpen) return;\n stopwatch.stop();\n stopwatch.reset();\n if (state.status === \"success\" || state.status === \"error\") {\n resetSteps();\n setLastExplorerUrl(\"\");\n setStatus(\"idle\");\n }\n }, [isDialogOpen, resetSteps, setStatus, state.status, stopwatch]);\n\n useEffect(() => {\n if (txError) {\n setTxError(null);\n }\n }, [inputs, txError]);\n\n return {\n inputs,\n setInputs,\n timer: stopwatch.seconds,\n setIsDialogOpen,\n setTxError,\n loading,\n refreshing,\n isDialogOpen,\n txError,\n handleTransaction,\n reset,\n filteredBridgableBalance,\n startTransaction,\n commitAmount,\n lastExplorerUrl,\n steps,\n status: state.status,\n availableSources,\n selectedSourceChains: effectiveSelectedSourceChains,\n toggleSourceChain,\n isSourceSelectionInsufficient: sourceSelection.isBelowRequired,\n isSourceSelectionBelowSafetyBuffer: sourceSelection.isBelowSafetyBuffer,\n isSourceSelectionReadyForAccept:\n sourceSelection.coverageState === \"healthy\",\n sourceCoverageState: sourceSelection.coverageState,\n sourceCoveragePercent: sourceSelection.coverageToSafetyPercent,\n missingToProceed: sourceSelection.missingToProceed,\n missingToSafety: sourceSelection.missingToSafety,\n selectedTotal: sourceSelection.selectedTotal,\n requiredTotal: sourceSelection.requiredTotal,\n requiredSafetyTotal: sourceSelection.requiredSafetyTotal,\n maxAvailableAmount: selectedSourcesMaxAmount ?? undefined,\n isInputsValid: areInputsValid,\n };\n}\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type {\n NexusNetwork,\n OnAllowanceHookData,\n OnIntentHookData,\n TokenBalance,\n ChainBalance,\n} from \"@avail-project/nexus-sdk-v2\";\nimport { parseUnits } from \"viem\";\nimport {\n useEffect,\n useMemo,\n useCallback,\n useRef,\n useState,\n useReducer,\n type RefObject,\n} from \"react\";\nimport { type Address, isAddress } from \"viem\";\nimport { useNexusError } from \"./useNexusError\";\nimport { useTransactionExecution } from \"./useTransactionExecution\";\nimport { usePolling } from \"./usePolling\";\nimport { useStopwatch } from \"./useStopwatch\";\nimport { useDebouncedCallback } from \"./useDebouncedCallback\";\nimport { type TransactionStatus } from \"../tx/types\";\nimport { useTransactionSteps } from \"../tx/useTransactionSteps\";\nimport {\n type SourceCoverageState,\n type TransactionFlowExecutor,\n type TransactionFlowInputs,\n type TransactionFlowPrefill,\n type TransactionFlowType,\n} from \"../types/transaction-flow\";\nimport {\n MAX_AMOUNT_DEBOUNCE_MS,\n buildInitialInputs,\n clampAmountToMax,\n formatAmountForDisplay,\n getCoverageDecimals,\n normalizeMaxAmount,\n} from \"../utils/transaction-flow\";\n\ntype NexusClient = ReturnType;\n\n// v2 uses a generic step shape; minimal type to satisfy getStepKey constraint\ntype BridgePlanStep = {\n typeID?: string;\n type?: string;\n [key: string]: unknown;\n};\n\ninterface BaseTransactionFlowProps {\n type: TransactionFlowType;\n network: NexusNetwork;\n nexusSDK: NexusClient | null;\n intent: RefObject;\n allowance: RefObject;\n bridgableBalance: TokenBalance[] | null;\n prefill?: TransactionFlowPrefill;\n onComplete?: (explorerUrl?: string) => void;\n onStart?: () => void;\n onError?: (message: string) => void;\n fetchBalance: () => Promise;\n maxAmount?: string | number;\n isSourceMenuOpen?: boolean;\n notifyHistoryRefresh?: () => void;\n executeTransaction: TransactionFlowExecutor;\n}\n\nexport interface UseTransactionFlowProps extends BaseTransactionFlowProps {\n connectedAddress?: Address;\n}\n\ntype State = {\n inputs: TransactionFlowInputs;\n status: TransactionStatus;\n};\n\ntype Action =\n | { type: \"setInputs\"; payload: Partial }\n | { type: \"resetInputs\" }\n | { type: \"setStatus\"; payload: TransactionStatus };\n\nexport function useTransactionFlow(props: UseTransactionFlowProps) {\n const {\n type,\n network,\n nexusSDK,\n intent,\n bridgableBalance,\n prefill,\n onComplete,\n onStart,\n onError,\n fetchBalance,\n allowance,\n maxAmount,\n isSourceMenuOpen = false,\n notifyHistoryRefresh,\n executeTransaction,\n } = props;\n\n const connectedAddress = props.connectedAddress;\n const operationName = type === \"bridge\" ? \"bridge\" : \"transfer\";\n const handleNexusError = useNexusError();\n const initialState: State = {\n inputs: buildInitialInputs({ type, network, connectedAddress, prefill }),\n status: \"idle\",\n };\n\n function reducer(state: State, action: Action): State {\n switch (action.type) {\n case \"setInputs\":\n return { ...state, inputs: { ...state.inputs, ...action.payload } };\n case \"resetInputs\":\n return {\n ...state,\n inputs: buildInitialInputs({\n type,\n network,\n connectedAddress,\n prefill,\n }),\n };\n case \"setStatus\":\n return { ...state, status: action.payload };\n default:\n return state;\n }\n }\n\n const [state, dispatch] = useReducer(reducer, initialState);\n const inputs = state.inputs;\n const setInputs = (\n next: TransactionFlowInputs | Partial,\n ) => {\n dispatch({\n type: \"setInputs\",\n payload: next as Partial,\n });\n };\n\n const loading = state.status === \"executing\";\n const [refreshing, setRefreshing] = useState(false);\n const [isDialogOpen, setIsDialogOpen] = useState(false);\n const [txError, setTxError] = useState(null);\n const [lastExplorerUrl, setLastExplorerUrl] = useState(\"\");\n const previousConnectedAddressRef = useRef
(\n connectedAddress,\n );\n const maxAmountRequestIdRef = useRef(0);\n const [selectedSourceChains, setSelectedSourceChains] = useState<\n number[] | null\n >(null);\n const [selectedSourcesMaxAmount, setSelectedSourcesMaxAmount] = useState<\n string | null\n >(null);\n const [appliedSourceSelectionKey, setAppliedSourceSelectionKey] =\n useState(\"ALL\");\n const {\n steps,\n onStepsList,\n onStepComplete,\n reset: resetSteps,\n } = useTransactionSteps();\n const configuredMaxAmount = useMemo(\n () => normalizeMaxAmount(maxAmount),\n [maxAmount],\n );\n\n const areInputsValid = useMemo(() => {\n const hasToken = inputs?.token !== undefined && inputs?.token !== null;\n const hasChain = inputs?.chain !== undefined && inputs?.chain !== null;\n const hasAmount = Boolean(inputs?.amount) && Number(inputs?.amount) > 0;\n const hasValidRecipient =\n Boolean(inputs?.recipient) && isAddress(inputs.recipient as string);\n return hasToken && hasChain && hasAmount && hasValidRecipient;\n }, [inputs]);\n\n const filteredBridgableBalance = useMemo(() => {\n return bridgableBalance?.find((bal) =>\n inputs?.token === \"USDM\"\n ? bal?.symbol === \"USDC\"\n : bal?.symbol === inputs?.token,\n );\n }, [bridgableBalance, inputs?.token]);\n\n const availableSources = useMemo(() => {\n // v2: chainBalances replaces breakdown\n const chainBalances = filteredBridgableBalance?.chainBalances ?? [];\n const destinationChainId = inputs?.chain;\n const nonZero = chainBalances.filter((source: ChainBalance) => {\n if (Number.parseFloat(source.balance ?? \"0\") <= 0) return false;\n if (typeof destinationChainId === \"number\") {\n return source.chain.id !== destinationChainId;\n }\n return true;\n });\n const decimals = filteredBridgableBalance?.decimals;\n if (!nexusSDK || typeof decimals !== \"number\") {\n return nonZero.sort(\n (a, b) => Number.parseFloat(b.balance) - Number.parseFloat(a.balance),\n );\n }\n return nonZero.sort((a: ChainBalance, b: ChainBalance) => {\n try {\n const aRaw = parseUnits(a.balance ?? \"0\", decimals);\n const bRaw = parseUnits(b.balance ?? \"0\", decimals);\n if (aRaw === bRaw) return 0;\n return aRaw > bRaw ? -1 : 1;\n } catch {\n return Number.parseFloat(b.balance) - Number.parseFloat(a.balance);\n }\n });\n }, [\n inputs?.chain,\n filteredBridgableBalance?.chainBalances,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n ]);\n\n const allAvailableSourceChainIds = useMemo(\n () => availableSources.map((source: ChainBalance) => source.chain.id),\n [availableSources],\n );\n\n const effectiveSelectedSourceChains = useMemo(() => {\n if (selectedSourceChains && selectedSourceChains.length > 0) {\n const availableSet = new Set(allAvailableSourceChainIds);\n const filteredSelection = selectedSourceChains.filter((id: number) =>\n availableSet.has(id),\n );\n if (filteredSelection.length > 0) {\n return filteredSelection;\n }\n }\n return allAvailableSourceChainIds;\n }, [selectedSourceChains, allAvailableSourceChainIds]);\n\n const sourceChainsForSdk =\n effectiveSelectedSourceChains.length > 0\n ? effectiveSelectedSourceChains\n : undefined;\n\n const sourceSelectionKey = useMemo(() => {\n if (allAvailableSourceChainIds.length === 0) return \"NONE\";\n if (!selectedSourceChains || selectedSourceChains.length === 0) {\n return \"ALL\";\n }\n return [...effectiveSelectedSourceChains].sort((a: number, b: number) => a - b).join(\"|\");\n }, [\n allAvailableSourceChainIds.length,\n effectiveSelectedSourceChains,\n selectedSourceChains,\n ]);\n const hasPendingSourceSelectionChanges =\n sourceSelectionKey !== appliedSourceSelectionKey;\n const intentSourceSpendAmount = intent.current?.intent?.sourcesTotal;\n\n /**\n * v2: calculateMaxForBridge is removed. Use simulateBridge to get the max amount,\n * or fall back to summing available source balances directly.\n */\n const getMaxForCurrentSelection = useCallback(async () => {\n if (!nexusSDK || !inputs?.token || !inputs?.chain) return undefined;\n\n // Sum balances from selected sources as a direct proxy for max\n const decimals = filteredBridgableBalance?.decimals;\n if (typeof decimals !== \"number\") return \"0\";\n\n const selectedSet = new Set(\n sourceChainsForSdk ?? allAvailableSourceChainIds,\n );\n const totalRaw = availableSources.reduce((sum: bigint, source: ChainBalance) => {\n if (!selectedSet.has(source.chain.id)) return sum;\n try {\n return sum + parseUnits(source.balance ?? \"0\", decimals);\n } catch {\n return sum;\n }\n }, BigInt(0));\n\n const totalReadable = formatToBigIntReadable(totalRaw, decimals);\n if (!totalReadable) return \"0\";\n\n return clampAmountToMax({\n amount: totalReadable,\n maxAmount: configuredMaxAmount,\n nexusSDK,\n token: inputs.token,\n chainId: inputs.chain,\n });\n }, [\n configuredMaxAmount,\n inputs?.chain,\n inputs?.token,\n nexusSDK,\n sourceChainsForSdk,\n allAvailableSourceChainIds,\n availableSources,\n filteredBridgableBalance?.decimals,\n ]);\n\n const toggleSourceChain = useCallback(\n (chainId: number) => {\n setSelectedSourceChains((prev) => {\n if (allAvailableSourceChainIds.length === 0) return prev;\n const current =\n prev && prev.length > 0 ? prev : allAvailableSourceChainIds;\n const next = current.includes(chainId)\n ? current.filter((id: number) => id !== chainId)\n : [...current, chainId];\n if (next.length === 0) {\n return current;\n }\n const isAllSelected =\n next.length === allAvailableSourceChainIds.length &&\n allAvailableSourceChainIds.every((id) => next.includes(id));\n return isAllSelected ? null : next;\n });\n },\n [allAvailableSourceChainIds],\n );\n\n const sourceSelection = useMemo(() => {\n const amount =\n intentSourceSpendAmount?.trim() ?? inputs?.amount?.trim() ?? \"\";\n const decimals = getCoverageDecimals({\n type,\n token: inputs?.token,\n chainId: inputs?.chain,\n fallback: filteredBridgableBalance?.decimals,\n });\n const selectedChainSet = new Set(effectiveSelectedSourceChains);\n const selectedTotalRaw =\n !nexusSDK || typeof decimals !== \"number\"\n ? BigInt(0)\n : availableSources.reduce((sum: bigint, source: ChainBalance) => {\n if (!selectedChainSet.has(source.chain.id)) return sum;\n try {\n return sum + parseUnits(source.balance ?? \"0\", decimals);\n } catch {\n return sum;\n }\n }, BigInt(0));\n const selectedTotal =\n !nexusSDK || typeof decimals !== \"number\"\n ? \"0\"\n : formatAmountForDisplay(selectedTotalRaw, decimals, nexusSDK);\n const baseSelection = {\n selectedTotal,\n requiredTotal: amount || \"0\",\n requiredSafetyTotal: amount || \"0\",\n missingToProceed: \"0\",\n missingToSafety: \"0\",\n coverageState: \"healthy\" as SourceCoverageState,\n coverageToSafetyPercent: 100,\n isBelowRequired: false,\n isBelowSafetyBuffer: false,\n };\n\n if (!nexusSDK || !inputs?.token || !inputs?.chain || !amount) {\n return baseSelection;\n }\n\n try {\n // v2: convertTokenReadableAmountToBigInt(amount, tokenSymbol, chainId)\n const requiredRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n inputs.token,\n inputs.chain,\n );\n if (requiredRaw <= BigInt(0)) {\n return baseSelection;\n }\n\n const missingToProceedRaw =\n selectedTotalRaw >= requiredRaw\n ? BigInt(0)\n : requiredRaw - selectedTotalRaw;\n const missingToSafetyRaw = missingToProceedRaw;\n\n const coverageState: SourceCoverageState =\n selectedTotalRaw < requiredRaw ? \"error\" : \"healthy\";\n\n const coverageBasisPoints =\n requiredRaw === BigInt(0)\n ? 10_000\n : selectedTotalRaw >= requiredRaw\n ? 10_000\n : Number((selectedTotalRaw * BigInt(10_000)) / requiredRaw);\n\n return {\n selectedTotal,\n requiredTotal: amount,\n requiredSafetyTotal: amount,\n missingToProceed: formatAmountForDisplay(\n missingToProceedRaw,\n decimals,\n nexusSDK,\n ),\n missingToSafety: formatAmountForDisplay(\n missingToSafetyRaw,\n decimals,\n nexusSDK,\n ),\n coverageState,\n coverageToSafetyPercent: coverageBasisPoints / 100,\n isBelowRequired: coverageState === \"error\",\n isBelowSafetyBuffer: coverageState === \"error\",\n };\n } catch {\n return baseSelection;\n }\n }, [\n type,\n filteredBridgableBalance?.decimals,\n nexusSDK,\n inputs?.chain,\n inputs?.amount,\n inputs?.token,\n intentSourceSpendAmount,\n availableSources,\n effectiveSelectedSourceChains,\n ]);\n\n const stopwatch = useStopwatch({ intervalMs: 100 });\n const setStatus = useCallback(\n (status: TransactionStatus) =>\n dispatch({ type: \"setStatus\", payload: status }),\n [],\n );\n\n const resetInputs = useCallback(() => {\n dispatch({ type: \"resetInputs\" });\n }, []);\n\n const {\n refreshIntent,\n handleTransaction,\n startTransaction,\n commitAmount,\n reset,\n invalidatePendingExecution,\n } = useTransactionExecution({\n operationName,\n nexusSDK,\n intent,\n allowance,\n inputs,\n configuredMaxAmount,\n allAvailableSourceChainIds,\n sourceChainsForSdk,\n sourceSelectionKey,\n sourceSelection,\n loading,\n txError,\n areInputsValid,\n executeTransaction,\n getMaxForCurrentSelection,\n onStepsList,\n onStepComplete,\n resetSteps,\n setStatus,\n resetInputs,\n setRefreshing,\n setIsDialogOpen,\n setTxError,\n setLastExplorerUrl,\n setSelectedSourceChains,\n setAppliedSourceSelectionKey,\n stopwatch,\n handleNexusError,\n onStart,\n onComplete,\n onError,\n fetchBalance,\n notifyHistoryRefresh,\n });\n\n usePolling(\n Boolean(intent.current) &&\n !isDialogOpen &&\n !isSourceMenuOpen &&\n !hasPendingSourceSelectionChanges,\n async () => {\n await refreshIntent();\n },\n 15000,\n );\n\n const debouncedRefreshMaxForSelection = useDebouncedCallback(\n async (requestId: number) => {\n try {\n const maxForCurrentSelection = await getMaxForCurrentSelection();\n if (requestId !== maxAmountRequestIdRef.current) return;\n setSelectedSourcesMaxAmount(maxForCurrentSelection ?? \"0\");\n } catch (error) {\n if (requestId !== maxAmountRequestIdRef.current) return;\n console.error(\"Unable to calculate max for selected sources:\", error);\n setSelectedSourcesMaxAmount(\"0\");\n }\n },\n MAX_AMOUNT_DEBOUNCE_MS,\n );\n\n useEffect(() => {\n debouncedRefreshMaxForSelection.cancel();\n if (!nexusSDK || !inputs?.token || !inputs?.chain) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(null);\n return;\n }\n if (allAvailableSourceChainIds.length === 0) {\n maxAmountRequestIdRef.current += 1;\n setSelectedSourcesMaxAmount(\"0\");\n return;\n }\n const requestId = ++maxAmountRequestIdRef.current;\n debouncedRefreshMaxForSelection(requestId);\n }, [\n allAvailableSourceChainIds.length,\n configuredMaxAmount,\n debouncedRefreshMaxForSelection,\n inputs?.recipient,\n sourceSelectionKey,\n inputs?.chain,\n inputs?.token,\n nexusSDK,\n ]);\n\n useEffect(() => {\n if (type !== \"bridge\" || !connectedAddress) return;\n const previousConnectedAddress = previousConnectedAddressRef.current;\n if (!previousConnectedAddress) {\n previousConnectedAddressRef.current = connectedAddress;\n return;\n }\n if (connectedAddress === previousConnectedAddress) return;\n previousConnectedAddressRef.current = connectedAddress;\n if (prefill?.recipient) return;\n if (!inputs?.recipient || inputs.recipient === previousConnectedAddress) {\n dispatch({ type: \"setInputs\", payload: { recipient: connectedAddress } });\n }\n }, [type, connectedAddress, inputs?.recipient, prefill?.recipient]);\n\n useEffect(() => {\n invalidatePendingExecution();\n }, [inputs, invalidatePendingExecution]);\n\n useEffect(() => {\n setSelectedSourceChains(null);\n }, [inputs?.token]);\n\n // Safety-net: stop the stopwatch as soon as status reaches a terminal state.\n // This ensures the timer freezes even if the onEvent closure's stopwatch.stop()\n // didn't fire (e.g. stale closure reference or SDK promise resolved oddly).\n useEffect(() => {\n if (state.status === \"success\" || state.status === \"error\") {\n stopwatch.stop();\n }\n }, [state.status, stopwatch]);\n\n useEffect(() => {\n if (isDialogOpen) return;\n stopwatch.stop();\n stopwatch.reset();\n if (state.status === \"success\" || state.status === \"error\") {\n resetSteps();\n setLastExplorerUrl(\"\");\n setStatus(\"idle\");\n }\n }, [isDialogOpen, resetSteps, setStatus, state.status, stopwatch]);\n\n useEffect(() => {\n if (txError) {\n setTxError(null);\n }\n }, [inputs, txError]);\n\n\n return {\n inputs,\n setInputs,\n timer: stopwatch.seconds,\n setIsDialogOpen,\n setTxError,\n loading,\n refreshing,\n isDialogOpen,\n txError,\n handleTransaction,\n reset,\n filteredBridgableBalance,\n startTransaction,\n commitAmount,\n lastExplorerUrl,\n steps,\n status: state.status,\n availableSources,\n selectedSourceChains: effectiveSelectedSourceChains,\n toggleSourceChain,\n isSourceSelectionInsufficient: sourceSelection.isBelowRequired,\n isSourceSelectionBelowSafetyBuffer: sourceSelection.isBelowSafetyBuffer,\n isSourceSelectionReadyForAccept:\n sourceSelection.coverageState === \"healthy\",\n sourceCoverageState: sourceSelection.coverageState,\n sourceCoveragePercent: sourceSelection.coverageToSafetyPercent,\n missingToProceed: sourceSelection.missingToProceed,\n missingToSafety: sourceSelection.missingToSafety,\n selectedTotal: sourceSelection.selectedTotal,\n requiredTotal: sourceSelection.requiredTotal,\n requiredSafetyTotal: sourceSelection.requiredSafetyTotal,\n maxAvailableAmount: selectedSourcesMaxAmount ?? undefined,\n isInputsValid: areInputsValid,\n };\n}\n\n/** Helper: format a bigint rawAmount with decimals into a readable decimal string. */\nfunction formatToBigIntReadable(raw: bigint, decimals: number): string {\n if (raw <= BigInt(0)) return \"0\";\n const divisor = BigInt(10 ** decimals);\n const whole = raw / divisor;\n const fraction = raw % divisor;\n if (fraction === BigInt(0)) return whole.toString();\n const fractionStr = fraction.toString().padStart(decimals, \"0\").replace(/0+$/, \"\");\n return `${whole}.${fractionStr}`;\n}\n", "type": "registry:component", "target": "components/common/hooks/useTransactionFlow.ts" }, @@ -110,7 +110,7 @@ }, { "path": "registry/nexus-elements/common/tx/steps.ts", - "content": "import type { SwapStepType } from \"@avail-project/nexus-core\";\nimport type { GenericStep } from \"./types\";\nimport { getStepKey } from \"./types\";\n\n/**\n * Predefined expected steps for swaps to seed UI before events arrive.\n * Kept here to avoid duplication across exact-in and exact-out hooks.\n */\nexport const SWAP_EXPECTED_STEPS: SwapStepType[] = [\n { type: \"SWAP_START\", typeID: \"SWAP_START\" } as SwapStepType,\n { type: \"DETERMINING_SWAP\", typeID: \"DETERMINING_SWAP\" } as SwapStepType,\n {\n type: \"CREATE_PERMIT_FOR_SOURCE_SWAP\",\n typeID:\n \"CREATE_PERMIT_FOR_SOURCE_SWAP\" as unknown as SwapStepType[\"typeID\"],\n } as SwapStepType,\n {\n type: \"SOURCE_SWAP_BATCH_TX\",\n typeID: \"SOURCE_SWAP_BATCH_TX\",\n } as SwapStepType,\n {\n type: \"SOURCE_SWAP_HASH\",\n typeID: \"SOURCE_SWAP_HASH\" as unknown as SwapStepType[\"typeID\"],\n } as SwapStepType,\n { type: \"RFF_ID\", typeID: \"RFF_ID\" } as SwapStepType,\n {\n type: \"DESTINATION_SWAP_BATCH_TX\",\n typeID: \"DESTINATION_SWAP_BATCH_TX\",\n } as SwapStepType,\n {\n type: \"DESTINATION_SWAP_HASH\",\n typeID: \"DESTINATION_SWAP_HASH\" as unknown as SwapStepType[\"typeID\"],\n } as SwapStepType,\n { type: \"SWAP_COMPLETE\", typeID: \"SWAP_COMPLETE\" } as SwapStepType,\n];\n\nexport function seedSteps(expected: T[]): Array> {\n return expected.map((st, index) => ({\n id: index,\n completed: false,\n step: st,\n }));\n}\n\nexport function computeAllCompleted(steps: Array>): boolean {\n return steps.length > 0 && steps.every((s) => s.completed);\n}\n\n/**\n * Replace the current list of steps with a new list, preserving completion\n * for any steps that were already marked completed (matched by key).\n */\nexport function mergeStepsList(\n prev: Array>,\n list: T[]\n): Array> {\n const completedKeys = new Set();\n for (const prevStep of prev) {\n if (prevStep.completed) {\n completedKeys.add(getStepKey(prevStep.step));\n }\n }\n const next: Array> = [];\n for (let index = 0; index < list.length; index++) {\n const step = list[index];\n const key = getStepKey(step);\n next.push({\n id: index,\n completed: completedKeys.has(key),\n step,\n });\n }\n return next;\n}\n\n/**\n * Mark a step complete in-place; if the step doesn't yet exist, append it.\n */\nexport function mergeStepComplete(\n prev: Array>,\n step: T\n): Array> {\n const key = getStepKey(step);\n const updated: Array> = [];\n let found = false;\n for (const s of prev) {\n if (getStepKey(s.step) === key) {\n updated.push({ ...s, completed: true, step: { ...s.step, ...step } });\n found = true;\n } else {\n updated.push(s);\n }\n }\n if (!found) {\n updated.push({\n id: updated.length,\n completed: true,\n step,\n });\n }\n return updated;\n}\n", + "content": "// v2: SwapStepType is no longer exported from the SDK — use a local step shape\n// that matches v2 SwapPlanStep discriminator pattern\nexport type SwapStepType = {\n typeID?: string;\n type?: string;\n [key: string]: unknown;\n};\n\nimport type { GenericStep } from \"./types\";\nimport { getStepKey } from \"./types\";\n\n/**\n * Predefined expected steps for swaps to seed UI before events arrive.\n * Uses v2 stepType names that match SwapPlanProgressEvent.stepType discriminators.\n */\nexport const SWAP_EXPECTED_STEPS: SwapStepType[] = [\n { type: \"source_swap\", typeID: \"source_swap\" },\n { type: \"eoa_to_ephemeral_transfer\", typeID: \"eoa_to_ephemeral_transfer\" },\n { type: \"bridge_deposit\", typeID: \"bridge_deposit\" },\n { type: \"bridge_intent_submission\", typeID: \"bridge_intent_submission\" },\n { type: \"bridge_fill\", typeID: \"bridge_fill\" },\n { type: \"destination_swap\", typeID: \"destination_swap\" },\n];\n\nexport function seedSteps(expected: T[]): Array> {\n return expected.map((st, index) => ({\n id: index,\n completed: false,\n step: st,\n }));\n}\n\nexport function computeAllCompleted(steps: Array>): boolean {\n return steps.length > 0 && steps.every((s) => s.completed);\n}\n\n/**\n * Replace the current list of steps with a new list, preserving completion\n * for any steps that were already marked completed (matched by key).\n */\nexport function mergeStepsList(\n prev: Array>,\n list: T[]\n): Array> {\n const completedKeys = new Set();\n for (const prevStep of prev) {\n if (prevStep.completed) {\n completedKeys.add(getStepKey(prevStep.step));\n }\n }\n const next: Array> = [];\n for (let index = 0; index < list.length; index++) {\n const step = list[index];\n const key = getStepKey(step);\n next.push({\n id: index,\n completed: completedKeys.has(key),\n step,\n });\n }\n return next;\n}\n\n/**\n * Mark a step complete in-place; if the step doesn't yet exist, append it.\n */\nexport function mergeStepComplete(\n prev: Array>,\n step: T\n): Array> {\n const key = getStepKey(step);\n const updated: Array> = [];\n let found = false;\n for (const s of prev) {\n if (getStepKey(s.step) === key) {\n updated.push({ ...s, completed: true, step: { ...s.step, ...step } });\n found = true;\n } else {\n updated.push(s);\n }\n }\n if (!found) {\n updated.push({\n id: updated.length,\n completed: true,\n step,\n });\n }\n return updated;\n}\n", "type": "registry:component", "target": "components/common/tx/steps.ts" }, @@ -128,25 +128,25 @@ }, { "path": "registry/nexus-elements/common/types/transaction-flow.ts", - "content": "import {\n type NexusSDK,\n type SUPPORTED_CHAINS_IDS,\n type SUPPORTED_TOKENS,\n} from \"@avail-project/nexus-core\";\nimport { type Address } from \"viem\";\n\nexport type TransactionFlowType = \"bridge\" | \"transfer\";\n\nexport interface TransactionFlowInputs {\n chain: SUPPORTED_CHAINS_IDS;\n token: SUPPORTED_TOKENS;\n amount?: string;\n recipient?: `0x${string}`;\n}\n\nexport interface TransactionFlowPrefill {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n}\n\ntype BridgeOptions = NonNullable[1]>;\n\nexport type TransactionFlowEvent =\n NonNullable extends (event: infer E) => void\n ? E\n : never;\n\nexport type TransactionFlowOnEvent = NonNullable;\n\nexport interface TransactionFlowExecuteParams {\n token: SUPPORTED_TOKENS;\n amount: bigint;\n toChainId: SUPPORTED_CHAINS_IDS;\n recipient: `0x${string}`;\n sourceChains?: number[];\n onEvent: TransactionFlowOnEvent;\n}\n\nexport type TransactionFlowExecutor = (\n params: TransactionFlowExecuteParams,\n) => Promise<{ explorerUrl: string } | null>;\n\nexport type SourceCoverageState = \"healthy\" | \"warning\" | \"error\";\n\nexport interface SourceSelectionValidation {\n coverageState: SourceCoverageState;\n isBelowRequired: boolean;\n missingToProceed: string;\n missingToSafety: string;\n}\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport { type Address } from \"viem\";\n\ntype NexusClient = ReturnType;\n\n// v2 uses string token symbols (toTokenSymbol) with number chain IDs\nexport type TransactionFlowType = \"bridge\" | \"transfer\";\n\nexport interface TransactionFlowInputs {\n chain: number;\n token: string;\n amount?: string;\n recipient?: `0x${string}`;\n}\n\nexport interface TransactionFlowPrefill {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n}\n\n// v2 bridge onEvent uses typed discriminated union, not NEXUS_EVENTS\nexport type TransactionFlowEvent =\n | { type: \"status\"; status: string }\n | { type: \"plan_preview\"; plan: { steps: unknown[] } }\n | { type: \"plan_confirmed\"; plan: { steps: unknown[] } }\n | { type: \"plan_progress\"; stepType: string; state: string; step: unknown };\n\nexport type TransactionFlowOnEvent = (event: TransactionFlowEvent) => void;\n\nexport interface TransactionFlowExecuteParams {\n token: string;\n amount: bigint;\n toChainId: number;\n recipient: `0x${string}`;\n sources?: number[];\n onEvent: TransactionFlowOnEvent;\n}\n\nexport type TransactionFlowExecutor = (\n params: TransactionFlowExecuteParams,\n) => Promise<{ explorerUrl: string } | null>;\n\nexport type SourceCoverageState = \"healthy\" | \"warning\" | \"error\";\n\nexport interface SourceSelectionValidation {\n coverageState: SourceCoverageState;\n isBelowRequired: boolean;\n missingToProceed: string;\n missingToSafety: string;\n}\n", "type": "registry:component", "target": "components/common/types/transaction-flow.ts" }, { "path": "registry/nexus-elements/common/utils/constant.ts", - "content": "import { SUPPORTED_CHAINS } from \"@avail-project/nexus-core\";\nimport { formatUnits, parseUnits } from \"viem\";\n\nexport const SHORT_CHAIN_NAME: Record = {\n [SUPPORTED_CHAINS.ETHEREUM]: \"Ethereum\",\n [SUPPORTED_CHAINS.BASE]: \"Base\",\n [SUPPORTED_CHAINS.ARBITRUM]: \"Arbitrum\",\n [SUPPORTED_CHAINS.OPTIMISM]: \"Optimism\",\n [SUPPORTED_CHAINS.POLYGON]: \"Polygon\",\n [SUPPORTED_CHAINS.AVALANCHE]: \"Avalanche\",\n [SUPPORTED_CHAINS.SCROLL]: \"Scroll\",\n [SUPPORTED_CHAINS.MEGAETH]: \"MegaETH\",\n [SUPPORTED_CHAINS.KAIA]: \"Kaia\",\n [SUPPORTED_CHAINS.BNB]: \"BNB\",\n [SUPPORTED_CHAINS.MONAD]: \"Monad\",\n [SUPPORTED_CHAINS.HYPEREVM]: \"HyperEVM\",\n [SUPPORTED_CHAINS.CITREA]: \"Citrea\",\n // [SUPPORTED_CHAINS.TRON]: \"Tron\",\n [SUPPORTED_CHAINS.SEPOLIA]: \"Sepolia\",\n [SUPPORTED_CHAINS.BASE_SEPOLIA]: \"Base Sepolia\",\n [SUPPORTED_CHAINS.ARBITRUM_SEPOLIA]: \"Arbitrum Sepolia\",\n [SUPPORTED_CHAINS.OPTIMISM_SEPOLIA]: \"Optimism Sepolia\",\n [SUPPORTED_CHAINS.POLYGON_AMOY]: \"Polygon Amoy\",\n [SUPPORTED_CHAINS.MONAD_TESTNET]: \"Monad Testnet\",\n // [SUPPORTED_CHAINS.TRON_SHASTA]: \"Tron Shasta\",\n} as const;\n\nconst DEFAULT_SAFETY_MARGIN = 0.01; // 1%\n\n/**\n * Compute an amount string for fraction buttons (25%, 50%, 75%, 100%).\n *\n * @param balanceStr - user's balance as a human decimal string (e.g. \"12.345\") OR as base-unit integer string if `balanceIsBaseUnits` true\n * @param fraction - fraction e.g. 0.25, 0.5, 0.75, 1\n * @param decimals - token decimals (6 for USDC/USDT, 18 for ETH)\n * @param safetyMargin - 0.01 for 1% default\n * @param balanceIsBaseUnits - if true, balanceStr is already base units integer string (wei / smallest unit)\n * @returns decimal string clipped to token decimals (rounded down)\n */\nexport function computeAmountFromFraction(\n balanceStr: string,\n fraction: number,\n decimals: number,\n safetyMargin = DEFAULT_SAFETY_MARGIN,\n balanceIsBaseUnits = false,\n): string {\n if (!balanceStr) return \"0\";\n\n // parse balance into base units (BigInt)\n const balanceUnits: bigint = balanceIsBaseUnits\n ? BigInt(balanceStr)\n : parseUnits(balanceStr, decimals);\n\n if (balanceUnits === BigInt(0)) return \"0\";\n\n // Use an integer precision multiplier to avoid FP issues\n const PREC = 1_000_000; // 1e6 precision for fraction & safety margin\n const safetyMul = BigInt(Math.max(0, Math.floor((1 - safetyMargin) * PREC))); // (1 - safetyMargin) * PREC\n const fractionMul = BigInt(Math.max(0, Math.floor(fraction * PREC))); // fraction * PREC\n\n // Apply safety margin: floor(balance * (1 - safetyMargin))\n const maxAfterSafety = (balanceUnits * safetyMul) / BigInt(PREC);\n\n // Apply fraction and floor: floor(maxAfterSafety * fraction)\n let desiredUnits = (maxAfterSafety * fractionMul) / BigInt(PREC);\n\n // Extra clamp just in case\n if (desiredUnits > balanceUnits) desiredUnits = balanceUnits;\n if (desiredUnits < BigInt(0)) desiredUnits = BigInt(0);\n\n // format back to human readable decimal string with token decimals (formatUnits truncates/keeps decimals)\n // formatUnits will produce exactly decimals digits if fractional part exists; we'll strip trailing zeros.\n const raw = formatUnits(desiredUnits, decimals);\n // strip trailing zeros and possible trailing dot\n return raw\n .replace(/(\\.\\d*?[1-9])0+$/u, \"$1\")\n .replace(/\\.0+$/u, \"\")\n .replace(/^\\.$/u, \"0\");\n}\n\nexport const usdFormatter = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n});\n\nconst usdFormatterPrecise = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 3,\n maximumFractionDigits: 3,\n});\n\n/**\n * Formats USD values for UI.\n * Values between 0 and 0.001 are shown as \"< $0.001\".\n * Values between 0.001 and 0.01 are shown with 3 decimals.\n */\nexport function formatUsdForDisplay(value: number): string {\n if (!Number.isFinite(value)) return usdFormatter.format(0);\n const absValue = Math.abs(value);\n\n if (absValue === 0) return usdFormatter.format(0);\n if (absValue < 0.001) return \"< $0.001\";\n if (absValue < 0.01) {\n return usdFormatterPrecise.format(value);\n }\n\n return usdFormatter.format(value);\n}\n", + "content": "import { formatUnits, parseUnits } from \"viem\";\n\n// v2: SUPPORTED_CHAINS removed — using literal EVM chain IDs\nexport const SHORT_CHAIN_NAME: Record = {\n 1: \"Ethereum\",\n 8453: \"Base\",\n 42161: \"Arbitrum\",\n 10: \"Optimism\",\n 137: \"Polygon\",\n 43114: \"Avalanche\",\n 534352: \"Scroll\",\n 6342: \"MegaETH\",\n 8217: \"Kaia\",\n 56: \"BNB\",\n 10143: \"Monad\",\n 999: \"HyperEVM\",\n 5115: \"Citrea\",\n 11155111: \"Sepolia\",\n 84532: \"Base Sepolia\",\n 421614: \"Arbitrum Sepolia\",\n 11155420: \"Optimism Sepolia\",\n 80002: \"Polygon Amoy\",\n} as const;\n\nconst DEFAULT_SAFETY_MARGIN = 0.01; // 1%\n\n/**\n * Compute an amount string for fraction buttons (25%, 50%, 75%, 100%).\n *\n * @param balanceStr - user's balance as a human decimal string (e.g. \"12.345\") OR as base-unit integer string if `balanceIsBaseUnits` true\n * @param fraction - fraction e.g. 0.25, 0.5, 0.75, 1\n * @param decimals - token decimals (6 for USDC/USDT, 18 for ETH)\n * @param safetyMargin - 0.01 for 1% default\n * @param balanceIsBaseUnits - if true, balanceStr is already base units integer string (wei / smallest unit)\n * @returns decimal string clipped to token decimals (rounded down)\n */\nexport function computeAmountFromFraction(\n balanceStr: string,\n fraction: number,\n decimals: number,\n safetyMargin = DEFAULT_SAFETY_MARGIN,\n balanceIsBaseUnits = false,\n): string {\n if (!balanceStr) return \"0\";\n\n // parse balance into base units (BigInt)\n const balanceUnits: bigint = balanceIsBaseUnits\n ? BigInt(balanceStr)\n : parseUnits(balanceStr, decimals);\n\n if (balanceUnits === BigInt(0)) return \"0\";\n\n // Use an integer precision multiplier to avoid FP issues\n const PREC = 1_000_000; // 1e6 precision for fraction & safety margin\n const safetyMul = BigInt(Math.max(0, Math.floor((1 - safetyMargin) * PREC))); // (1 - safetyMargin) * PREC\n const fractionMul = BigInt(Math.max(0, Math.floor(fraction * PREC))); // fraction * PREC\n\n // Apply safety margin: floor(balance * (1 - safetyMargin))\n const maxAfterSafety = (balanceUnits * safetyMul) / BigInt(PREC);\n\n // Apply fraction and floor: floor(maxAfterSafety * fraction)\n let desiredUnits = (maxAfterSafety * fractionMul) / BigInt(PREC);\n\n // Extra clamp just in case\n if (desiredUnits > balanceUnits) desiredUnits = balanceUnits;\n if (desiredUnits < BigInt(0)) desiredUnits = BigInt(0);\n\n // format back to human readable decimal string with token decimals (formatUnits truncates/keeps decimals)\n // formatUnits will produce exactly decimals digits if fractional part exists; we'll strip trailing zeros.\n const raw = formatUnits(desiredUnits, decimals);\n // strip trailing zeros and possible trailing dot\n return raw\n .replace(/(\\.\\d*?[1-9])0+$/u, \"$1\")\n .replace(/\\.0+$/u, \"\")\n .replace(/^\\.$/u, \"0\");\n}\n\nexport const usdFormatter = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n});\n\nconst usdFormatterPrecise = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n minimumFractionDigits: 3,\n maximumFractionDigits: 3,\n});\n\n/**\n * Formats USD values for UI.\n * Values between 0 and 0.001 are shown as \"< $0.001\".\n * Values between 0.001 and 0.01 are shown with 3 decimals.\n */\nexport function formatUsdForDisplay(value: number): string {\n if (!Number.isFinite(value)) return usdFormatter.format(0);\n const absValue = Math.abs(value);\n\n if (absValue === 0) return usdFormatter.format(0);\n if (absValue < 0.001) return \"< $0.001\";\n if (absValue < 0.01) {\n return usdFormatterPrecise.format(value);\n }\n\n return usdFormatter.format(value);\n}\n", "type": "registry:component", "target": "components/common/utils/constant.ts" }, { "path": "registry/nexus-elements/common/utils/token-pricing.ts", - "content": "import type { SupportedChainsAndTokensResult } from \"@avail-project/nexus-core\";\n\nconst COINBASE_SPOT_API_BASE = \"https://api.coinbase.com/v2/prices\";\nconst COINBASE_EXCHANGE_RATES_API_BASE =\n \"https://api.coinbase.com/v2/exchange-rates\";\n\nexport const DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS = 4_000;\nexport const USD_PEGGED_FALLBACK_RATE = 1;\nexport const DEFAULT_USD_PEGGED_TOKEN_SYMBOLS = [\n \"USDT\",\n \"USDC\",\n \"USDS\",\n \"DAI\",\n \"USDM\",\n \"FDUSD\",\n \"BUSD\",\n \"TUSD\",\n \"PYUSD\",\n \"GUSD\",\n \"LUSD\",\n \"USDE\",\n \"USDP\",\n] as const;\n\ntype CoinbaseSpotPriceResponse = {\n data?: {\n amount?: string | number;\n };\n};\n\ntype CoinbaseExchangeRatesResponse = {\n data?: {\n rates?: Record;\n };\n};\n\ntype SupportedTokenMetadata = {\n symbol?: string;\n equivalentCurrency?: string;\n};\n\ntype SupportedChainMetadata = {\n tokens?: SupportedTokenMetadata[];\n};\n\nexport function normalizeTokenSymbol(tokenSymbol: string): string {\n return tokenSymbol.trim().toUpperCase();\n}\n\nexport function toFinitePositiveNumber(value: unknown): number | null {\n const parsed = Number.parseFloat(String(value));\n if (!Number.isFinite(parsed) || parsed <= 0) {\n return null;\n }\n return parsed;\n}\n\nexport function getCoinbaseSymbolCandidates(tokenSymbol: string): string[] {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return [];\n\n const baseSymbol = normalized.split(/[._-]/)[0] ?? normalized;\n const wrappedBase =\n baseSymbol.startsWith(\"W\") && baseSymbol.length > 3\n ? baseSymbol.slice(1)\n : null;\n\n return Array.from(\n new Set(\n [normalized, baseSymbol, wrappedBase].filter(\n (symbol): symbol is string => Boolean(symbol),\n ),\n ),\n );\n}\n\nexport function buildUsdPeggedSymbolSet(\n supportedChains: SupportedChainsAndTokensResult | null,\n baseSymbols: Iterable = DEFAULT_USD_PEGGED_TOKEN_SYMBOLS,\n): Set {\n const symbolSet = new Set(baseSymbols);\n\n for (const chain of (supportedChains ?? []) as SupportedChainMetadata[]) {\n for (const token of chain.tokens ?? []) {\n const symbol = normalizeTokenSymbol(token.symbol ?? \"\");\n const equivalent = normalizeTokenSymbol(token.equivalentCurrency ?? \"\");\n if (!symbol) continue;\n\n if (equivalent && symbolSet.has(equivalent)) {\n symbolSet.add(symbol);\n }\n }\n }\n\n return symbolSet;\n}\n\nasync function fetchJsonWithTimeout(\n url: string,\n requestTimeoutMs: number,\n): Promise {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);\n try {\n const response = await fetch(url, {\n signal: controller.signal,\n });\n if (!response.ok) return null;\n return (await response.json()) as T;\n } catch {\n return null;\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nexport async function fetchCoinbaseUsdRate(\n tokenSymbol: string,\n requestTimeoutMs = DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS,\n): Promise {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return null;\n\n for (const candidate of getCoinbaseSymbolCandidates(normalized)) {\n const spotBody = await fetchJsonWithTimeout(\n `${COINBASE_SPOT_API_BASE}/${encodeURIComponent(candidate)}-USD/spot`,\n requestTimeoutMs,\n );\n const spotAmount = toFinitePositiveNumber(spotBody?.data?.amount);\n if (spotAmount) return spotAmount;\n\n const exchangeRatesBody =\n await fetchJsonWithTimeout(\n `${COINBASE_EXCHANGE_RATES_API_BASE}?currency=${encodeURIComponent(candidate)}`,\n requestTimeoutMs,\n );\n const exchangeRatesAmount = toFinitePositiveNumber(\n exchangeRatesBody?.data?.rates?.USD,\n );\n if (exchangeRatesAmount) return exchangeRatesAmount;\n }\n\n return null;\n}\n", + "content": "// v2: getSupportedChains() return type is inferred directly; define a structural type\ntype SupportedChainsAndTokensResult = readonly {\n tokens?: { symbol?: string; equivalentCurrency?: string }[];\n [key: string]: unknown;\n}[];\n\nconst COINBASE_SPOT_API_BASE = \"https://api.coinbase.com/v2/prices\";\nconst COINBASE_EXCHANGE_RATES_API_BASE =\n \"https://api.coinbase.com/v2/exchange-rates\";\n\nexport const DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS = 4_000;\nexport const USD_PEGGED_FALLBACK_RATE = 1;\nexport const DEFAULT_USD_PEGGED_TOKEN_SYMBOLS = [\n \"USDT\",\n \"USDC\",\n \"USDS\",\n \"DAI\",\n \"USDM\",\n \"FDUSD\",\n \"BUSD\",\n \"TUSD\",\n \"PYUSD\",\n \"GUSD\",\n \"LUSD\",\n \"USDE\",\n \"USDP\",\n] as const;\n\ntype CoinbaseSpotPriceResponse = {\n data?: {\n amount?: string | number;\n };\n};\n\ntype CoinbaseExchangeRatesResponse = {\n data?: {\n rates?: Record;\n };\n};\n\ntype SupportedTokenMetadata = {\n symbol?: string;\n equivalentCurrency?: string;\n};\n\ntype SupportedChainMetadata = {\n tokens?: SupportedTokenMetadata[];\n};\n\nexport function normalizeTokenSymbol(tokenSymbol: string): string {\n return tokenSymbol.trim().toUpperCase();\n}\n\nexport function toFinitePositiveNumber(value: unknown): number | null {\n const parsed = Number.parseFloat(String(value));\n if (!Number.isFinite(parsed) || parsed <= 0) {\n return null;\n }\n return parsed;\n}\n\nexport function getCoinbaseSymbolCandidates(tokenSymbol: string): string[] {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return [];\n\n const baseSymbol = normalized.split(/[._-]/)[0] ?? normalized;\n const wrappedBase =\n baseSymbol.startsWith(\"W\") && baseSymbol.length > 3\n ? baseSymbol.slice(1)\n : null;\n\n return Array.from(\n new Set(\n [normalized, baseSymbol, wrappedBase].filter(\n (symbol): symbol is string => Boolean(symbol),\n ),\n ),\n );\n}\n\nexport function buildUsdPeggedSymbolSet(\n supportedChains: SupportedChainsAndTokensResult | null,\n baseSymbols: Iterable = DEFAULT_USD_PEGGED_TOKEN_SYMBOLS,\n): Set {\n const symbolSet = new Set(baseSymbols);\n\n for (const chain of (supportedChains ?? []) as SupportedChainMetadata[]) {\n for (const token of chain.tokens ?? []) {\n const symbol = normalizeTokenSymbol(token.symbol ?? \"\");\n const equivalent = normalizeTokenSymbol(token.equivalentCurrency ?? \"\");\n if (!symbol) continue;\n\n if (equivalent && symbolSet.has(equivalent)) {\n symbolSet.add(symbol);\n }\n }\n }\n\n return symbolSet;\n}\n\nasync function fetchJsonWithTimeout(\n url: string,\n requestTimeoutMs: number,\n): Promise {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);\n try {\n const response = await fetch(url, {\n signal: controller.signal,\n });\n if (!response.ok) return null;\n return (await response.json()) as T;\n } catch {\n return null;\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nexport async function fetchCoinbaseUsdRate(\n tokenSymbol: string,\n requestTimeoutMs = DEFAULT_COINBASE_PRICE_REQUEST_TIMEOUT_MS,\n): Promise {\n const normalized = normalizeTokenSymbol(tokenSymbol);\n if (!normalized) return null;\n\n for (const candidate of getCoinbaseSymbolCandidates(normalized)) {\n const spotBody = await fetchJsonWithTimeout(\n `${COINBASE_SPOT_API_BASE}/${encodeURIComponent(candidate)}-USD/spot`,\n requestTimeoutMs,\n );\n const spotAmount = toFinitePositiveNumber(spotBody?.data?.amount);\n if (spotAmount) return spotAmount;\n\n const exchangeRatesBody =\n await fetchJsonWithTimeout(\n `${COINBASE_EXCHANGE_RATES_API_BASE}?currency=${encodeURIComponent(candidate)}`,\n requestTimeoutMs,\n );\n const exchangeRatesAmount = toFinitePositiveNumber(\n exchangeRatesBody?.data?.rates?.USD,\n );\n if (exchangeRatesAmount) return exchangeRatesAmount;\n }\n\n return null;\n}\n", "type": "registry:component", "target": "components/common/utils/token-pricing.ts" }, { "path": "registry/nexus-elements/common/utils/transaction-flow.ts", - "content": "import {\n formatUnits,\n type NexusNetwork,\n NexusSDK,\n SUPPORTED_CHAINS,\n type SUPPORTED_CHAINS_IDS,\n type SUPPORTED_TOKENS,\n} from \"@avail-project/nexus-core\";\nimport { type Address } from \"viem\";\n\nconst MAX_AMOUNT_REGEX = /^\\d*\\.?\\d+$/;\n\nexport const MAX_AMOUNT_DEBOUNCE_MS = 300;\n\nexport const normalizeMaxAmount = (\n maxAmount?: string | number,\n): string | undefined => {\n if (maxAmount === undefined || maxAmount === null) return undefined;\n const value = String(maxAmount).trim();\n if (!value || value === \".\" || !MAX_AMOUNT_REGEX.test(value)) {\n return undefined;\n }\n const parsed = Number.parseFloat(value);\n if (!Number.isFinite(parsed) || parsed <= 0) return undefined;\n return value;\n};\n\nexport const clampAmountToMax = ({\n amount,\n maxAmount,\n nexusSDK,\n token,\n chainId,\n}: {\n amount: string;\n maxAmount?: string;\n nexusSDK: NexusSDK;\n token: SUPPORTED_TOKENS;\n chainId: SUPPORTED_CHAINS_IDS;\n}): string => {\n if (!maxAmount) return amount;\n try {\n const amountRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n token,\n chainId,\n );\n const maxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxAmount,\n token,\n chainId,\n );\n return amountRaw > maxRaw ? maxAmount : amount;\n } catch {\n return amount;\n }\n};\n\nexport const formatAmountForDisplay = (\n amount: bigint,\n decimals: number | undefined,\n nexusSDK: NexusSDK,\n): string => {\n if (typeof decimals !== \"number\") return amount.toString();\n const formatted = formatUnits(amount, decimals);\n if (!formatted.includes(\".\")) return formatted;\n const [whole, fraction] = formatted.split(\".\");\n const trimmedFraction = fraction.slice(0, 6).replace(/0+$/, \"\");\n if (!trimmedFraction && whole === \"0\" && amount > BigInt(0)) {\n return \"0.000001\";\n }\n return trimmedFraction ? `${whole}.${trimmedFraction}` : whole;\n};\n\nexport const buildInitialInputs = ({\n type,\n network,\n connectedAddress,\n prefill,\n}: {\n type: \"bridge\" | \"transfer\";\n network: NexusNetwork;\n connectedAddress?: Address;\n prefill?: {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n };\n}) => {\n return {\n chain:\n (prefill?.chainId as SUPPORTED_CHAINS_IDS) ??\n (network === \"testnet\"\n ? SUPPORTED_CHAINS.SEPOLIA\n : SUPPORTED_CHAINS.ETHEREUM),\n token: (prefill?.token as SUPPORTED_TOKENS) ?? \"USDC\",\n amount: prefill?.amount ?? undefined,\n recipient:\n (prefill?.recipient as `0x${string}`) ??\n (type === \"bridge\" ? connectedAddress : undefined),\n };\n};\n\nexport const getCoverageDecimals = ({\n type,\n token,\n chainId,\n fallback,\n}: {\n type: \"bridge\" | \"transfer\";\n token?: SUPPORTED_TOKENS;\n chainId?: SUPPORTED_CHAINS_IDS;\n fallback: number | undefined;\n}) => {\n if (token === \"USDM\") return 18;\n if (\n type === \"bridge\" &&\n token === \"USDC\" &&\n chainId === SUPPORTED_CHAINS.BNB\n ) {\n return 18;\n }\n return fallback;\n};\n", + "content": "import type { createNexusClient } from \"@avail-project/nexus-sdk-v2\";\nimport type { NexusNetwork } from \"@avail-project/nexus-sdk-v2\";\nimport { formatUnits, type Address } from \"viem\";\n\ntype NexusClient = ReturnType;\n\nconst MAX_AMOUNT_REGEX = /^\\d*\\.?\\d+$/;\n\n// v2 chain IDs for defaults\nconst SEPOLIA_CHAIN_ID = 11155111;\nconst ETHEREUM_CHAIN_ID = 1;\n// v2: BNB chain ID for edge-case decimal override\nconst BNB_CHAIN_ID = 56;\n\nexport const MAX_AMOUNT_DEBOUNCE_MS = 300;\n\nexport const normalizeMaxAmount = (\n maxAmount?: string | number,\n): string | undefined => {\n if (maxAmount === undefined || maxAmount === null) return undefined;\n const value = String(maxAmount).trim();\n if (!value || value === \".\" || !MAX_AMOUNT_REGEX.test(value)) {\n return undefined;\n }\n const parsed = Number.parseFloat(value);\n if (!Number.isFinite(parsed) || parsed <= 0) return undefined;\n return value;\n};\n\nexport const clampAmountToMax = ({\n amount,\n maxAmount,\n nexusSDK,\n token,\n chainId,\n}: {\n amount: string;\n maxAmount?: string;\n nexusSDK: NexusClient;\n token: string;\n chainId: number;\n}): string => {\n if (!maxAmount) return amount;\n try {\n // v2: convertTokenReadableAmountToBigInt(amount, tokenSymbol, chainId)\n const amountRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n amount,\n token,\n chainId,\n );\n const maxRaw = nexusSDK.convertTokenReadableAmountToBigInt(\n maxAmount,\n token,\n chainId,\n );\n return amountRaw > maxRaw ? maxAmount : amount;\n } catch {\n return amount;\n }\n};\n\nexport const formatAmountForDisplay = (\n amount: bigint,\n decimals: number | undefined,\n // nexusSDK kept for API compatibility but formatUnits is now imported directly\n _nexusSDK: NexusClient,\n): string => {\n if (typeof decimals !== \"number\") return amount.toString();\n const formatted = formatUnits(amount, decimals);\n if (!formatted.includes(\".\")) return formatted;\n const [whole, fraction] = formatted.split(\".\");\n const trimmedFraction = fraction.slice(0, 6).replace(/0+$/, \"\");\n if (!trimmedFraction && whole === \"0\" && amount > BigInt(0)) {\n return \"0.000001\";\n }\n return trimmedFraction ? `${whole}.${trimmedFraction}` : whole;\n};\n\nexport const buildInitialInputs = ({\n type,\n network,\n connectedAddress,\n prefill,\n}: {\n type: \"bridge\" | \"transfer\";\n network: NexusNetwork;\n connectedAddress?: Address;\n prefill?: {\n token: string;\n chainId: number;\n amount?: string;\n recipient?: Address;\n };\n}) => {\n return {\n // v2 uses plain number chain IDs and string token symbols\n chain:\n prefill?.chainId ??\n (network === \"testnet\" ? SEPOLIA_CHAIN_ID : ETHEREUM_CHAIN_ID),\n token: prefill?.token ?? \"USDC\",\n amount: prefill?.amount ?? undefined,\n recipient:\n (prefill?.recipient as `0x${string}`) ??\n (type === \"bridge\" ? connectedAddress : undefined),\n };\n};\n\nexport const getCoverageDecimals = ({\n type,\n token,\n chainId,\n fallback,\n}: {\n type: \"bridge\" | \"transfer\";\n token?: string;\n chainId?: number;\n fallback: number | undefined;\n}) => {\n if (token === \"USDM\") return 18;\n if (type === \"bridge\" && token === \"USDC\" && chainId === BNB_CHAIN_ID) {\n return 18;\n }\n return fallback;\n};\n", "type": "registry:component", "target": "components/common/utils/transaction-flow.ts" } diff --git a/registry.json b/registry.json index d493857..6c17804 100644 --- a/registry.json +++ b/registry.json @@ -248,7 +248,7 @@ } ], "dependencies": [ - "@avail-project/nexus-core@1.2.0", + "@avail-project/nexus-sdk-v2", "lucide-react", "viem" ], @@ -604,7 +604,7 @@ } ], "dependencies": [ - "@avail-project/nexus-core@1.2.0", + "@avail-project/nexus-sdk-v2", "clsx", "lucide-react", "tailwind-merge", @@ -638,6 +638,7 @@ ], "dependencies": [ "@radix-ui/react-dialog", + "@radix-ui/react-visually-hidden", "lucide-react" ], "registryDependencies": [ @@ -797,7 +798,7 @@ } ], "dependencies": [ - "@avail-project/nexus-core@1.2.0", + "@avail-project/nexus-sdk-v2", "lucide-react", "viem" ], @@ -865,7 +866,7 @@ } ], "dependencies": [ - "@avail-project/nexus-core@1.2.0", + "@avail-project/nexus-sdk-v2", "wagmi" ] }, @@ -1118,7 +1119,7 @@ } ], "dependencies": [ - "@avail-project/nexus-core@1.2.0", + "@avail-project/nexus-sdk-v2", "lucide-react", "viem" ], @@ -1326,7 +1327,7 @@ } ], "dependencies": [ - "@avail-project/nexus-core@1.2.0", + "@avail-project/nexus-sdk-v2", "lucide-react", "viem" ], @@ -1359,7 +1360,7 @@ } ], "dependencies": [ - "@avail-project/nexus-core@1.2.0", + "@avail-project/nexus-sdk-v2", "lucide-react" ], "registryDependencies": [ @@ -1506,7 +1507,7 @@ } ], "dependencies": [ - "@avail-project/nexus-core@1.2.0", + "@avail-project/nexus-sdk-v2", "lucide-react" ], "registryDependencies": [ diff --git a/registry/nexus-elements/bridge-deposit/components/allowance-modal.tsx b/registry/nexus-elements/bridge-deposit/components/allowance-modal.tsx index 7a87534..8697a06 100644 --- a/registry/nexus-elements/bridge-deposit/components/allowance-modal.tsx +++ b/registry/nexus-elements/bridge-deposit/components/allowance-modal.tsx @@ -12,11 +12,10 @@ import { Input } from "../../ui/input"; import { Label } from "../../ui/label"; import { type AllowanceHookSource, - CHAIN_METADATA, - formatTokenBalance, type OnAllowanceHookData, - parseUnits, -} from "@avail-project/nexus-core"; +} from "@avail-project/nexus-sdk-v2"; +import { formatTokenBalance } from "@avail-project/nexus-sdk-v2/utils"; +import { parseUnits } from "viem"; import { useNexusError } from "../../common"; import { Loader2 } from "lucide-react"; @@ -231,7 +230,8 @@ const AllowanceModal: FC = ({
{source.chain.name}, - "onChange" | "value" - > { +interface AmountInputProps extends Omit< + React.InputHTMLAttributes, + "onChange" | "value" +> { value?: string; onChange?: (value: string) => void; - bridgableBalance?: UserAsset; - destinationChain: SUPPORTED_CHAINS_IDS; + bridgableBalance?: TokenBalance; + // v2: chainBalances are chain-level entries; destinationChain used for finding chain decimals + destinationChain: number; } const AmountInput = ({ @@ -61,9 +58,10 @@ const AmountInput = ({ const { nexusSDK, loading } = useNexus(); const hasSelectedSources = - bridgableBalance && bridgableBalance.breakdown.length > 0; + // v2: chainBalances replaces breakdown + bridgableBalance && (bridgableBalance.chainBalances?.length ?? 0) > 0; const hasBalance = - hasSelectedSources && Number.parseFloat(bridgableBalance.balance) > 0; + hasSelectedSources && Number.parseFloat(bridgableBalance!.balance) > 0; return (
@@ -114,7 +112,8 @@ const AmountInput = ({ const amount = computeAmountFromFraction( bridgableBalance.balance, option.value, - bridgableBalance?.breakdown.find( + // v2: find chain decimals from chainBalances + bridgableBalance?.chainBalances?.find( (chain) => chain?.chain?.id === destinationChain, )?.decimals ?? bridgableBalance?.decimals, SAFETY_MARGIN, @@ -138,7 +137,7 @@ const AmountInput = ({
- {bridgableBalance?.breakdown.map((chain) => { + {bridgableBalance?.chainBalances?.map((chain: any) => { if (Number.parseFloat(chain.balance) === 0) return null; return ( @@ -146,7 +145,7 @@ const AmountInput = ({
{chain.chain.name}

- ${chain.balanceInFiat.toFixed(2)} + {/* v2: value is a string USD amount */} + ${Number.parseFloat(chain.value ?? "0").toFixed(2)}

diff --git a/registry/nexus-elements/bridge-deposit/components/container.tsx b/registry/nexus-elements/bridge-deposit/components/container.tsx index f168a59..a023ce4 100644 --- a/registry/nexus-elements/bridge-deposit/components/container.tsx +++ b/registry/nexus-elements/bridge-deposit/components/container.tsx @@ -4,7 +4,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/tabs"; import SimpleDeposit from "./simple-deposit"; import { useNexus } from "../../nexus/NexusProvider"; import { type BaseDepositProps } from "../deposit"; -import { truncateAddress } from "@avail-project/nexus-core"; +import { truncateAddress } from "@avail-project/nexus-sdk-v2/utils"; interface ContainerProps extends BaseDepositProps { fiatSubheading?: string; diff --git a/registry/nexus-elements/bridge-deposit/components/simple-deposit.tsx b/registry/nexus-elements/bridge-deposit/components/simple-deposit.tsx index 2c5831a..c5a1b9c 100644 --- a/registry/nexus-elements/bridge-deposit/components/simple-deposit.tsx +++ b/registry/nexus-elements/bridge-deposit/components/simple-deposit.tsx @@ -19,7 +19,7 @@ import { useNexus } from "../../nexus/NexusProvider"; import useDeposit from "../hooks/useDeposit"; import { LoaderPinwheel, X } from "lucide-react"; import { Skeleton } from "../../ui/skeleton"; -import { type SUPPORTED_TOKENS } from "@avail-project/nexus-core"; +// v2: SUPPORTED_TOKENS removed — token is plain string interface SimpleDepositProps extends BaseDepositProps { destinationLabel?: string; @@ -66,9 +66,9 @@ const SimpleDeposit = ({ } = useDeposit({ token: token ?? "USDC", chain, - nexusSDK, - intent, - bridgableBalance, + nexusSDK: nexusSDK as any, // v2: NexusClient — cast to bypass v1 NexusSDK boundary + intent: intent as any, // v2: OnIntentHookData ref + bridgableBalance: bridgableBalance as any, // v2: UserAsset[] allowance, chainOptions, address, @@ -159,7 +159,7 @@ const SimpleDeposit = ({ <>
@@ -180,8 +180,8 @@ const SimpleDeposit = ({ {simulation && inputs?.amount && ( <> { return Number.parseFloat( - bridgableBalance?.breakdown?.find((b) => b.chain?.id === chain) + // v2: chainBalances replaces breakdown + bridgableBalance?.chainBalances?.find((b: any) => b.chain?.id === chain) ?.balance ?? "0", ); }, [bridgableBalance, chain]); const amountSpend = useMemo(() => { + // v2: BridgeIntent.selectedSources[0].token.decimals for decimals + const decimals = intent?.selectedSources?.[0]?.token?.decimals; const amountToFormat = intent ? Number.parseFloat(requiredAmount ?? "0") + Number.parseFloat(intent?.fees?.total ?? "0") : (requiredAmount ?? "0"); return formatTokenBalance(amountToFormat, { symbol: tokenSymbol, - decimals: intent?.token?.decimals, + decimals, }); }, [requiredAmount, intent, tokenSymbol]); @@ -65,13 +65,21 @@ const SourceBreakdown = ({ return [ { chainID: chain, - chainLogo: CHAIN_METADATA[chain]?.logo, - chainName: CHAIN_METADATA[chain]?.name ?? "Destination", + chainLogo: undefined, + chainName: "Destination", amount: requiredAmount ?? "0", contractAddress: "", }, ]; - const baseSources: ReadableIntentSource[] = intent?.sources ?? []; + // v2: BridgeIntent.selectedSources has {chain:{id,name,logo}, token:{contractAddress,...}} shape + const rawSources = intent?.selectedSources ?? []; + const baseSources: ReadableIntentSource[] = rawSources.map((s: BridgeIntent["selectedSources"][0]) => ({ + chainID: s.chain.id, + chainLogo: s.chain.logo, + chainName: s.chain.name, + amount: s.amount, + contractAddress: (s.token?.contractAddress ?? "") as `0x${string}`, + })); const requiredAmountNumber = Number(requiredAmount ?? "0"); const destUsed = Math.max( Math.min(requiredAmountNumber, fundsOnDestination), @@ -80,19 +88,18 @@ const SourceBreakdown = ({ if (destUsed <= 0) { return baseSources; } - const allSources = intent?.allSources ?? []; - const destDetails = allSources?.find?.( - (s: ReadableIntentSource) => s?.chainID === chain, - ); + // v2: BridgeIntent.availableSources replaces allSources + const allSources = intent?.availableSources ?? []; + const destDetails = allSources?.find?.((s) => s?.chain?.id === chain); const hasDest = baseSources?.some?.( (s: ReadableIntentSource) => s?.chainID === chain, ); const destSource = { chainID: chain, - chainLogo: destDetails?.chainLogo, - chainName: destDetails?.chainName ?? "Destination", + chainLogo: destDetails?.chain?.logo, + chainName: destDetails?.chain?.name ?? "Destination", amount: destUsed.toString(), - contractAddress: destDetails?.contractAddress ?? "", + contractAddress: destDetails?.token?.contractAddress ?? "", }; if (hasDest) { return baseSources.map((s: ReadableIntentSource) => diff --git a/registry/nexus-elements/bridge-deposit/components/source-select.tsx b/registry/nexus-elements/bridge-deposit/components/source-select.tsx index 1c35dd5..05b1aa5 100644 --- a/registry/nexus-elements/bridge-deposit/components/source-select.tsx +++ b/registry/nexus-elements/bridge-deposit/components/source-select.tsx @@ -2,14 +2,11 @@ import { ChevronDown } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover"; import { Label } from "../../ui/label"; import { Checkbox } from "../../ui/checkbox"; -import { - type SUPPORTED_TOKENS, - type UserAsset, -} from "@avail-project/nexus-core"; +import { type TokenBalance } from "@avail-project/nexus-sdk-v2"; interface SourceSelectProps { - token?: SUPPORTED_TOKENS; - balanceBreakdown?: UserAsset; + token?: string; + balanceBreakdown?: TokenBalance; selected?: number[]; onChange?: (selected: number[]) => void; disabled?: boolean; @@ -31,17 +28,18 @@ const SourceSelect = ({ }; const allSelected = - Boolean(balanceBreakdown?.breakdown.length) && - balanceBreakdown?.breakdown.every((chain) => + // v2: chainBalances replaces breakdown + Boolean(balanceBreakdown?.chainBalances?.length) && + balanceBreakdown?.chainBalances?.every((chain) => selected.includes(chain.chain.id) ); const toggleAll = () => { - if (!onChange || disabled || !balanceBreakdown?.breakdown.length) return; + if (!onChange || disabled || !balanceBreakdown?.chainBalances?.length) return; if (allSelected) { onChange([]); } else { - onChange(balanceBreakdown.breakdown.map((chain) => chain.chain.id)); + onChange(balanceBreakdown.chainBalances.map((chain) => chain.chain.id)); } }; @@ -58,7 +56,7 @@ const SourceSelect = ({ - {balanceBreakdown && balanceBreakdown?.breakdown.length > 0 ? ( + {balanceBreakdown && (balanceBreakdown?.chainBalances?.length ?? 0) > 0 ? ( <>
- {balanceBreakdown?.breakdown.map((chain) => ( + {balanceBreakdown?.chainBalances?.map((chain) => (
{chain.chain.name} Omit; } diff --git a/registry/nexus-elements/bridge-deposit/hooks/useDeposit.ts b/registry/nexus-elements/bridge-deposit/hooks/useDeposit.ts index 91ad535..a9ca6fd 100644 --- a/registry/nexus-elements/bridge-deposit/hooks/useDeposit.ts +++ b/registry/nexus-elements/bridge-deposit/hooks/useDeposit.ts @@ -1,22 +1,17 @@ "use client"; +import type { createNexusClient } from "@avail-project/nexus-sdk-v2"; import { - type SUPPORTED_CHAINS_IDS, - type SUPPORTED_TOKENS, - type UserAsset, - NexusSDK, type OnIntentHookData, type OnAllowanceHookData, type ExecuteParams, type BridgeAndExecuteParams, type BridgeAndExecuteResult, type BridgeAndExecuteSimulationResult, - NEXUS_EVENTS, - type BridgeStepType, - CHAIN_METADATA, - formatTokenBalance, - formatUnits, -} from "@avail-project/nexus-core"; + type TokenBalance, + type ChainBalance, +} from "@avail-project/nexus-sdk-v2"; +import { formatTokenBalance } from "@avail-project/nexus-sdk-v2/utils"; import { useEffect, useMemo, @@ -27,7 +22,7 @@ import { type RefObject, } from "react"; import { useNexus } from "../../nexus/NexusProvider"; -import { type Address } from "viem"; +import { formatUnits, type Address } from "viem"; import { useDebouncedValue, useNexusError, @@ -36,6 +31,15 @@ import { useTransactionSteps, } from "../../common"; +type NexusClient = ReturnType; + +// v2 uses a generic step shape +type BridgeStepType = { + typeID?: string; + type?: string; + [key: string]: unknown; +}; + export type DepositStatus = | "idle" | "previewing" @@ -44,25 +48,25 @@ export type DepositStatus = | "error"; interface DepositInputs { - chain: SUPPORTED_CHAINS_IDS; + chain: number; amount?: string; selectedSources: number[]; } interface UseDepositProps { - token: SUPPORTED_TOKENS; - chain: SUPPORTED_CHAINS_IDS; - nexusSDK: NexusSDK | null; + token: string; + chain: number; + nexusSDK: NexusClient | null; intent: RefObject; allowance: RefObject; - bridgableBalance: UserAsset[] | null; + bridgableBalance: TokenBalance[] | null; fetchBridgableBalance: () => Promise; chainOptions?: { id: number; name: string; logo: string }[]; address: Address; executeBuilder?: ( - token: SUPPORTED_TOKENS, + token: string, amount: string, - chainId: SUPPORTED_CHAINS_IDS, + chainId: number, userAddress: `0x${string}`, ) => Omit; executeConfig?: Omit; @@ -214,25 +218,26 @@ const useDeposit = ({ const tokenBalance = bridgableBalance?.find((bal) => bal?.symbol === token); if (!tokenBalance) return undefined; - const nonZeroBreakdown = tokenBalance.breakdown.filter( + // v2: chainBalances replaces breakdown + const nonZeroChainBalances = tokenBalance.chainBalances.filter( (chain) => Number.parseFloat(chain.balance) > 0, ); - const totalBalance = nonZeroBreakdown.reduce( + const totalBalance = nonZeroChainBalances.reduce( (sum, chain) => sum + Number.parseFloat(chain.balance), 0, ); - const totalBalanceInFiat = nonZeroBreakdown.reduce( - (sum, chain) => sum + chain.balanceInFiat, + const totalValue = nonZeroChainBalances.reduce( + (sum, chain) => sum + Number.parseFloat(chain.value ?? "0"), 0, ); return { ...tokenBalance, balance: totalBalance.toString(), - balanceInFiat: totalBalanceInFiat, - breakdown: nonZeroBreakdown, + value: totalValue.toString(), + chainBalances: nonZeroChainBalances, }; }, [bridgableBalance, token]); @@ -241,27 +246,28 @@ const useDeposit = ({ if (!tokenBalance) return undefined; const selectedSourcesSet = new Set(inputs.selectedSources); - const filteredBreakdown = tokenBalance.breakdown.filter( + // v2: chainBalances replaces breakdown + const filteredChainBalances = tokenBalance.chainBalances.filter( (chain) => selectedSourcesSet.has(chain.chain.id) && Number.parseFloat(chain.balance) > 0, ); - const totalBalance = filteredBreakdown.reduce( + const totalBalance = filteredChainBalances.reduce( (sum, chain) => sum + Number.parseFloat(chain.balance), 0, ); - const totalBalanceInFiat = filteredBreakdown.reduce( - (sum, chain) => sum + chain.balanceInFiat, + const totalValue = filteredChainBalances.reduce( + (sum, chain) => sum + Number.parseFloat(chain.value ?? "0"), 0, ); return { ...tokenBalance, balance: totalBalance.toString(), - balanceInFiat: totalBalanceInFiat, - breakdown: filteredBreakdown, + value: totalValue.toString(), + chainBalances: filteredChainBalances, }; }, [bridgableBalance, token, inputs.selectedSources]); @@ -286,30 +292,31 @@ const useDeposit = ({ gasUsd: 0, gasFormatted: "0", }; - const native = CHAIN_METADATA[chain]?.nativeCurrency; - const nativeSymbol = native.symbol; - const nativeDecimals = native.decimals; + // v2: ExecuteSimulation.estimatedTotalCost (bigint) replaces gasFee + const costRaw = simulation?.executeSimulation?.estimatedTotalCost; + const nativeDecimals = 18; + const nativeSymbol = "ETH"; const gasFormatted = - formatTokenBalance(simulation?.executeSimulation?.gasFee, { + formatTokenBalance(costRaw, { symbol: nativeSymbol, decimals: nativeDecimals, }) ?? "0"; const gasUnits = Number.parseFloat( - formatUnits(simulation?.executeSimulation?.gasFee, nativeDecimals), + formatUnits(costRaw ?? BigInt(0), nativeDecimals), ); const gasUsd = getFiatValue(gasUnits, nativeSymbol); if (simulation?.bridgeSimulation) { const tokenDecimals = - simulation?.bridgeSimulation?.intent?.token?.decimals; + simulation?.bridgeSimulation?.intent?.destination?.token?.decimals; const bridgeFormatted = formatTokenBalance(simulation?.bridgeSimulation?.intent?.fees?.total, { symbol: token, decimals: tokenDecimals, }) ?? "0"; const bridgeUsd = getFiatValue( - Number.parseFloat(simulation?.bridgeSimulation?.intent?.fees?.total), + Number.parseFloat(simulation?.bridgeSimulation?.intent?.fees?.total ?? "0"), token, ); @@ -352,11 +359,12 @@ const useDeposit = ({ executeBuilder ? executeBuilder(token, inputs.amount, inputs.chain, address) : executeConfig; + // v2: BridgeAndExecuteParams uses toTokenSymbol, toAmountRaw, sources const params: BridgeAndExecuteParams = { - token, - amount: amountBigInt, + toTokenSymbol: token, + toAmountRaw: amountBigInt, toChainId: inputs.chain, - sourceChains: inputs.selectedSources, + sources: inputs.selectedSources, execute: executeParams as Omit, waitForReceipt: true, }; @@ -365,18 +373,20 @@ const useDeposit = ({ params, { onEvent: (event) => { - if (event.name === NEXUS_EVENTS.STEPS_LIST) { - const list = Array.isArray(event.args) ? event.args : []; - onStepsList(list); + // v2: events use event.type (not event.name) + if (event.type === "plan_preview") { + const planEvent = event as { type: string; plan: { steps?: unknown[] } }; + const list = planEvent.plan?.steps ?? []; + onStepsList(list as Parameters[0]); } - if (event.name === NEXUS_EVENTS.STEP_COMPLETE) { + if (event.type === "plan_progress") { if ( !transactionStartedRef.current && - event.args.type === "INTENT_HASH_SIGNED" + (event as { stepType?: string })?.stepType === "bridge_request_signing" ) { transactionStartedRef.current = true; } - onStepComplete(event.args); + onStepComplete(event as Parameters[0]); } }, }, @@ -447,11 +457,12 @@ const useDeposit = ({ executeBuilder ? executeBuilder(token, amountToUse, inputs.chain, address) : executeConfig; + // v2: BridgeAndExecuteParams uses toTokenSymbol, toAmountRaw, sources const params: BridgeAndExecuteParams = { - token, - amount: amountBigInt, + toTokenSymbol: token, + toAmountRaw: amountBigInt, toChainId: inputs.chain, - sourceChains: inputs.selectedSources, + sources: inputs.selectedSources, execute: executeParams as Omit, waitForReceipt: false, }; @@ -538,8 +549,8 @@ const useDeposit = ({ usePolling( Boolean(simulation?.bridgeSimulation?.intent) && - !isProcessing && - !isSuccess, + !isProcessing && + !isSuccess, async () => { await refreshSimulation(); }, diff --git a/registry/nexus-elements/common/hooks/useNexusError.ts b/registry/nexus-elements/common/hooks/useNexusError.ts index f28c3d9..4cf9c82 100644 --- a/registry/nexus-elements/common/hooks/useNexusError.ts +++ b/registry/nexus-elements/common/hooks/useNexusError.ts @@ -1,4 +1,4 @@ -import { ERROR_CODES, NexusError } from "@avail-project/nexus-core"; +import { ERROR_CODES, NexusError } from "@avail-project/nexus-sdk-v2"; const DEFAULT_ERROR_MESSAGE = "Oops! Something went wrong. Please try again."; const USER_REJECTED_MESSAGE = "Transaction was rejected in your wallet."; @@ -18,8 +18,6 @@ const ERROR_MESSAGE_BY_CODE: Partial> = { "Chain metadata is unavailable for this route. Please try another chain.", [ERROR_CODES.ASSET_NOT_FOUND]: "Requested asset was not found in your balances.", - [ERROR_CODES.COSMOS_ERROR]: - "Cosmos-side operation failed. Please retry in a moment.", [ERROR_CODES.TOKEN_NOT_SUPPORTED]: "Selected token is not supported for this route.", [ERROR_CODES.UNIVERSE_NOT_SUPPORTED]: @@ -30,21 +28,14 @@ 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, [ERROR_CODES.USER_DENIED_ALLOWANCE]: USER_REJECTED_MESSAGE, [ERROR_CODES.USER_DENIED_INTENT_SIGNATURE]: USER_REJECTED_MESSAGE, - [ERROR_CODES.USER_DENIED_SIWE_SIGNATURE]: USER_REJECTED_MESSAGE, [ERROR_CODES.INSUFFICIENT_BALANCE]: "Insufficient balance to proceed.", [ERROR_CODES.WALLET_NOT_CONNECTED]: "Wallet is not connected. Connect your wallet and try again.", - [ERROR_CODES.FETCH_GAS_PRICE_FAILED]: - "Unable to estimate gas right now. Please retry.", [ERROR_CODES.SIMULATION_FAILED]: "Simulation failed. Please review your inputs and try again.", [ERROR_CODES.QUOTE_FAILED]: @@ -56,7 +47,8 @@ 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]: + // v2: RFF_FEE_EXPIRED was removed; use string key for forward compat + ["RFF_FEE_EXPIRED"]: "Quote expired. Refresh and try again.", [ERROR_CODES.INVALID_INPUT]: "Some transaction inputs are invalid. Please review and try again.", @@ -96,8 +88,7 @@ function looksLikeUserRejection(err: unknown): boolean { return ( err.code === ERROR_CODES.USER_DENIED_ALLOWANCE || err.code === ERROR_CODES.USER_DENIED_INTENT || - err.code === ERROR_CODES.USER_DENIED_INTENT_SIGNATURE || - err.code === ERROR_CODES.USER_DENIED_SIWE_SIGNATURE + err.code === ERROR_CODES.USER_DENIED_INTENT_SIGNATURE ); } @@ -151,8 +142,8 @@ function handler(err: unknown) { return { code: err.code, message: mappedMessage, - context: err?.data?.context, - details: err?.data?.details, + context: (err as unknown as { data?: { context?: unknown } })?.data?.context, + details: (err as unknown as { data?: { details?: unknown } })?.data?.details, }; } diff --git a/registry/nexus-elements/common/hooks/useTransactionExecution.ts b/registry/nexus-elements/common/hooks/useTransactionExecution.ts index f272b8e..b94038b 100644 --- a/registry/nexus-elements/common/hooks/useTransactionExecution.ts +++ b/registry/nexus-elements/common/hooks/useTransactionExecution.ts @@ -1,10 +1,5 @@ -import { - type BridgeStepType, - NEXUS_EVENTS, - type NexusSDK, - type OnAllowanceHookData, - type OnIntentHookData, -} from "@avail-project/nexus-core"; +import type { createNexusClient } from "@avail-project/nexus-sdk-v2"; +import type { OnAllowanceHookData, OnIntentHookData } from "@avail-project/nexus-sdk-v2"; import { type Dispatch, type RefObject, @@ -20,6 +15,8 @@ import { type TransactionFlowInputs, } from "../types/transaction-flow"; +type NexusClient = ReturnType; + interface NexusErrorInfo { code: string; message: string; @@ -29,9 +26,12 @@ interface NexusErrorInfo { type NexusErrorHandler = (error: unknown) => NexusErrorInfo; +// v2 plan_progress step types for bridge +const BRIDGE_STEP_INTENT_SIGNED = "request_signing"; + interface UseTransactionExecutionProps { operationName: "bridge" | "transfer"; - nexusSDK: NexusSDK | null; + nexusSDK: NexusClient | null; intent: RefObject; allowance: RefObject; inputs: TransactionFlowInputs; @@ -45,8 +45,8 @@ interface UseTransactionExecutionProps { areInputsValid: boolean; executeTransaction: TransactionFlowExecutor; getMaxForCurrentSelection: () => Promise; - onStepsList: (steps: BridgeStepType[]) => void; - onStepComplete: (step: BridgeStepType) => void; + onStepsList: (steps: { typeID?: string; type?: string; [key: string]: unknown }[]) => void; + onStepComplete: (step: { typeID?: string; type?: string; [key: string]: unknown }) => void; resetSteps: () => void; setStatus: (status: TransactionStatus) => void; resetInputs: () => void; @@ -154,8 +154,15 @@ export function useTransactionExecution({ commitLockRef.current = true; const currentRunId = ++runIdRef.current; let didEnterExecutingState = false; + // Declared here (outside try/catch) so both the event handler and the catch block + // can read/write it — prevents the catch from clobbering event-driven completions + let completedFromEvent = false; const cleanupSupersededExecution = () => { if (!didEnterExecutingState) return; + // Don't tear down the dialog if an event already handled success/failure — + // resetInputs() inside onSuccess triggers invalidatePendingExecution which + // increments runIdRef, making this branch fire spuriously. + if (completedFromEvent) return; setRefreshing(false); setIsDialogOpen(false); setLastExplorerUrl(""); @@ -165,6 +172,7 @@ export function useTransactionExecution({ setStatus("idle"); }; + try { if ( !inputs?.amount || @@ -199,6 +207,7 @@ export function useTransactionExecution({ return; } + // v2: convertTokenReadableAmountToBigInt(amount, tokenSymbol, chainId) const amountBigInt = nexusSDK.convertTokenReadableAmountToBigInt( inputs.amount, inputs.token, @@ -249,22 +258,69 @@ export function useTransactionExecution({ setLastExplorerUrl(""); setAppliedSourceSelectionKey(sourceSelectionKey); + // Terminal step types — when state:"completed" fires on these, the operation is done + const TERMINAL_STEP_TYPES = new Set([ + "bridge_fill", // bridge & transfer final fill + "destination_swap", // swap final step + ]); + + // v2 onEvent uses typed discriminated union: { type, ... } const onEvent = (event: TransactionFlowEvent) => { if (currentRunId !== runIdRef.current) return; - if (event.name === NEXUS_EVENTS.STEPS_LIST) { - const list = Array.isArray(event.args) ? event.args : []; - onStepsList(list as BridgeStepType[]); + + if (event.type === "plan_preview") { + // Seed UI with the step list from the plan + type StepShape = { typeID?: string; type?: string; [key: string]: unknown }; + const steps = ((event as { type: string; plan: { steps: StepShape[] } }).plan?.steps ?? []) as StepShape[]; + onStepsList(steps); } - if (event.name === NEXUS_EVENTS.STEP_COMPLETE) { - if ( - !Array.isArray(event.args) && - "type" in event.args && - event.args.type === "INTENT_HASH_SIGNED" - ) { - stopwatch.start(); + + if (event.type === "plan_progress") { + const progressEvent = event as { + type: string; + stepType: string; + state: string; + step: { typeID?: string; type?: string; [key: string]: unknown }; + error?: string; + }; + + // Always mark step as complete/updated in UI + onStepComplete(progressEvent.step); + + const isTerminal = TERMINAL_STEP_TYPES.has(progressEvent.stepType); + + if (progressEvent.state === "failed") { + // Any step failure → abort + if (!completedFromEvent) { + completedFromEvent = true; + const errorMessage = progressEvent.error ?? "Transaction failed"; + stopwatch.stop(); + setTxError(errorMessage); + onError?.(errorMessage); + setStatus("error"); + } + return; } - if (!Array.isArray(event.args)) { - onStepComplete(event.args as BridgeStepType); + + if (isTerminal && progressEvent.state === "completed") { + // Terminal step completed → success + if (!completedFromEvent) { + completedFromEvent = true; + stopwatch.stop(); + // explorerUrl is on the event itself, not the step object + const explorerUrl = (event as { explorerUrl?: string }).explorerUrl; + if (explorerUrl) setLastExplorerUrl(explorerUrl); + void onSuccess(explorerUrl); + } + } + } + + if (event.type === "status") { + const statusEvent = event as { type: string; status: string }; + if (statusEvent.status === "completed" && !completedFromEvent) { + completedFromEvent = true; + stopwatch.stop(); + void onSuccess(undefined); } } }; @@ -274,24 +330,43 @@ export function useTransactionExecution({ amount: amountBigInt, toChainId: inputs.chain, recipient: inputs.recipient, - sourceChains: sourceChainsForSdk, + sources: sourceChainsForSdk, onEvent, }); if (currentRunId !== runIdRef.current) { - cleanupSupersededExecution(); - return; + cleanupSupersededExecution(); // no-op when completedFromEvent=true + if (!completedFromEvent) return; // only bail if not already completed + // else fall through — still want to capture explorerUrl from the result } if (!transactionResult) { - throw new Error("Transaction rejected by user"); + if (!completedFromEvent) { + throw new Error("Transaction rejected by user"); + } + // Already handled via events + return; } - setLastExplorerUrl(transactionResult.explorerUrl); - await onSuccess(transactionResult.explorerUrl); + + // SDK promise resolved — use result for explorerUrl if event-driven success didn't set it + if (!completedFromEvent) { + // Fallback: SDK resolved but we never got a terminal event (e.g. single-step flows) + setLastExplorerUrl(transactionResult.explorerUrl ?? ""); + await onSuccess(transactionResult.explorerUrl); + } else { + // Event-driven success already ran — capture the explorerUrl from the resolved result + if (transactionResult.explorerUrl) { + setLastExplorerUrl(transactionResult.explorerUrl); + } + } + } catch (error) { if (currentRunId !== runIdRef.current) { cleanupSupersededExecution(); return; } + // If event-driven success/failure already handled this transaction, ignore SDK-level errors + // (the SDK may throw or return oddly after a successful fill event) + if (completedFromEvent) return; const { message, code, context, details } = handleNexusError(error); console.error(`Fast ${operationName} transaction failed:`, { code, @@ -353,6 +428,10 @@ export function useTransactionExecution({ if (!refreshed || !intent.current) return; intent.current.allow(); setIsDialogOpen(true); + // Start the stopwatch AFTER the dialog opens so the isDialogOpen effect + // does not immediately reset it (the effect only resets when dialog is closed) + stopwatch.reset(); + stopwatch.start(); setTxError(null); })(); }; diff --git a/registry/nexus-elements/common/hooks/useTransactionFlow.ts b/registry/nexus-elements/common/hooks/useTransactionFlow.ts index 2cfc9e7..fe956df 100644 --- a/registry/nexus-elements/common/hooks/useTransactionFlow.ts +++ b/registry/nexus-elements/common/hooks/useTransactionFlow.ts @@ -1,12 +1,12 @@ -import { - type BridgeStepType, - type NexusNetwork, - NexusSDK, - type OnAllowanceHookData, - type OnIntentHookData, - parseUnits, - type UserAsset, -} from "@avail-project/nexus-core"; +import type { createNexusClient } from "@avail-project/nexus-sdk-v2"; +import type { + NexusNetwork, + OnAllowanceHookData, + OnIntentHookData, + TokenBalance, + ChainBalance, +} from "@avail-project/nexus-sdk-v2"; +import { parseUnits } from "viem"; import { useEffect, useMemo, @@ -40,13 +40,22 @@ import { normalizeMaxAmount, } from "../utils/transaction-flow"; +type NexusClient = ReturnType; + +// v2 uses a generic step shape; minimal type to satisfy getStepKey constraint +type BridgePlanStep = { + typeID?: string; + type?: string; + [key: string]: unknown; +}; + interface BaseTransactionFlowProps { type: TransactionFlowType; network: NexusNetwork; - nexusSDK: NexusSDK | null; + nexusSDK: NexusClient | null; intent: RefObject; allowance: RefObject; - bridgableBalance: UserAsset[] | null; + bridgableBalance: TokenBalance[] | null; prefill?: TransactionFlowPrefill; onComplete?: (explorerUrl?: string) => void; onStart?: () => void; @@ -153,7 +162,7 @@ export function useTransactionFlow(props: UseTransactionFlowProps) { onStepsList, onStepComplete, reset: resetSteps, - } = useTransactionSteps(); + } = useTransactionSteps(); const configuredMaxAmount = useMemo( () => normalizeMaxAmount(maxAmount), [maxAmount], @@ -177,9 +186,10 @@ export function useTransactionFlow(props: UseTransactionFlowProps) { }, [bridgableBalance, inputs?.token]); const availableSources = useMemo(() => { - const breakdown = filteredBridgableBalance?.breakdown ?? []; + // v2: chainBalances replaces breakdown + const chainBalances = filteredBridgableBalance?.chainBalances ?? []; const destinationChainId = inputs?.chain; - const nonZero = breakdown.filter((source) => { + const nonZero = chainBalances.filter((source: ChainBalance) => { if (Number.parseFloat(source.balance ?? "0") <= 0) return false; if (typeof destinationChainId === "number") { return source.chain.id !== destinationChainId; @@ -192,7 +202,7 @@ export function useTransactionFlow(props: UseTransactionFlowProps) { (a, b) => Number.parseFloat(b.balance) - Number.parseFloat(a.balance), ); } - return nonZero.sort((a, b) => { + return nonZero.sort((a: ChainBalance, b: ChainBalance) => { try { const aRaw = parseUnits(a.balance ?? "0", decimals); const bRaw = parseUnits(b.balance ?? "0", decimals); @@ -204,20 +214,20 @@ export function useTransactionFlow(props: UseTransactionFlowProps) { }); }, [ inputs?.chain, - filteredBridgableBalance?.breakdown, + filteredBridgableBalance?.chainBalances, filteredBridgableBalance?.decimals, nexusSDK, ]); const allAvailableSourceChainIds = useMemo( - () => availableSources.map((source) => source.chain.id), + () => availableSources.map((source: ChainBalance) => source.chain.id), [availableSources], ); const effectiveSelectedSourceChains = useMemo(() => { if (selectedSourceChains && selectedSourceChains.length > 0) { const availableSet = new Set(allAvailableSourceChainIds); - const filteredSelection = selectedSourceChains.filter((id) => + const filteredSelection = selectedSourceChains.filter((id: number) => availableSet.has(id), ); if (filteredSelection.length > 0) { @@ -237,7 +247,7 @@ export function useTransactionFlow(props: UseTransactionFlowProps) { if (!selectedSourceChains || selectedSourceChains.length === 0) { return "ALL"; } - return [...effectiveSelectedSourceChains].sort((a, b) => a - b).join("|"); + return [...effectiveSelectedSourceChains].sort((a: number, b: number) => a - b).join("|"); }, [ allAvailableSourceChainIds.length, effectiveSelectedSourceChains, @@ -247,17 +257,34 @@ export function useTransactionFlow(props: UseTransactionFlowProps) { sourceSelectionKey !== appliedSourceSelectionKey; const intentSourceSpendAmount = intent.current?.intent?.sourcesTotal; + /** + * v2: calculateMaxForBridge is removed. Use simulateBridge to get the max amount, + * or fall back to summing available source balances directly. + */ const getMaxForCurrentSelection = useCallback(async () => { if (!nexusSDK || !inputs?.token || !inputs?.chain) return undefined; - const maxBalAvailable = await nexusSDK.calculateMaxForBridge({ - token: inputs.token, - toChainId: inputs.chain, - recipient: inputs.recipient, - sourceChains: sourceChainsForSdk, - }); - if (!maxBalAvailable?.amount) return "0"; + + // Sum balances from selected sources as a direct proxy for max + const decimals = filteredBridgableBalance?.decimals; + if (typeof decimals !== "number") return "0"; + + const selectedSet = new Set( + sourceChainsForSdk ?? allAvailableSourceChainIds, + ); + const totalRaw = availableSources.reduce((sum: bigint, source: ChainBalance) => { + if (!selectedSet.has(source.chain.id)) return sum; + try { + return sum + parseUnits(source.balance ?? "0", decimals); + } catch { + return sum; + } + }, BigInt(0)); + + const totalReadable = formatToBigIntReadable(totalRaw, decimals); + if (!totalReadable) return "0"; + return clampAmountToMax({ - amount: maxBalAvailable.amount, + amount: totalReadable, maxAmount: configuredMaxAmount, nexusSDK, token: inputs.token, @@ -266,10 +293,12 @@ export function useTransactionFlow(props: UseTransactionFlowProps) { }, [ configuredMaxAmount, inputs?.chain, - inputs?.recipient, inputs?.token, nexusSDK, sourceChainsForSdk, + allAvailableSourceChainIds, + availableSources, + filteredBridgableBalance?.decimals, ]); const toggleSourceChain = useCallback( @@ -279,7 +308,7 @@ export function useTransactionFlow(props: UseTransactionFlowProps) { const current = prev && prev.length > 0 ? prev : allAvailableSourceChainIds; const next = current.includes(chainId) - ? current.filter((id) => id !== chainId) + ? current.filter((id: number) => id !== chainId) : [...current, chainId]; if (next.length === 0) { return current; @@ -306,14 +335,14 @@ export function useTransactionFlow(props: UseTransactionFlowProps) { const selectedTotalRaw = !nexusSDK || typeof decimals !== "number" ? BigInt(0) - : availableSources.reduce((sum, source) => { - if (!selectedChainSet.has(source.chain.id)) return sum; - try { - return sum + parseUnits(source.balance ?? "0", decimals); - } catch { - return sum; - } - }, BigInt(0)); + : availableSources.reduce((sum: bigint, source: ChainBalance) => { + if (!selectedChainSet.has(source.chain.id)) return sum; + try { + return sum + parseUnits(source.balance ?? "0", decimals); + } catch { + return sum; + } + }, BigInt(0)); const selectedTotal = !nexusSDK || typeof decimals !== "number" ? "0" @@ -335,6 +364,7 @@ export function useTransactionFlow(props: UseTransactionFlowProps) { } try { + // v2: convertTokenReadableAmountToBigInt(amount, tokenSymbol, chainId) const requiredRaw = nexusSDK.convertTokenReadableAmountToBigInt( amount, inputs.token, @@ -450,9 +480,9 @@ export function useTransactionFlow(props: UseTransactionFlowProps) { usePolling( Boolean(intent.current) && - !isDialogOpen && - !isSourceMenuOpen && - !hasPendingSourceSelectionChanges, + !isDialogOpen && + !isSourceMenuOpen && + !hasPendingSourceSelectionChanges, async () => { await refreshIntent(); }, @@ -522,6 +552,15 @@ export function useTransactionFlow(props: UseTransactionFlowProps) { setSelectedSourceChains(null); }, [inputs?.token]); + // Safety-net: stop the stopwatch as soon as status reaches a terminal state. + // This ensures the timer freezes even if the onEvent closure's stopwatch.stop() + // didn't fire (e.g. stale closure reference or SDK promise resolved oddly). + useEffect(() => { + if (state.status === "success" || state.status === "error") { + stopwatch.stop(); + } + }, [state.status, stopwatch]); + useEffect(() => { if (isDialogOpen) return; stopwatch.stop(); @@ -539,6 +578,7 @@ export function useTransactionFlow(props: UseTransactionFlowProps) { } }, [inputs, txError]); + return { inputs, setInputs, @@ -575,3 +615,14 @@ export function useTransactionFlow(props: UseTransactionFlowProps) { isInputsValid: areInputsValid, }; } + +/** Helper: format a bigint rawAmount with decimals into a readable decimal string. */ +function formatToBigIntReadable(raw: bigint, decimals: number): string { + if (raw <= BigInt(0)) return "0"; + const divisor = BigInt(10 ** decimals); + const whole = raw / divisor; + const fraction = raw % divisor; + if (fraction === BigInt(0)) return whole.toString(); + const fractionStr = fraction.toString().padStart(decimals, "0").replace(/0+$/, ""); + return `${whole}.${fractionStr}`; +} diff --git a/registry/nexus-elements/common/tx/steps.ts b/registry/nexus-elements/common/tx/steps.ts index 9d44a88..c2053da 100644 --- a/registry/nexus-elements/common/tx/steps.ts +++ b/registry/nexus-elements/common/tx/steps.ts @@ -1,37 +1,25 @@ -import type { SwapStepType } from "@avail-project/nexus-core"; +// v2: SwapStepType is no longer exported from the SDK — use a local step shape +// that matches v2 SwapPlanStep discriminator pattern +export type SwapStepType = { + typeID?: string; + type?: string; + [key: string]: unknown; +}; + import type { GenericStep } from "./types"; import { getStepKey } from "./types"; /** * Predefined expected steps for swaps to seed UI before events arrive. - * Kept here to avoid duplication across exact-in and exact-out hooks. + * Uses v2 stepType names that match SwapPlanProgressEvent.stepType discriminators. */ export const SWAP_EXPECTED_STEPS: SwapStepType[] = [ - { type: "SWAP_START", typeID: "SWAP_START" } as SwapStepType, - { type: "DETERMINING_SWAP", typeID: "DETERMINING_SWAP" } as SwapStepType, - { - type: "CREATE_PERMIT_FOR_SOURCE_SWAP", - typeID: - "CREATE_PERMIT_FOR_SOURCE_SWAP" as unknown as SwapStepType["typeID"], - } as SwapStepType, - { - type: "SOURCE_SWAP_BATCH_TX", - typeID: "SOURCE_SWAP_BATCH_TX", - } as SwapStepType, - { - type: "SOURCE_SWAP_HASH", - typeID: "SOURCE_SWAP_HASH" as unknown as SwapStepType["typeID"], - } as SwapStepType, - { type: "RFF_ID", typeID: "RFF_ID" } as SwapStepType, - { - type: "DESTINATION_SWAP_BATCH_TX", - typeID: "DESTINATION_SWAP_BATCH_TX", - } as SwapStepType, - { - type: "DESTINATION_SWAP_HASH", - typeID: "DESTINATION_SWAP_HASH" as unknown as SwapStepType["typeID"], - } as SwapStepType, - { type: "SWAP_COMPLETE", typeID: "SWAP_COMPLETE" } as SwapStepType, + { type: "source_swap", typeID: "source_swap" }, + { type: "eoa_to_ephemeral_transfer", typeID: "eoa_to_ephemeral_transfer" }, + { type: "bridge_deposit", typeID: "bridge_deposit" }, + { type: "bridge_intent_submission", typeID: "bridge_intent_submission" }, + { type: "bridge_fill", typeID: "bridge_fill" }, + { type: "destination_swap", typeID: "destination_swap" }, ]; export function seedSteps(expected: T[]): Array> { diff --git a/registry/nexus-elements/common/types/transaction-flow.ts b/registry/nexus-elements/common/types/transaction-flow.ts index 27e81bc..64d7a56 100644 --- a/registry/nexus-elements/common/types/transaction-flow.ts +++ b/registry/nexus-elements/common/types/transaction-flow.ts @@ -1,15 +1,14 @@ -import { - type NexusSDK, - type SUPPORTED_CHAINS_IDS, - type SUPPORTED_TOKENS, -} from "@avail-project/nexus-core"; +import type { createNexusClient } from "@avail-project/nexus-sdk-v2"; import { type Address } from "viem"; +type NexusClient = ReturnType; + +// v2 uses string token symbols (toTokenSymbol) with number chain IDs export type TransactionFlowType = "bridge" | "transfer"; export interface TransactionFlowInputs { - chain: SUPPORTED_CHAINS_IDS; - token: SUPPORTED_TOKENS; + chain: number; + token: string; amount?: string; recipient?: `0x${string}`; } @@ -21,21 +20,21 @@ export interface TransactionFlowPrefill { recipient?: Address; } -type BridgeOptions = NonNullable[1]>; - +// v2 bridge onEvent uses typed discriminated union, not NEXUS_EVENTS export type TransactionFlowEvent = - NonNullable extends (event: infer E) => void - ? E - : never; + | { type: "status"; status: string } + | { type: "plan_preview"; plan: { steps: unknown[] } } + | { type: "plan_confirmed"; plan: { steps: unknown[] } } + | { type: "plan_progress"; stepType: string; state: string; step: unknown }; -export type TransactionFlowOnEvent = NonNullable; +export type TransactionFlowOnEvent = (event: TransactionFlowEvent) => void; export interface TransactionFlowExecuteParams { - token: SUPPORTED_TOKENS; + token: string; amount: bigint; - toChainId: SUPPORTED_CHAINS_IDS; + toChainId: number; recipient: `0x${string}`; - sourceChains?: number[]; + sources?: number[]; onEvent: TransactionFlowOnEvent; } diff --git a/registry/nexus-elements/common/utils/constant.ts b/registry/nexus-elements/common/utils/constant.ts index 9f30e65..d0f8a66 100644 --- a/registry/nexus-elements/common/utils/constant.ts +++ b/registry/nexus-elements/common/utils/constant.ts @@ -1,28 +1,25 @@ -import { SUPPORTED_CHAINS } from "@avail-project/nexus-core"; import { formatUnits, parseUnits } from "viem"; +// v2: SUPPORTED_CHAINS removed — using literal EVM chain IDs export const SHORT_CHAIN_NAME: Record = { - [SUPPORTED_CHAINS.ETHEREUM]: "Ethereum", - [SUPPORTED_CHAINS.BASE]: "Base", - [SUPPORTED_CHAINS.ARBITRUM]: "Arbitrum", - [SUPPORTED_CHAINS.OPTIMISM]: "Optimism", - [SUPPORTED_CHAINS.POLYGON]: "Polygon", - [SUPPORTED_CHAINS.AVALANCHE]: "Avalanche", - [SUPPORTED_CHAINS.SCROLL]: "Scroll", - [SUPPORTED_CHAINS.MEGAETH]: "MegaETH", - [SUPPORTED_CHAINS.KAIA]: "Kaia", - [SUPPORTED_CHAINS.BNB]: "BNB", - [SUPPORTED_CHAINS.MONAD]: "Monad", - [SUPPORTED_CHAINS.HYPEREVM]: "HyperEVM", - [SUPPORTED_CHAINS.CITREA]: "Citrea", - // [SUPPORTED_CHAINS.TRON]: "Tron", - [SUPPORTED_CHAINS.SEPOLIA]: "Sepolia", - [SUPPORTED_CHAINS.BASE_SEPOLIA]: "Base Sepolia", - [SUPPORTED_CHAINS.ARBITRUM_SEPOLIA]: "Arbitrum Sepolia", - [SUPPORTED_CHAINS.OPTIMISM_SEPOLIA]: "Optimism Sepolia", - [SUPPORTED_CHAINS.POLYGON_AMOY]: "Polygon Amoy", - [SUPPORTED_CHAINS.MONAD_TESTNET]: "Monad Testnet", - // [SUPPORTED_CHAINS.TRON_SHASTA]: "Tron Shasta", + 1: "Ethereum", + 8453: "Base", + 42161: "Arbitrum", + 10: "Optimism", + 137: "Polygon", + 43114: "Avalanche", + 534352: "Scroll", + 6342: "MegaETH", + 8217: "Kaia", + 56: "BNB", + 10143: "Monad", + 999: "HyperEVM", + 5115: "Citrea", + 11155111: "Sepolia", + 84532: "Base Sepolia", + 421614: "Arbitrum Sepolia", + 11155420: "Optimism Sepolia", + 80002: "Polygon Amoy", } as const; const DEFAULT_SAFETY_MARGIN = 0.01; // 1% diff --git a/registry/nexus-elements/common/utils/token-pricing.ts b/registry/nexus-elements/common/utils/token-pricing.ts index 3b5eeaa..efdd784 100644 --- a/registry/nexus-elements/common/utils/token-pricing.ts +++ b/registry/nexus-elements/common/utils/token-pricing.ts @@ -1,4 +1,8 @@ -import type { SupportedChainsAndTokensResult } from "@avail-project/nexus-core"; +// v2: getSupportedChains() return type is inferred directly; define a structural type +type SupportedChainsAndTokensResult = readonly { + tokens?: { symbol?: string; equivalentCurrency?: string }[]; + [key: string]: unknown; +}[]; const COINBASE_SPOT_API_BASE = "https://api.coinbase.com/v2/prices"; const COINBASE_EXCHANGE_RATES_API_BASE = diff --git a/registry/nexus-elements/common/utils/transaction-flow.ts b/registry/nexus-elements/common/utils/transaction-flow.ts index 11ada46..1214af9 100644 --- a/registry/nexus-elements/common/utils/transaction-flow.ts +++ b/registry/nexus-elements/common/utils/transaction-flow.ts @@ -1,15 +1,17 @@ -import { - formatUnits, - type NexusNetwork, - NexusSDK, - SUPPORTED_CHAINS, - type SUPPORTED_CHAINS_IDS, - type SUPPORTED_TOKENS, -} from "@avail-project/nexus-core"; -import { type Address } from "viem"; +import type { createNexusClient } from "@avail-project/nexus-sdk-v2"; +import type { NexusNetwork } from "@avail-project/nexus-sdk-v2"; +import { formatUnits, type Address } from "viem"; + +type NexusClient = ReturnType; const MAX_AMOUNT_REGEX = /^\d*\.?\d+$/; +// v2 chain IDs for defaults +const SEPOLIA_CHAIN_ID = 11155111; +const ETHEREUM_CHAIN_ID = 1; +// v2: BNB chain ID for edge-case decimal override +const BNB_CHAIN_ID = 56; + export const MAX_AMOUNT_DEBOUNCE_MS = 300; export const normalizeMaxAmount = ( @@ -34,12 +36,13 @@ export const clampAmountToMax = ({ }: { amount: string; maxAmount?: string; - nexusSDK: NexusSDK; - token: SUPPORTED_TOKENS; - chainId: SUPPORTED_CHAINS_IDS; + nexusSDK: NexusClient; + token: string; + chainId: number; }): string => { if (!maxAmount) return amount; try { + // v2: convertTokenReadableAmountToBigInt(amount, tokenSymbol, chainId) const amountRaw = nexusSDK.convertTokenReadableAmountToBigInt( amount, token, @@ -59,7 +62,8 @@ export const clampAmountToMax = ({ export const formatAmountForDisplay = ( amount: bigint, decimals: number | undefined, - nexusSDK: NexusSDK, + // nexusSDK kept for API compatibility but formatUnits is now imported directly + _nexusSDK: NexusClient, ): string => { if (typeof decimals !== "number") return amount.toString(); const formatted = formatUnits(amount, decimals); @@ -89,12 +93,11 @@ export const buildInitialInputs = ({ }; }) => { return { + // v2 uses plain number chain IDs and string token symbols chain: - (prefill?.chainId as SUPPORTED_CHAINS_IDS) ?? - (network === "testnet" - ? SUPPORTED_CHAINS.SEPOLIA - : SUPPORTED_CHAINS.ETHEREUM), - token: (prefill?.token as SUPPORTED_TOKENS) ?? "USDC", + prefill?.chainId ?? + (network === "testnet" ? SEPOLIA_CHAIN_ID : ETHEREUM_CHAIN_ID), + token: prefill?.token ?? "USDC", amount: prefill?.amount ?? undefined, recipient: (prefill?.recipient as `0x${string}`) ?? @@ -109,16 +112,12 @@ export const getCoverageDecimals = ({ fallback, }: { type: "bridge" | "transfer"; - token?: SUPPORTED_TOKENS; - chainId?: SUPPORTED_CHAINS_IDS; + token?: string; + chainId?: number; fallback: number | undefined; }) => { if (token === "USDM") return 18; - if ( - type === "bridge" && - token === "USDC" && - chainId === SUPPORTED_CHAINS.BNB - ) { + if (type === "bridge" && token === "USDC" && chainId === BNB_CHAIN_ID) { return 18; } return fallback; diff --git a/registry/nexus-elements/deposit/components/amount-card.tsx b/registry/nexus-elements/deposit/components/amount-card.tsx index f439419..6ebd1aa 100644 --- a/registry/nexus-elements/deposit/components/amount-card.tsx +++ b/registry/nexus-elements/deposit/components/amount-card.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback, useRef, useEffect, useState, useMemo } from "react"; -import type { MaxSwapInput } from "@avail-project/nexus-core"; +import type { SwapMaxParams as MaxSwapInput } from "@avail-project/nexus-sdk-v2"; import { TokenIcon } from "./token-icon"; import { ErrorBanner } from "./error-banner"; import { PercentageSelector } from "./percentage-selector"; diff --git a/registry/nexus-elements/deposit/components/amount-container.tsx b/registry/nexus-elements/deposit/components/amount-container.tsx index fea9dc6..fb7a19a 100644 --- a/registry/nexus-elements/deposit/components/amount-container.tsx +++ b/registry/nexus-elements/deposit/components/amount-container.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback, useMemo, useState } from "react"; -import type { MaxSwapInput } from "@avail-project/nexus-core"; +import type { SwapMaxParams as MaxSwapInput } from "@avail-project/nexus-sdk-v2"; import WidgetHeader from "./widget-header"; import type { DepositWidgetContextValue } from "../types"; import AmountCard from "./amount-card"; @@ -30,7 +30,7 @@ const AmountContainer = ({ const hasPositiveSwapBalance = useMemo( () => (widget.swapBalance ?? []).some((asset) => - (asset.breakdown ?? []).some((chain) => { + (asset.chainBalances ?? []).some((chain) => { const amount = Number.parseFloat(chain.balance ?? "0"); return Number.isFinite(amount) && amount > 0; }), diff --git a/registry/nexus-elements/deposit/components/asset-selection-container.tsx b/registry/nexus-elements/deposit/components/asset-selection-container.tsx index b6f6adb..e573ad6 100644 --- a/registry/nexus-elements/deposit/components/asset-selection-container.tsx +++ b/registry/nexus-elements/deposit/components/asset-selection-container.tsx @@ -16,7 +16,8 @@ import { Tabs, TabsList, TabsTrigger } from "../../ui/tabs"; import { CardContent } from "../../ui/card"; import { Button } from "../../ui/button"; import TokenRow from "./token-row"; -import { formatTokenBalance, type UserAsset } from "@avail-project/nexus-core"; +import { formatTokenBalance } from "@avail-project/nexus-sdk-v2/utils"; +import type { TokenBalance, ChainBalance } from "@avail-project/nexus-sdk-v2"; import { usdFormatter } from "../../common"; import { X } from "lucide-react"; import { @@ -50,7 +51,8 @@ type ChainItemWithTokenMeta = ChainItem & { tokenLogo: string; }; -type AssetBreakdownWithOptionalIcon = UserAsset["breakdown"][number] & { +// v2: ChainBalance replaces UserAssetDatum["breakdown"][number] +type AssetBreakdownWithOptionalIcon = ChainBalance & { icon?: string; }; @@ -61,19 +63,20 @@ function parseNonNegativeNumber(value: unknown): number { } function getBreakdownTokenMeta( - breakdown: UserAsset["breakdown"][number], - asset: UserAsset, + breakdown: ChainBalance, + asset: TokenBalance ) { + // v2: logo replaces icon; value (string) replaces balanceInFiat (number) const breakdownIcon = (breakdown as AssetBreakdownWithOptionalIcon).icon; return { - symbol: breakdown.symbol, + symbol: asset.symbol, decimals: breakdown.decimals ?? asset.decimals, - logo: breakdownIcon || "", + logo: breakdownIcon || breakdown.chain.logo || asset.logo || "", }; } function transformSwapBalanceToTokens( - swapBalance: UserAsset[] | null, + swapBalance: TokenBalance[] | null, destination: Pick< DepositWidgetContextValue["destination"], "chainId" | "tokenAddress" | "tokenSymbol" @@ -91,7 +94,8 @@ function transformSwapBalanceToTokens( const allSourceIds = new Set(); swapBalance.forEach((asset) => { - asset.breakdown?.forEach((breakdown) => { + // v2: chainBalances replaces breakdown + asset.chainBalances?.forEach((breakdown) => { if (!breakdown.chain?.id || !breakdown.contractAddress) return; allSourceIds.add(`${breakdown.contractAddress}-${breakdown.chain.id}`); }); @@ -158,16 +162,16 @@ function transformSwapBalanceToTokens( const belowMinimumTokens: TokenWithMeta[] = []; for (const asset of swapBalance) { - if (!asset.breakdown?.length) continue; + if (!asset.chainBalances?.length) continue; const chainsBySymbol = new Map(); - asset.breakdown - .filter((b) => b.chain && b.balance) + asset.chainBalances + .filter((b: any) => b.chain && b.balance) .forEach((b) => { const balanceNum = parseFloat(b.balance); if (!Number.isFinite(balanceNum) || balanceNum <= 0) return; - const usdValue = parseNonNegativeNumber(b.balanceInFiat); + const usdValue = parseNonNegativeNumber(parseFloat(b.value ?? "0")); const tokenMeta = getBreakdownTokenMeta(b, asset); const existing = chainsBySymbol.get(tokenMeta.symbol) ?? []; existing.push({ diff --git a/registry/nexus-elements/deposit/components/confirmation-container.tsx b/registry/nexus-elements/deposit/components/confirmation-container.tsx index f3837b7..d2db818 100644 --- a/registry/nexus-elements/deposit/components/confirmation-container.tsx +++ b/registry/nexus-elements/deposit/components/confirmation-container.tsx @@ -10,7 +10,7 @@ import type { DepositWidgetContextValue } from "../types"; import { Button } from "../../ui/button"; import { CardContent } from "../../ui/card"; import { usdFormatter } from "../../common"; -import { formatTokenBalance } from "@avail-project/nexus-core"; +import { formatTokenBalance } from "@avail-project/nexus-sdk-v2/utils"; import { useNexus } from "../../nexus/NexusProvider"; import { BadgePercent, ShieldCheck } from "lucide-react"; import { formatFeeUsd, formatImpactPercent, formatSignedUsd } from "../utils"; @@ -78,7 +78,7 @@ const ConfirmationContainer = ({ tokenSymbol: source.symbol ?? "", tokenDecimals: source.decimals ?? 6, amount: source.balance ?? "0", - amountUsd: source.balanceInFiat, + amountUsd: parseFloat(source.value ?? "0"), isDestinationBalance: source.isDestinationBalance ?? false, }); } diff --git a/registry/nexus-elements/deposit/components/pay-using.tsx b/registry/nexus-elements/deposit/components/pay-using.tsx index d02c26d..c0f0422 100644 --- a/registry/nexus-elements/deposit/components/pay-using.tsx +++ b/registry/nexus-elements/deposit/components/pay-using.tsx @@ -7,7 +7,7 @@ import { MIN_SELECTABLE_SOURCE_BALANCE_USD, } from "../constants/widget"; import type { DestinationConfig, AssetFilterType } from "../types"; -import type { UserAsset } from "@avail-project/nexus-core"; +import type { TokenBalance as UserAsset } from "@avail-project/nexus-sdk-v2"; import { resolveDepositSourceSelection } from "../utils"; function parseUsdAmount(value?: string): number { @@ -64,7 +64,7 @@ function PayUsing({ const symbolBySourceId = new Map(); swapBalance.forEach((asset) => { - asset.breakdown?.forEach((breakdown) => { + asset.chainBalances?.forEach((breakdown) => { const chainId = breakdown.chain?.id; const tokenAddress = breakdown.contractAddress; if (!chainId || !tokenAddress) return; diff --git a/registry/nexus-elements/deposit/components/token-row.tsx b/registry/nexus-elements/deposit/components/token-row.tsx index 1617cc4..28b3bb2 100644 --- a/registry/nexus-elements/deposit/components/token-row.tsx +++ b/registry/nexus-elements/deposit/components/token-row.tsx @@ -4,7 +4,7 @@ import { ChevronDownIcon } from "./icons"; import type { Token } from "../types"; import { Checkbox } from "../../ui/checkbox"; import { usdFormatter } from "../../common"; -import { formatTokenBalance } from "@avail-project/nexus-core"; +import { formatTokenBalance } from "@avail-project/nexus-sdk-v2/utils"; import { TOKEN_IMAGES } from "../constants/assets"; import { CHAIN_ITEM_HEIGHT_PX, @@ -76,8 +76,8 @@ export function TokenRow({ {token.symbol} ({ * Handles selection of tokens/chains for cross-chain swaps. */ export function useAssetSelection( - swapBalance: UserAsset[] | null, + swapBalance: TokenBalance[] | null, destination: Pick< DestinationConfig, "chainId" | "tokenAddress" | "tokenSymbol" diff --git a/registry/nexus-elements/deposit/hooks/use-deposit-computed.ts b/registry/nexus-elements/deposit/hooks/use-deposit-computed.ts index 9ff0744..8b94896 100644 --- a/registry/nexus-elements/deposit/hooks/use-deposit-computed.ts +++ b/registry/nexus-elements/deposit/hooks/use-deposit-computed.ts @@ -2,19 +2,44 @@ import { useMemo } from "react"; import type { DestinationConfig, AssetSelectionState } from "../types"; +import type { createNexusClient } from "@avail-project/nexus-sdk-v2"; import type { - OnSwapIntentHookData, - NexusSDK, - UserAsset, -} from "@avail-project/nexus-core"; -import { CHAIN_METADATA, formatTokenBalance } from "@avail-project/nexus-core"; + SwapAndExecuteOnIntentHookData, + TokenBalance, + ChainBalance, +} from "@avail-project/nexus-sdk-v2"; +import { formatTokenBalance } from "@avail-project/nexus-sdk-v2/utils"; import { usdFormatter } from "../../common"; import type { SwapSkippedData } from "./use-deposit-state"; +type NexusClient = ReturnType; + const NATIVE_TOKEN_PLACEHOLDER_ADDRESS = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +// v2: CHAIN_METADATA not exported — hardcode well-known native symbols per chain +const NATIVE_SYMBOL_BY_CHAIN: Record = { + 1: "ETH", // Ethereum + 8453: "ETH", // Base + 42161: "ETH", // Arbitrum + 10: "ETH", // Optimism + 137: "MATIC", // Polygon + 43114: "AVAX", // Avalanche + 534352: "ETH", // Scroll + 56: "BNB", // BNB + 8217: "KAIA", // Kaia + 6342: "ETH", // MegaETH + 10143: "MON", // Monad + 999: "HYPE", // HyperEVM + 5115: "cBTC", // Citrea + 11155111: "ETH", // Sepolia + 84532: "ETH", // Base Sepolia + 421614: "ETH", // Arbitrum Sepolia + 11155420: "ETH", // Optimism Sepolia + 80002: "MATIC", // Polygon Amoy +}; + function normalizeAddress(address?: string | null): string { return (address ?? "").toLowerCase(); } @@ -37,9 +62,7 @@ function resolvePricingSymbol(params: { return fallbackSymbol; } - const nativeSymbol = - CHAIN_METADATA[chainId as keyof typeof CHAIN_METADATA]?.nativeCurrency - ?.symbol; + const nativeSymbol = NATIVE_SYMBOL_BY_CHAIN[chainId]; return nativeSymbol ?? fallbackSymbol; } @@ -73,9 +96,9 @@ function formatFeeKeyLabel(key: string): string { } interface UseDepositComputedProps { - swapBalance: UserAsset[] | null; + swapBalance: TokenBalance[] | null; assetSelection: AssetSelectionState; - activeIntent: OnSwapIntentHookData | null; + activeIntent: SwapAndExecuteOnIntentHookData | null; destination: DestinationConfig; inputAmount: string | undefined; exchangeRate: Record | null; @@ -83,7 +106,30 @@ interface UseDepositComputedProps { actualGasFeeUsd: number | null; swapSkippedData: SwapSkippedData | null; skipSwap: boolean; - nexusSDK: NexusSDK | null; + nexusSDK: NexusClient | null; +} + +/** + * v2: SwapAndExecuteIntent wraps the inner SwapIntent under .swap when swapRequired=true. + * This helper extracts compatible properties for display. + */ +type SwapIntentLike = { + sources?: { chain: { id: number; name: string; logo: string }; token: { contractAddress: string; symbol: string; decimals: number }; amount: string; value?: string }[]; + destination?: { chain?: { id?: number; name?: string }; value?: string; gas?: { value?: string } }; + feesAndBuffer?: { bridge?: Record & { total?: string; caGas?: string; protocol?: string; solver?: string }; buffer?: string }; +}; + +function getSwapIntentLike( + intent: SwapAndExecuteOnIntentHookData["intent"] | undefined | null, +): SwapIntentLike | null { + if (!intent) return null; + if (intent.swapRequired) { + // v2: inner SwapIntent is at intent.swap + const inner = (intent as { swapRequired: true; swap: unknown }).swap; + return inner as SwapIntentLike; + } + // swapRequired=false: no swap data available + return null; } /** @@ -95,13 +141,14 @@ export interface AvailableAsset { decimals: number; symbol: string; balance: string; - balanceInFiat?: number; + value?: string; // v2: USD value as string (from ChainBalance.value) + balanceInFiat?: number; // kept for legacy callers tokenLogo?: string; chainLogo?: string; chainName?: string; } -type AssetBreakdownWithOptionalIcon = UserAsset["breakdown"][number] & { +type AssetBreakdownWithOptionalIcon = ChainBalance & { icon?: string; }; @@ -132,8 +179,8 @@ export function useDepositComputed(props: UseDepositComputedProps) { const items: AvailableAsset[] = []; for (const asset of swapBalance) { - if (!asset?.breakdown?.length) continue; - for (const breakdown of asset.breakdown) { + if (!asset?.chainBalances?.length) continue; + for (const breakdown of asset.chainBalances) { if (!breakdown?.chain?.id || !breakdown.balance) continue; const numericBalance = Number.parseFloat(breakdown.balance); if (!Number.isFinite(numericBalance) || numericBalance <= 0) continue; @@ -144,17 +191,17 @@ export function useDepositComputed(props: UseDepositComputedProps) { chainId: breakdown.chain.id, tokenAddress: breakdown.contractAddress as `0x${string}`, decimals: breakdown.decimals ?? asset.decimals, - symbol: breakdown.symbol, + // v2: breakdown has no .symbol — use parent asset.symbol + symbol: asset.symbol, balance: breakdown.balance, - balanceInFiat: breakdown.balanceInFiat, + value: breakdown.value, tokenLogo: breakdownIcon || "", chainLogo: breakdown.chain.logo, chainName: breakdown.chain.name, }); } } - return items.toSorted( - (a, b) => (b.balanceInFiat ?? 0) - (a.balanceInFiat ?? 0), + return items.toSorted((a: AvailableAsset, b: AvailableAsset) => (parseFloat(b.value ?? "0") ?? 0) - (parseFloat(a.value ?? "0") ?? 0), ); }, [swapBalance]); @@ -166,7 +213,7 @@ export function useDepositComputed(props: UseDepositComputedProps) { availableAssets.reduce((sum, asset) => { const key = `${asset.tokenAddress}-${asset.chainId}`; if (assetSelection.selectedChainIds.has(key)) { - return sum + (asset.balanceInFiat ?? 0); + return sum + (parseFloat(asset.value ?? "0") ?? 0); } return sum; }, 0), @@ -183,7 +230,7 @@ export function useDepositComputed(props: UseDepositComputedProps) { 0, ) ?? 0; const usdBalance = - swapBalance?.reduce((acc, balance) => acc + balance.balanceInFiat, 0) ?? + swapBalance?.reduce((acc, balance) => acc + parseFloat(balance.value ?? "0"), 0) ?? 0; return { balance, usdBalance }; }, [swapBalance]); @@ -194,12 +241,12 @@ export function useDepositComputed(props: UseDepositComputedProps) { const destinationBalance = useMemo(() => { if (!nexusSDK || !swapBalance || !destination) return undefined; return swapBalance - ?.flatMap((token) => token.breakdown ?? []) + ?.flatMap((token) => token.chainBalances ?? []) ?.find( (chain) => chain.chain?.id === destination.chainId && normalizeAddress(chain.contractAddress) === - normalizeAddress(destination.tokenAddress), + normalizeAddress(destination.tokenAddress), ); }, [swapBalance, nexusSDK, destination]); @@ -280,7 +327,10 @@ export function useDepositComputed(props: UseDepositComputedProps) { isDestinationBalance?: boolean; }> = []; - activeIntent.intent.sources.forEach((source) => { + // v2: extract inner SwapIntent from SwapAndExecuteIntent + const swapIntent = getSwapIntentLike(activeIntent.intent); + + swapIntent?.sources?.forEach((source) => { const sourcePricingSymbol = resolvePricingSymbol({ chainId: source.chain.id, contractAddress: source.token.contractAddress, @@ -320,17 +370,17 @@ export function useDepositComputed(props: UseDepositComputedProps) { }); // Calculate total spent from cross-chain sources - const totalAmountSpentUsd = activeIntent.intent.sources?.reduce( - (acc, source) => acc + parseNonNegativeNumber(source.value), + const totalAmountSpentUsd = swapIntent?.sources?.reduce( + (acc: number, source: { value?: string }) => acc + parseNonNegativeNumber(source.value), 0, - ); + ) ?? 0; // Get the actual amount arriving on destination (AFTER fees) const destinationAmountUsd = parseNonNegativeNumber( - activeIntent.intent.destination?.value, + swapIntent?.destination?.value, ); - const intentFeesAndBuffer = activeIntent.intent.feesAndBuffer; + const intentFeesAndBuffer = swapIntent?.feesAndBuffer; const bridgeFeeEntries = Object.entries(intentFeesAndBuffer?.bridge ?? {}) .filter(([key]) => key !== "total") .map(([key, value]) => ({ @@ -367,8 +417,7 @@ export function useDepositComputed(props: UseDepositComputedProps) { if (usedFromDestinationUsd > 0) { const usedTokenAmount = usedFromDestinationUsd / safeTokenExchangeRate; - const chainMeta = - CHAIN_METADATA[destination.chainId as keyof typeof CHAIN_METADATA]; + // v2: no CHAIN_METADATA — chainLogo and chainName are not available here sources.push({ chainId: destination.chainId, @@ -378,8 +427,8 @@ export function useDepositComputed(props: UseDepositComputedProps) { balance: usedTokenAmount.toString(), balanceInFiat: usedFromDestinationUsd, tokenLogo: destination.tokenLogo, - chainLogo: chainMeta?.logo, - chainName: chainMeta?.name, + chainLogo: undefined, + chainName: undefined, isDestinationBalance: true, }); } @@ -398,7 +447,7 @@ export function useDepositComputed(props: UseDepositComputedProps) { receiveAmountAfterSwap, receiveTokenLogo: destination.tokenLogo, receiveTokenChain: destination.chainId, - destinationChainName: activeIntent.intent.destination?.chain?.name, + destinationChainName: swapIntent?.destination?.chain?.name, }; }, [ activeIntent, @@ -430,13 +479,21 @@ export function useDepositComputed(props: UseDepositComputedProps) { estimatedFeeEth, destination.gasTokenSymbol ?? "ETH", ); - } else if (activeIntent?.intent?.destination?.gas) { - // Otherwise use estimated gas from intent - const gas = activeIntent.intent.destination.gas; - gasUsd = parseNonNegativeNumber(gas.value); + } else if (activeIntent?.intent) { + // v2: extract inner SwapIntent for gas info + const swapIntentLike = getSwapIntentLike(activeIntent.intent); + if (swapIntentLike?.destination?.gas) { + // Otherwise use estimated gas from intent + const gas = swapIntentLike.destination.gas; + gasUsd = parseNonNegativeNumber(gas.value); + } } - const bridgeRaw = activeIntent?.intent?.feesAndBuffer?.bridge; + const bridgeRaw = (() => { + if (!activeIntent?.intent) return undefined; + const swapIntentLike = getSwapIntentLike(activeIntent.intent); + return swapIntentLike?.feesAndBuffer?.bridge; + })(); const caGasUsd = parseNonNegativeNumber(bridgeRaw?.caGas); const gasSuppliedUsd = parseNonNegativeNumber( (bridgeRaw as Record | undefined) @@ -472,7 +529,11 @@ export function useDepositComputed(props: UseDepositComputedProps) { // Intent buffer can be displayed for transparency but is not added to total fee. const bufferUsd = parseNonNegativeNumber( - activeIntent?.intent?.feesAndBuffer?.buffer, + (() => { + if (!activeIntent?.intent) return undefined; + const swapIntentLike2 = getSwapIntentLike(activeIntent.intent); + return swapIntentLike2?.feesAndBuffer?.buffer; + })(), ); const totalFeeUsd = @@ -483,14 +544,20 @@ export function useDepositComputed(props: UseDepositComputedProps) { otherBridgeFeeUsd; const gasFormatted = usdFormatter.format(gasUsd); - const sourceValueUsd = (activeIntent?.intent?.sources ?? []).reduce( - (sum, source) => sum + parseNonNegativeNumber(source.value), - 0, - ); + const sourceValueUsd = (() => { + if (!activeIntent?.intent) return 0; + const swapIntentLike3 = getSwapIntentLike(activeIntent.intent); + return (swapIntentLike3?.sources ?? []).reduce( + (sum: number, source: { value?: string }) => sum + parseNonNegativeNumber(source.value), + 0, + ); + })(); - const destinationValueUsd = parseNonNegativeNumber( - activeIntent?.intent?.destination?.value, - ); + const destinationValueUsd = (() => { + if (!activeIntent?.intent) return 0; + const swapIntentLike4 = getSwapIntentLike(activeIntent.intent); + return parseNonNegativeNumber(swapIntentLike4?.destination?.value); + })(); const totalSomething = destinationValueUsd + totalFeeUsd + bufferUsd; const swapImpactUsd = totalSomething - sourceValueUsd; diff --git a/registry/nexus-elements/deposit/hooks/use-deposit-state.ts b/registry/nexus-elements/deposit/hooks/use-deposit-state.ts index 75e0249..0601056 100644 --- a/registry/nexus-elements/deposit/hooks/use-deposit-state.ts +++ b/registry/nexus-elements/deposit/hooks/use-deposit-state.ts @@ -7,7 +7,7 @@ import type { DepositInputs, NavigationDirection, } from "../types"; -import type { OnSwapIntentHookData } from "@avail-project/nexus-core"; +import type { SwapAndExecuteOnIntentHookData } from "@avail-project/nexus-sdk-v2"; /** * Source swap info collected during transaction execution @@ -65,7 +65,7 @@ export interface DepositState { lastResult: unknown; navigationDirection: NavigationDirection; simulation: { - swapIntent: OnSwapIntentHookData; + swapIntent: SwapAndExecuteOnIntentHookData; } | null; simulationLoading: boolean; receiveAmount: string | null; @@ -93,7 +93,7 @@ export type DepositAction = | { type: "setSimulation"; payload: { - swapIntent: OnSwapIntentHookData; + swapIntent: SwapAndExecuteOnIntentHookData; }; } | { type: "setSimulationLoading"; payload: boolean } diff --git a/registry/nexus-elements/deposit/hooks/use-deposit-widget.ts b/registry/nexus-elements/deposit/hooks/use-deposit-widget.ts index ba649a3..74c7236 100644 --- a/registry/nexus-elements/deposit/hooks/use-deposit-widget.ts +++ b/registry/nexus-elements/deposit/hooks/use-deposit-widget.ts @@ -9,15 +9,20 @@ import type { } from "../types"; import { ERROR_CODES, - NEXUS_EVENTS, - CHAIN_METADATA, - type SwapStepType, type ExecuteParams, type OnSwapIntentHookData, + type SwapAndExecuteOnIntentHookData, type SwapAndExecuteParams, type SwapAndExecuteResult, - parseUnits, -} from "@avail-project/nexus-core"; +} from "@avail-project/nexus-sdk-v2"; +import { parseUnits } from "viem"; + +// v2: SwapStepType removed — use a local step shape +type SwapStepType = { + typeID?: string; + type?: string; + [key: string]: unknown; +}; import { SWAP_EXPECTED_STEPS, useNexusError, @@ -64,14 +69,19 @@ function parseUsdAmount(value?: string): number { } function summarizeIntentSources( - intentSources: OnSwapIntentHookData["intent"]["sources"] | undefined, + intent: SwapAndExecuteOnIntentHookData["intent"] | undefined, ) { - return (intentSources ?? []).map((source) => ({ - chainId: source.chain.id, - chainName: source.chain.name, - tokenAddress: source.token.contractAddress, - tokenSymbol: source.token.symbol, - amount: source.amount, + // v2: SwapAndExecuteIntent has swap.sources only when swapRequired=true + if (!intent) return []; + const sources = intent.swapRequired + ? ((intent as { swapRequired: true; swap: { sources?: unknown[] } }).swap?.sources ?? []) + : []; + return sources.map((source) => ({ + chainId: (source as { chain?: { id?: number } }).chain?.id, + chainName: (source as { chain?: { name?: string } }).chain?.name, + tokenAddress: (source as { token?: { contractAddress?: string } }).token?.contractAddress, + tokenSymbol: (source as { token?: { symbol?: string } }).token?.symbol, + amount: (source as { amount?: string }).amount, })); } @@ -231,88 +241,87 @@ export function useDepositWidget( const inputsWithSources = { ...inputs, - fromSources, + sources: fromSources, // v2: fromSources renamed to sources }; nexusSDK .swapAndExecute(inputsWithSources, { onEvent: (event) => { - if (event.name === NEXUS_EVENTS.SWAP_STEP_COMPLETE) { - const step = event.args as SwapStepType & { - completed?: boolean; - data?: SwapSkippedData; - }; + // v2: typed discriminated union — plan_preview seeds steps, plan_progress updates them + if (event.type === "plan_preview") { + const planSteps = (event as { type: string; plan?: { steps?: unknown[] } }).plan?.steps ?? []; + seed(planSteps.map((s, i) => ({ + typeID: `step-${i}`, + ...(s as Record), + })) as SwapStepType[]); + return; + } - // Handle SWAP_SKIPPED - go directly to transaction-status - if (step?.type === "SWAP_SKIPPED") { - dispatch({ type: "setSkipSwap", payload: true }); - dispatch({ - type: "setSwapSkippedData", - payload: step.data ?? null, - }); - dispatch({ type: "setStatus", payload: "executing" }); - dispatch({ - type: "setStep", - payload: { step: "transaction-status", direction: "forward" }, - }); - stopwatch.start(); - } + if (event.type === "plan_progress") { + const progressEvent = event as { + type: string; + stepType: string; + state: string; + step: Record; + explorerUrl?: string; + }; - if (step?.type === "DETERMINING_SWAP" && step?.completed) { + // DETERMINING_SWAP equivalent: intent_hook / route_ready state + if ( + progressEvent.stepType === "bridge_intent_submission" && + progressEvent.state === "completed" + ) { determiningSwapComplete.current = true; stopwatch.start(); dispatch({ type: "setIntentReady", payload: true }); } + + const step: SwapStepType = { + typeID: progressEvent.stepType, + type: progressEvent.stepType, + ...progressEvent.step, + explorerURL: progressEvent.explorerUrl, + }; onStepComplete(step); } }, + onIntent: (intentData) => { + // v2: onIntent is a top-level SwapAndExecuteOptions hook + swapIntent.current = intentData; + determiningSwapComplete.current = true; + stopwatch.start(); + dispatch({ type: "setIntentReady", payload: true }); + }, }) .then((data: SwapAndExecuteResult) => { suppressNextWidgetPreviewCancelError.current = false; - // Extract source swaps from the result + // Extract source swaps from the result (v2: SuccessfulSwapResult.sourceSwaps are ChainSwap[]) const sourceSwapsFromResult = data.swapResult?.sourceSwaps ?? []; sourceSwapsFromResult.forEach((sourceSwap) => { - const chainMeta = - CHAIN_METADATA[sourceSwap.chainId as keyof typeof CHAIN_METADATA]; - const baseUrl = chainMeta?.blockExplorerUrls?.[0] ?? ""; - const explorerUrl = baseUrl - ? `${baseUrl}/tx/${sourceSwap.txHash}` - : ""; + // v2: no CHAIN_METADATA — use txHash for explorer URL via intentExplorerUrl dispatch({ type: "addSourceSwap", payload: { chainId: sourceSwap.chainId, - chainName: chainMeta?.name ?? `Chain ${sourceSwap.chainId}`, - explorerUrl, + chainName: `Chain ${sourceSwap.chainId}`, + explorerUrl: data.swapResult?.intentExplorerUrl ?? "", }, }); }); // Set explorer URLs from the result if (sourceSwapsFromResult.length > 0) { - const firstSourceSwap = sourceSwapsFromResult[0]; - const chainMeta = - CHAIN_METADATA[ - firstSourceSwap.chainId as keyof typeof CHAIN_METADATA - ]; - const baseUrl = chainMeta?.blockExplorerUrls?.[0] ?? ""; - const sourceExplorerUrl = baseUrl - ? `${baseUrl}/tx/${firstSourceSwap.txHash}` - : ""; dispatch({ type: "setExplorerUrls", - payload: { sourceExplorerUrl }, + payload: { sourceExplorerUrl: data.swapResult?.intentExplorerUrl ?? null }, }); } - // Destination explorer URL - const destChainMeta = - CHAIN_METADATA[destination.chainId as keyof typeof CHAIN_METADATA]; - const destBaseUrl = destChainMeta?.blockExplorerUrls?.[0] ?? ""; + // Destination explorer URL (v2: intentExplorerUrl replaces swapResult.explorerURL) const destinationExplorerUrl = - data.swapResult?.explorerURL ?? - (data.executeResponse?.txHash && destBaseUrl - ? `${destBaseUrl}/tx/${data.executeResponse.txHash}` + data.swapResult?.intentExplorerUrl ?? + (data.executeResponse?.txHash + ? `https://explorer.avail.so/tx/${data.executeResponse.txHash}` : null); if (destinationExplorerUrl) { @@ -325,7 +334,7 @@ export function useDepositWidget( // Store Nexus intent URL and deposit tx hash dispatch({ type: "setNexusIntentUrl", - payload: data.swapResult?.explorerURL ?? null, + payload: data.swapResult?.intentExplorerUrl ?? null, }); dispatch({ type: "setDepositTxHash", @@ -349,7 +358,10 @@ export function useDepositWidget( dispatch({ type: "setReceiveAmount", - payload: swapIntent.current?.intent?.destination?.amount ?? "", + // v2: SwapAndExecuteIntent doesn't have destination.amount — use swapResult if available + payload: data.swapResult?.intentExplorerUrl + ? (swapIntent.current as unknown as { intent?: { destination?: { amount?: string } } })?.intent?.destination?.amount ?? "" + : "", }); onSuccess?.(); dispatch({ type: "setStatus", payload: "success" }); @@ -363,8 +375,8 @@ export function useDepositWidget( const isUserRejectedError = code === ERROR_CODES.USER_DENIED_INTENT || code === ERROR_CODES.USER_DENIED_INTENT_SIGNATURE || - code === ERROR_CODES.USER_DENIED_ALLOWANCE || - code === ERROR_CODES.USER_DENIED_SIWE_SIGNATURE; + code === ERROR_CODES.USER_DENIED_ALLOWANCE; + // v2: USER_DENIED_SIWE_SIGNATURE removed const shouldSuppressWidgetError = suppressNextWidgetPreviewCancelError.current && isUserRejectedError; @@ -476,17 +488,20 @@ export function useDepositWidget( const newInputs: SwapAndExecuteParams = { toChainId: destination.chainId, toTokenAddress: destination.tokenAddress, - toAmount: parsed, + toAmountRaw: parsed, // v2: toAmount renamed to toAmountRaw execute: { to: executeParams.to, value: executeParams.value, data: executeParams.data, gasPrice: executeParams.gasPrice, - tokenApproval: executeParams.tokenApproval as { - token: `0x${string}`; - amount: bigint; - spender: Hex; - }, + tokenApproval: executeParams.tokenApproval + ? ({ + toTokenAddress: (executeParams.tokenApproval as unknown as { token?: string; toTokenAddress?: string }).toTokenAddress + ?? (executeParams.tokenApproval as unknown as { token?: string }).token, + amount: (executeParams.tokenApproval as { amount: bigint }).amount, + spender: (executeParams.tokenApproval as { spender: `0x${string}` }).spender, + } as { toTokenAddress: `0x${string}`; amount: bigint; spender: `0x${string}` }) + : undefined, gas: BigInt(400_000), }, }; @@ -691,9 +706,9 @@ export function useDepositWidget( // Polling for simulation refresh usePolling( pollingEnabled && - state.status === "previewing" && - Boolean(swapIntent.current) && - !state.simulationLoading, + state.status === "previewing" && + Boolean(swapIntent.current) && + !state.simulationLoading, async () => { await refreshSimulation(); }, diff --git a/registry/nexus-elements/deposit/types.ts b/registry/nexus-elements/deposit/types.ts index de911a5..aea0e03 100644 --- a/registry/nexus-elements/deposit/types.ts +++ b/registry/nexus-elements/deposit/types.ts @@ -1,12 +1,21 @@ import type { - SUPPORTED_CHAINS_IDS, ExecuteParams, OnSwapIntentHookData, - SwapStepType, - UserAsset, -} from "@avail-project/nexus-core"; + SwapAndExecuteOnIntentHookData, + TokenBalance, +} from "@avail-project/nexus-sdk-v2"; import type { Address } from "viem"; +// v2: SwapStepType removed — use a local generic step shape +export type SwapStepType = { + typeID?: string; + type?: string; + [key: string]: unknown; +}; + +// Re-export for convenience +export type { OnSwapIntentHookData }; + export type WidgetStep = | "amount" | "confirmation" @@ -69,7 +78,7 @@ export interface SetAssetSelectionOptions { } export interface DestinationConfig { - chainId: SUPPORTED_CHAINS_IDS; + chainId: number; // v2: was SUPPORTED_CHAINS_IDS depositTargetLogo?: string; tokenAddress: `0x${string}`; tokenSymbol: string; @@ -163,8 +172,8 @@ export interface DepositWidgetContextValue { ) => void; // SDK integration - swapBalance: UserAsset[] | null; - activeIntent: OnSwapIntentHookData | null; + swapBalance: TokenBalance[] | null; + activeIntent: SwapAndExecuteOnIntentHookData | null; confirmationDetails: { sourceLabel: string; sources: Array< @@ -174,7 +183,8 @@ export interface DepositWidgetContextValue { decimals: number; symbol: string; balance: string; - balanceInFiat?: number; + value?: string; // v2: string USD value (replaces balanceInFiat: number) + balanceInFiat?: number; // kept for any legacy callers tokenLogo?: string; chainLogo?: string; chainName?: string; diff --git a/registry/nexus-elements/deposit/utils.ts b/registry/nexus-elements/deposit/utils.ts index 3a45e1b..9f4d6a4 100644 --- a/registry/nexus-elements/deposit/utils.ts +++ b/registry/nexus-elements/deposit/utils.ts @@ -1,15 +1,17 @@ import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; import { STABLECOIN_SYMBOLS } from "./constants/widget"; -import { - CHAIN_METADATA, - sortSourcesByPriority, - UserAsset, -} from "@avail-project/nexus-core"; +import type { TokenBalance } from "@avail-project/nexus-sdk-v2"; import { AssetFilterType, DestinationConfig, Token } from "./types"; import { Hex, padHex } from "viem"; import { formatUsdForDisplay } from "../common"; +// v2: CHAIN_METADATA not exported — use a stable hardcoded set of well-known native symbols +const WELL_KNOWN_NATIVE_SYMBOLS = new Set([ + "ETH", "MATIC", "AVAX", "BNB", "OP", "ARB", "KAIA", "CELO", "FTM", "MON", + "HYPE", +]); + export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } @@ -34,9 +36,7 @@ export function isStablecoin(symbol: string): boolean { } export function isNative(symbol: string): boolean { - return Object.values(CHAIN_METADATA).some( - (chain) => chain.nativeCurrency.symbol === symbol, - ); + return WELL_KNOWN_NATIVE_SYMBOLS.has(symbol?.toUpperCase()); } /** @@ -75,6 +75,7 @@ export function checkIfMatchesPreset( if (isStablecoin(token.symbol)) { stableIds.add(chain.id); } + console.log("token", token) if (isNative(token.symbol)) { nativeIds.add(chain.id); } @@ -184,18 +185,18 @@ export function parseSourceId(sourceId: string): { } function buildSourceFiatByKeyMap( - swapBalance: UserAsset[] | null, + swapBalance: TokenBalance[] | null, ): Map { const map = new Map(); if (!swapBalance) return map; for (const asset of swapBalance) { - for (const breakdown of asset.breakdown ?? []) { + for (const breakdown of asset.chainBalances ?? []) { const chainId = breakdown.chain?.id; const tokenAddress = breakdown.contractAddress; if (!chainId || !tokenAddress) continue; - const balanceInFiat = parseNonNegativeNumber(breakdown.balanceInFiat); + const balanceInFiat = parseNonNegativeNumber(breakdown.value); map.set(getFiatLookupKey(tokenAddress, chainId), balanceInFiat); } @@ -204,8 +205,9 @@ function buildSourceFiatByKeyMap( return map; } +// v2: sortSourcesByPriority removed — build priority rank map by fiat balance descending function buildPriorityRankMap( - swapBalance: UserAsset[] | null, + swapBalance: TokenBalance[] | null, destination: Pick< DestinationConfig, "chainId" | "tokenAddress" | "tokenSymbol" @@ -214,22 +216,29 @@ function buildPriorityRankMap( const map = new Map(); if (!swapBalance?.length) return map; - const sortedSources = sortSourcesByPriority(swapBalance, { - chainID: destination.chainId, - tokenAddress: destination.tokenAddress, - symbol: destination.tokenSymbol, - }); - - sortedSources.forEach((source, index) => { - map.set(getPriorityLookupKey(source.tokenAddress, source.chainID), index); - }); - + // Collect all sources with their fiat values and sort by fiat descending + const candidates: { key: string; balanceInFiat: number }[] = []; + for (const asset of swapBalance) { + for (const breakdown of asset.chainBalances ?? []) { + const chainId = breakdown.chain?.id; + const tokenAddress = breakdown.contractAddress; + if (!chainId || !tokenAddress) continue; + // Exclude the destination chain + if (chainId === destination.chainId) continue; + candidates.push({ + key: getPriorityLookupKey(tokenAddress, chainId), + balanceInFiat: parseNonNegativeNumber(breakdown.value), + }); + } + } + candidates.sort((a, b) => b.balanceInFiat - a.balanceInFiat); + candidates.forEach((c, i) => map.set(c.key, i)); return map; } function sortSourceIdsByPriority(params: { sourceIds: Iterable; - swapBalance: UserAsset[] | null; + swapBalance: TokenBalance[] | null; destination: Pick< DestinationConfig, "chainId" | "tokenAddress" | "tokenSymbol" @@ -241,7 +250,7 @@ function sortSourceIdsByPriority(params: { function buildSortedSourceCandidates(params: { sourceIds: Iterable; - swapBalance: UserAsset[] | null; + swapBalance: TokenBalance[] | null; destination: Pick< DestinationConfig, "chainId" | "tokenAddress" | "tokenSymbol" @@ -292,7 +301,7 @@ function buildSortedSourceCandidates(params: { } export function buildDepositSourcePoolIds(params: { - swapBalance: UserAsset[] | null; + swapBalance: TokenBalance[] | null; filter: AssetFilterType; selectedSourceIds: Iterable; isManualSelection: boolean; @@ -307,13 +316,14 @@ export function buildDepositSourcePoolIds(params: { const sourceIds = new Set(); swapBalance?.forEach((asset) => { - asset.breakdown?.forEach((breakdown) => { + asset.chainBalances?.forEach((breakdown) => { const chainId = breakdown.chain?.id; const tokenAddress = breakdown.contractAddress; if (!chainId || !tokenAddress) return; - const stable = isStablecoin(breakdown.symbol); - const native = isNative(breakdown.symbol); + // v2: breakdown has no .symbol — use parent asset.symbol + const stable = isStablecoin(asset.symbol); + const native = isNative(asset.symbol); const sourceId = `${tokenAddress}-${chainId}`; const include = filter === "all" || @@ -337,7 +347,7 @@ export interface ResolvedDepositSourceSelection { } export function resolveDepositSourceSelection(params: { - swapBalance: UserAsset[] | null; + swapBalance: TokenBalance[] | null; destination: Pick< DestinationConfig, "chainId" | "tokenAddress" | "tokenSymbol" @@ -367,18 +377,18 @@ export function resolveDepositSourceSelection(params: { const resolvedSelectedSourceIds = isManualSelection ? sortSourceIdsByPriority({ - sourceIds: sourcePoolIds, - swapBalance, - destination, - minimumBalanceUsd, - }) + sourceIds: sourcePoolIds, + swapBalance, + destination, + minimumBalanceUsd, + }) : buildPrioritySelectedSourceIds({ - swapBalance, - destination, - minimumBalanceUsd, - targetAmountUsd, - sourceIds: sourcePoolIds, - }); + swapBalance, + destination, + minimumBalanceUsd, + targetAmountUsd, + sourceIds: sourcePoolIds, + }); const fromSources = buildSortedFromSources({ sourceIds: resolvedSelectedSourceIds, @@ -395,7 +405,7 @@ export function resolveDepositSourceSelection(params: { } export function buildSelectableSourceIds(params: { - swapBalance: UserAsset[] | null; + swapBalance: TokenBalance[] | null; destination: Pick< DestinationConfig, "chainId" | "tokenAddress" | "tokenSymbol" @@ -408,7 +418,7 @@ export function buildSelectableSourceIds(params: { if (!swapBalance) return []; for (const asset of swapBalance) { - for (const breakdown of asset.breakdown ?? []) { + for (const breakdown of asset.chainBalances ?? []) { const chainId = breakdown.chain?.id; const tokenAddress = breakdown.contractAddress; if (!chainId || !tokenAddress) continue; @@ -426,7 +436,7 @@ export function buildSelectableSourceIds(params: { } export function buildPrioritySelectedSourceIds(params: { - swapBalance: UserAsset[] | null; + swapBalance: TokenBalance[] | null; destination: Pick< DestinationConfig, "chainId" | "tokenAddress" | "tokenSymbol" @@ -446,16 +456,16 @@ export function buildPrioritySelectedSourceIds(params: { const requestedSourceIds = sourceIds ? [...new Set(sourceIds)] : undefined; const orderedCandidateSourceIds = requestedSourceIds ? sortSourceIdsByPriority({ - sourceIds: requestedSourceIds, - swapBalance, - destination, - minimumBalanceUsd, - }) + sourceIds: requestedSourceIds, + swapBalance, + destination, + minimumBalanceUsd, + }) : buildSelectableSourceIds({ - swapBalance, - destination, - minimumBalanceUsd, - }); + swapBalance, + destination, + minimumBalanceUsd, + }); if (orderedCandidateSourceIds.length === 0) return []; @@ -491,7 +501,7 @@ export function buildPrioritySelectedSourceIds(params: { export function buildSortedFromSources(params: { sourceIds: Iterable; - swapBalance: UserAsset[] | null; + swapBalance: TokenBalance[] | null; destination: Pick< DestinationConfig, "chainId" | "tokenAddress" | "tokenSymbol" diff --git a/registry/nexus-elements/fast-bridge/components/allowance-modal.tsx b/registry/nexus-elements/fast-bridge/components/allowance-modal.tsx index 84d939f..1b83c57 100644 --- a/registry/nexus-elements/fast-bridge/components/allowance-modal.tsx +++ b/registry/nexus-elements/fast-bridge/components/allowance-modal.tsx @@ -12,12 +12,11 @@ import { Input } from "../../ui/input"; import { Label } from "../../ui/label"; import { type AllowanceHookSource, - CHAIN_METADATA, - formatTokenBalance, type OnAllowanceHookData, - parseUnits, -} from "@avail-project/nexus-core"; +} from "@avail-project/nexus-sdk-v2"; +import { parseUnits } from "viem"; import { useNexusError } from "../../common"; +import { formatTokenBalance } from "@avail-project/nexus-sdk-v2/utils"; interface AllowanceModalProps { allowance: RefObject; @@ -228,7 +227,7 @@ const AllowanceModal: FC = ({
{source.chain.name} = ({
- {bridgableBalance?.breakdown.map((chain) => { + {bridgableBalance?.chainBalances?.map((chain: any) => { if (Number.parseFloat(chain.balance) === 0) return null; if (inputs?.chain === chain.chain.id) return null; return ( diff --git a/registry/nexus-elements/fast-bridge/components/chain-select.tsx b/registry/nexus-elements/fast-bridge/components/chain-select.tsx index 3899cd2..d25a5ec 100644 --- a/registry/nexus-elements/fast-bridge/components/chain-select.tsx +++ b/registry/nexus-elements/fast-bridge/components/chain-select.tsx @@ -8,7 +8,7 @@ import { SelectTrigger, SelectValue, } from "../../ui/select"; -import { type SUPPORTED_CHAINS_IDS } from "@avail-project/nexus-core"; +// v2: SUPPORTED_CHAINS_IDS removed — use plain number import { cn } from "@/lib/utils"; import { useNexus } from "../../nexus/NexusProvider"; @@ -18,7 +18,7 @@ interface ChainSelectProps { hidden?: boolean; className?: string; label?: string; - handleSelect: (chainId: SUPPORTED_CHAINS_IDS) => void; + handleSelect: (chainId: number) => void; } const ChainSelect: FC = ({ @@ -42,7 +42,7 @@ const ChainSelect: FC = ({ value={selectedChain?.toString() ?? ""} onValueChange={(value) => { if (!disabled) { - handleSelect(Number.parseInt(value) as SUPPORTED_CHAINS_IDS); + handleSelect(Number.parseInt(value)); } }} > diff --git a/registry/nexus-elements/fast-bridge/components/fee-breakdown.tsx b/registry/nexus-elements/fast-bridge/components/fee-breakdown.tsx index 3514490..57f4ce0 100644 --- a/registry/nexus-elements/fast-bridge/components/fee-breakdown.tsx +++ b/registry/nexus-elements/fast-bridge/components/fee-breakdown.tsx @@ -5,19 +5,16 @@ import { AccordionItem, AccordionTrigger, } from "../../ui/accordion"; -import { - formatTokenBalance, - SUPPORTED_TOKENS, - type ReadableIntent, -} from "@avail-project/nexus-core"; +import { type BridgeIntent } from "@avail-project/nexus-sdk-v2"; +import { formatTokenBalance } from "@avail-project/nexus-sdk-v2/utils"; import { Skeleton } from "../../ui/skeleton"; import { useNexus } from "../../nexus/NexusProvider"; import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/tooltip"; import { MessageCircleQuestion } from "lucide-react"; interface FeeBreakdownProps { - intent: ReadableIntent; - tokenSymbol: SUPPORTED_TOKENS; + intent: BridgeIntent; + tokenSymbol?: string; // v2: was SUPPORTED_TOKENS isLoading?: boolean; } @@ -39,7 +36,7 @@ const FeeBreakdown: FC = ({ { key: "gasSupplied", label: "Gas Supplied", - value: intent?.fees?.gasSupplied, + value: intent?.fees?.caGas, description: "The amount of gas tokens supplied to cover transaction costs on the destination chain.", }, @@ -72,7 +69,7 @@ const FeeBreakdown: FC = ({

{formatTokenBalance(intent.fees?.total, { symbol: tokenSymbol, - decimals: intent?.token?.decimals, + decimals: intent?.availableSources?.[0]?.token?.decimals, })}

)} @@ -104,7 +101,7 @@ const FeeBreakdown: FC = ({

{formatTokenBalance(value, { symbol: tokenSymbol, - decimals: intent?.token?.decimals, + decimals: intent?.availableSources?.[0]?.token?.decimals, })}

)} diff --git a/registry/nexus-elements/fast-bridge/components/recipient-address.tsx b/registry/nexus-elements/fast-bridge/components/recipient-address.tsx index 1b15550..a35b24b 100644 --- a/registry/nexus-elements/fast-bridge/components/recipient-address.tsx +++ b/registry/nexus-elements/fast-bridge/components/recipient-address.tsx @@ -5,7 +5,7 @@ import { Check, Edit } from "lucide-react"; import { Button } from "../../ui/button"; import { useNexus } from "../../nexus/NexusProvider"; import { type Address } from "viem"; -import { truncateAddress } from "@avail-project/nexus-core"; +import { truncateAddress } from "@avail-project/nexus-sdk-v2/utils"; interface RecipientAddressProps { address?: Address; diff --git a/registry/nexus-elements/fast-bridge/components/source-breakdown.tsx b/registry/nexus-elements/fast-bridge/components/source-breakdown.tsx index e44821b..73cabc4 100644 --- a/registry/nexus-elements/fast-bridge/components/source-breakdown.tsx +++ b/registry/nexus-elements/fast-bridge/components/source-breakdown.tsx @@ -1,9 +1,8 @@ import { - formatTokenBalance, - type ReadableIntent, - type SUPPORTED_TOKENS, - type UserAsset, -} from "@avail-project/nexus-core"; + type BridgeIntent, + type TokenBalance, type ChainBalance, +} from "@avail-project/nexus-sdk-v2"; +import { formatTokenBalance } from "@avail-project/nexus-sdk-v2/utils"; import { Accordion, AccordionContent, @@ -17,10 +16,10 @@ import { cn } from "@/lib/utils"; type SourceCoverageState = "healthy" | "warning" | "error"; interface SourceBreakdownProps { - intent?: ReadableIntent; - tokenSymbol: SUPPORTED_TOKENS; + intent?: BridgeIntent; + tokenSymbol?: string; // v2: was SUPPORTED_TOKENS isLoading?: boolean; - availableSources: UserAsset["breakdown"]; + availableSources: ChainBalance[]; // v2: was UserAsset["breakdown"] selectedSourceChains: number[]; onToggleSourceChain: (chainId: number) => void; onSourceMenuOpenChange?: (open: boolean) => void; @@ -50,7 +49,7 @@ const SourceBreakdown = ({ requiredTotal, requiredSafetyTotal, }: SourceBreakdownProps) => { - const displayTokenSymbol = availableSources[0]?.symbol ?? tokenSymbol; + const displayTokenSymbol = tokenSymbol ?? availableSources[0]?.chain?.name; const normalizedCoverage = Math.max(0, Math.min(100, sourceCoveragePercent)); const progressRadius = 16; const progressCircumference = 2 * Math.PI * progressRadius; @@ -129,14 +128,14 @@ const SourceBreakdown = ({
) : ( - intent?.sources && ( + intent?.availableSources && ( <>

You Spend

{`${displayTokenSymbol} on ${ - intent?.sources?.length - } ${intent?.sources?.length > 1 ? "chains" : "chain"}`} + intent?.availableSources?.length + } ${intent?.availableSources?.length > 1 ? "chains" : "chain"}`}

@@ -144,7 +143,7 @@ const SourceBreakdown = ({

{formatTokenBalance(intent?.sourcesTotal, { symbol: displayTokenSymbol, - decimals: intent?.token?.decimals, + decimals: intent?.availableSources?.[0]?.token?.decimals, })}

{formatTokenBalance(parseFloat(selectedTotal ?? "0"), { symbol: displayTokenSymbol, - decimals: intent?.token?.decimals, + decimals: intent?.availableSources?.[0]?.token?.decimals, })}

@@ -219,7 +218,7 @@ const SourceBreakdown = ({ parseFloat(requiredSafetyTotal ?? "0"), { symbol: displayTokenSymbol, - decimals: intent?.token?.decimals, + decimals: intent?.availableSources?.[0]?.token?.decimals, }, )} @@ -274,8 +273,8 @@ const SourceBreakdown = ({ const isLastSelected = isSelected ? selectedSourceChains.length === 1 : false; - const willUseAmount = intent?.sources?.find( - (s) => s.chainID === chainId, + const willUseAmount = intent?.availableSources?.find( + (s: any) => s.chain.id === chainId, )?.amount; return ( @@ -327,7 +326,7 @@ const SourceBreakdown = ({

{formatTokenBalance(source.balance, { - symbol: source.symbol, + symbol: tokenSymbol ?? source.chain?.name, decimals: source.decimals, })}

@@ -335,8 +334,9 @@ const SourceBreakdown = ({

Estimated to use:{" "} {formatTokenBalance(willUseAmount, { - symbol: source.symbol, - decimals: intent?.token?.decimals, + symbol: tokenSymbol ?? source.chain?.name, + decimals: + intent?.availableSources?.[0]?.token?.decimals, })}

)} diff --git a/registry/nexus-elements/fast-bridge/components/token-select.tsx b/registry/nexus-elements/fast-bridge/components/token-select.tsx index e869231..8dfb147 100644 --- a/registry/nexus-elements/fast-bridge/components/token-select.tsx +++ b/registry/nexus-elements/fast-bridge/components/token-select.tsx @@ -1,7 +1,4 @@ -import { - type SUPPORTED_CHAINS_IDS, - type SUPPORTED_TOKENS, -} from "@avail-project/nexus-core"; +// v2: SUPPORTED_CHAINS_IDS, SUPPORTED_TOKENS removed — use plain string/number import { Select, SelectContent, @@ -15,9 +12,9 @@ import { useNexus } from "../../nexus/NexusProvider"; import { useMemo } from "react"; interface TokenSelectProps { - selectedToken?: SUPPORTED_TOKENS; - selectedChain: SUPPORTED_CHAINS_IDS; - handleTokenSelect: (token: SUPPORTED_TOKENS) => void; + selectedToken?: string; + selectedChain: number; + handleTokenSelect: (token: string) => void; isTestnet?: boolean; disabled?: boolean; label?: string; @@ -46,7 +43,7 @@ const TokenSelect = ({ { const matchedChain = chainsWithTokens.find( - (chain) => String(chain) === value, + (chain: any) => String((chain as any).id) === value, ); if (matchedChain) { setTempChain(matchedChain); @@ -138,8 +148,8 @@ const DestinationAssetSelect: FC = ({ {tempChain ? ( {CHAIN_METADATA[tempChain].name} = ({
{CHAIN_METADATA[c].name} - {CHAIN_METADATA[c].name} + {getChainMeta(c).name}
))} @@ -191,7 +201,7 @@ const DestinationAssetSelect: FC = ({
diff --git a/registry/nexus-elements/swaps/components/destination-container.tsx b/registry/nexus-elements/swaps/components/destination-container.tsx index 8412ff8..caa8823 100644 --- a/registry/nexus-elements/swaps/components/destination-container.tsx +++ b/registry/nexus-elements/swaps/components/destination-container.tsx @@ -2,11 +2,11 @@ import React, { type RefObject, useMemo } from "react"; import { Label } from "../../ui/label"; import { cn } from "@/lib/utils"; import { - CHAIN_METADATA, type OnSwapIntentHookData, - type SUPPORTED_CHAINS_IDS, - type UserAsset, -} from "@avail-project/nexus-core"; + type TokenBalance, + type ChainBalance, +} from "@avail-project/nexus-sdk-v2"; +import { useNexus } from "../../nexus/NexusProvider"; import { type SwapInputs, type SwapMode, @@ -31,9 +31,9 @@ interface DestinationContainerProps { destinationHovered: boolean; inputs: SwapInputs; swapIntent: RefObject; - destinationBalance?: UserAsset["breakdown"][0]; - swapBalance: UserAsset[] | null; - availableStables: UserAsset[]; + destinationBalance?: ChainBalance; // v2: was UserAsset["breakdown"][0] + swapBalance: TokenBalance[] | null; + availableStables: TokenBalance[]; swapMode: SwapMode; status: TransactionStatus; setInputs: (inputs: Partial) => void; @@ -46,7 +46,8 @@ interface DestinationContainerProps { ) => string | undefined; } -type AssetBreakdownWithOptionalIcon = UserAsset["breakdown"][number] & { +// v2: ChainBalance replaces UserAsset["breakdown"][number] +type AssetBreakdownWithOptionalIcon = ChainBalance & { icon?: string; }; @@ -74,26 +75,27 @@ const DestinationContainer: React.FC = ({ swapIntent?.current?.intent?.destination?.token?.decimals ) ?? ""; + const { swapSupportedChainsAndTokens } = useNexus(); + const getChainMeta = (id?: number) => + swapSupportedChainsAndTokens?.find((c) => c.id === id) ?? { id: id ?? 0, name: "", logo: "" }; + + // v2: quick-pick tokens from chainBalances (replaces breakdown) const quickPickTokens = useMemo( () => - availableStables + (availableStables ?? []) .map((token) => { const breakdown = - token.breakdown?.find( - (entry) => Number.parseFloat(entry.balance ?? "0") > 0, - ) ?? token.breakdown?.[0]; + token.chainBalances?.find( + (b) => b.chain.id === inputs?.toChainID, + ) ?? token.chainBalances?.[0]; if (!breakdown) return null; return { token, breakdown }; }) - .filter( - ( - item, - ): item is { - token: UserAsset; - breakdown: UserAsset["breakdown"][number]; - } => item !== null, - ), - [availableStables], + .filter(Boolean) as { + token: TokenBalance; + breakdown: ChainBalance; + }[], + [availableStables, inputs?.toChainID], ); return ( @@ -116,6 +118,7 @@ const DestinationContainer: React.FC = ({ variant={"secondary"} onClick={() => { const normalizedSymbol = breakdown.symbol.toUpperCase(); + // v2: ChainBalanceWithIcon uses icon, not logo directly const breakdownIcon = ( breakdown as AssetBreakdownWithOptionalIcon ).icon; @@ -123,7 +126,7 @@ const DestinationContainer: React.FC = ({ breakdownIcon || TOKEN_IMAGES[breakdown.symbol] || TOKEN_IMAGES[normalizedSymbol] || - token.icon || + token.logo || ""; setInputs({ ...inputs, @@ -134,7 +137,7 @@ const DestinationContainer: React.FC = ({ name: breakdown.symbol, symbol: breakdown.symbol, }, - toChainID: breakdown.chain.id as SUPPORTED_CHAINS_IDS, + toChainID: breakdown.chain.id, }); }} className="bg-transparent rounded-full hover:-translate-y-1 hover:object-scale-down" @@ -145,7 +148,7 @@ const DestinationContainer: React.FC = ({ (breakdown as AssetBreakdownWithOptionalIcon).icon || TOKEN_IMAGES[breakdown.symbol] || TOKEN_IMAGES[breakdown.symbol.toUpperCase()] || - token.icon || + token.logo || "" } chainLogo={breakdown.chain.logo} @@ -173,7 +176,7 @@ const DestinationContainer: React.FC = ({ tokenLogo={inputs?.toToken?.logo} chainLogo={ inputs?.toChainID - ? CHAIN_METADATA[inputs?.toChainID]?.logo + ? getChainMeta(inputs?.toChainID).logo || undefined : undefined } size="lg" diff --git a/registry/nexus-elements/swaps/components/source-asset-select.tsx b/registry/nexus-elements/swaps/components/source-asset-select.tsx index 44119f0..dc0e867 100644 --- a/registry/nexus-elements/swaps/components/source-asset-select.tsx +++ b/registry/nexus-elements/swaps/components/source-asset-select.tsx @@ -2,12 +2,8 @@ import { type FC, useMemo, useState } from "react"; import { Button } from "../../ui/button"; import { useNexus } from "../../nexus/NexusProvider"; -import { - type UserAsset, - type SUPPORTED_CHAINS_IDS, - CHAIN_METADATA, - formatTokenBalance, -} from "@avail-project/nexus-core"; +import { type TokenBalance, type ChainBalance } from "@avail-project/nexus-sdk-v2"; +import { formatTokenBalance } from "@avail-project/nexus-sdk-v2/utils"; import { TOKEN_IMAGES } from "../config/destination"; import { Link2, Loader2, Search, X } from "lucide-react"; import { DialogClose } from "../../ui/dialog"; @@ -23,11 +19,12 @@ import { SHORT_CHAIN_NAME } from "../../common"; import { type SourceTokenInfo } from "../hooks/useSwaps"; interface SourceAssetSelectProps { - onSelect: (chainId: SUPPORTED_CHAINS_IDS, token: SourceTokenInfo) => void; - swapBalance: UserAsset[] | null; + onSelect: (chainId: number, token: SourceTokenInfo) => void; + swapBalance: TokenBalance[] | null; } -type AssetBreakdownWithOptionalIcon = UserAsset["breakdown"][number] & { +// v2: ChainBalance replaces UserAsset["breakdown"] +type ChainBalanceWithOptionalIcon = ChainBalance & { icon?: string; }; @@ -49,31 +46,34 @@ const SourceAssetSelect: FC = ({ const tokens: SourceTokenInfo[] = []; for (const asset of swapBalance) { - if (!asset?.breakdown?.length) continue; - for (const breakdown of asset.breakdown) { - if (Number.parseFloat(breakdown.balance) <= 0) continue; - const tokenSymbol = breakdown.symbol; + // v2: chainBalances replaces breakdown + if (!asset?.chainBalances?.length) continue; + for (const chainBal of asset.chainBalances) { + if (Number.parseFloat(chainBal.balance) <= 0) continue; + const tokenSymbol = chainBal.symbol; const normalizedTokenSymbol = tokenSymbol.toUpperCase(); - const breakdownIcon = (breakdown as AssetBreakdownWithOptionalIcon).icon; + // v2: logo is on chain.logo for ChainBalance; contractAddress is the token address const tokenLogo = - breakdownIcon || + (chainBal as ChainBalanceWithOptionalIcon).icon || + chainBal.chain.logo || TOKEN_IMAGES[tokenSymbol] || TOKEN_IMAGES[normalizedTokenSymbol] || - asset.icon || + asset.logo || ""; tokens.push({ - contractAddress: breakdown.contractAddress, - decimals: breakdown.decimals ?? asset.decimals, + contractAddress: chainBal.contractAddress, + decimals: chainBal.decimals ?? asset.decimals, logo: tokenLogo, name: tokenSymbol, symbol: tokenSymbol, - balance: formatTokenBalance(breakdown?.balance, { + balance: formatTokenBalance(chainBal?.balance, { symbol: tokenSymbol, - decimals: breakdown.decimals ?? asset.decimals, + decimals: chainBal.decimals ?? asset.decimals, }), - balanceInFiat: `$${breakdown.balanceInFiat}`, - chainId: breakdown.chain?.id, + // v2: value is a string USD amount per ChainBalance + balanceInFiat: `$${Number.parseFloat(chainBal.value ?? "0").toFixed(2)}`, + chainId: chainBal.chain?.id, }); } } @@ -122,7 +122,7 @@ const SourceAssetSelect: FC = ({ const handlePick = (tok: SourceTokenInfo) => { const chainId = tempChain?.id ?? tok.chainId; if (!chainId) return; - onSelect(chainId as SUPPORTED_CHAINS_IDS, tok); + onSelect(chainId, tok); }; if (!swapBalance) @@ -223,7 +223,8 @@ const SourceAssetSelect: FC = ({ c.id === (t.chainId ?? 1))?.logo || undefined} className="border border-border rounded-full" />
diff --git a/registry/nexus-elements/swaps/components/source-container.tsx b/registry/nexus-elements/swaps/components/source-container.tsx index d9ac335..fc456c5 100644 --- a/registry/nexus-elements/swaps/components/source-container.tsx +++ b/registry/nexus-elements/swaps/components/source-container.tsx @@ -9,10 +9,11 @@ import { } from "../hooks/useSwaps"; import { computeAmountFromFraction, usdFormatter } from "../../common"; import { - CHAIN_METADATA, - type UserAsset, + type TokenBalance, + type ChainBalance, type OnSwapIntentHookData, -} from "@avail-project/nexus-core"; +} from "@avail-project/nexus-sdk-v2"; +import { useNexus } from "../../nexus/NexusProvider"; import AmountInput from "./amount-input"; import { Dialog, @@ -50,8 +51,8 @@ interface SourceContainerProps { status: TransactionStatus; sourceHovered: boolean; inputs: SwapInputs; - availableBalance?: UserAsset["breakdown"][0]; - swapBalance: UserAsset[] | null; + availableBalance?: ChainBalance; // v2: was UserAsset["breakdown"][0] + swapBalance: TokenBalance[] | null; swapMode: SwapMode; swapIntent: RefObject; setInputs: (inputs: Partial) => void; @@ -79,6 +80,11 @@ const SourceContainer: React.FC = ({ getFiatValue, formatBalance, }) => { + const { swapSupportedChainsAndTokens } = useNexus(); + const fromChainLogo = swapSupportedChainsAndTokens?.find( + (c) => c.id === inputs?.fromChainID + )?.logo || undefined; + const isExactOut = swapMode === "exactOut"; // In exactIn mode, show user's input; in exactOut mode, show calculated source from intent @@ -181,7 +187,7 @@ const SourceContainer: React.FC = ({ tokenLogo={inputs?.fromToken?.logo} chainLogo={ inputs?.fromChainID - ? CHAIN_METADATA[inputs?.fromChainID]?.logo + ? fromChainLogo : undefined } size="lg" diff --git a/registry/nexus-elements/swaps/components/transaction-progress.tsx b/registry/nexus-elements/swaps/components/transaction-progress.tsx index c62ac53..29948cc 100644 --- a/registry/nexus-elements/swaps/components/transaction-progress.tsx +++ b/registry/nexus-elements/swaps/components/transaction-progress.tsx @@ -1,12 +1,10 @@ import { type FC, useMemo } from "react"; -import { - type BridgeStepType, - type SwapStepType, -} from "@avail-project/nexus-core"; +// v2: BridgeStepType/SwapStepType removed — use generic record step shape +type ProgressStep = { type?: string; typeID?: string; [key: string]: unknown }; + import { StepFlow } from "./step-flow"; export type DisplayStep = { id: string; label: string; completed: boolean }; -type ProgressStep = BridgeStepType | SwapStepType; interface TokenSource { tokenLogo: string; @@ -35,20 +33,20 @@ interface TransactionProgressProps { } const STEP_TYPES = { - INTENT_VERIFICATION: ["CREATE_PERMIT_FOR_SOURCE_SWAP"], + // v2 step type strings (snake_case) + INTENT_VERIFICATION: ["bridge_intent_submission", "request_signing"], SOURCE_STEP_TYPES: [ - "CREATE_PERMIT_EOA_TO_EPHEMERAL", - "CREATE_PERMIT_FOR_SOURCE_SWAP", - "SOURCE_SWAP_BATCH_TX", - "SOURCE_SWAP_HASH", + "eoa_to_ephemeral_transfer", + "source_swap", + "bridge_deposit", + "bridge_intent_submission", ], - SOURCE_TRANSACTION: ["SOURCE_SWAP_HASH", "SOURCE_SWAP_BATCH_TX"], + SOURCE_TRANSACTION: ["source_swap", "bridge_deposit"], DESTINATION_STEP_TYPES: [ - "DESTINATION_SWAP_BATCH_TX", - "DESTINATION_SWAP_HASH", - "SWAP_COMPLETE", + "bridge_fill", + "destination_swap", ], - TRANSACTION_COMPLETE: ["SWAP_COMPLETE"], + TRANSACTION_COMPLETE: ["bridge_fill", "destination_swap"], }; const TransactionProgress: FC = ({ @@ -70,8 +68,9 @@ const TransactionProgress: FC = ({ steps ?.filter((s) => { const st = s?.step ?? {}; + // v2: emitted steps have chain, id, or other properties merged in return ( - "explorerURL" in st || "chain" in st || "completed" in st // present when event args were merged into step + "chain" in st || "id" in st || "explorerURL" in st || "completed" in st ); }) .map((s) => s?.step?.type) @@ -80,10 +79,14 @@ const TransactionProgress: FC = ({ types.some((t) => completedTypes.has(t)); const sawAny = (types: string[]) => types.some((t) => eventfulTypes.has(t)); - const intentVerified = hasAny(["DETERMINING_SWAP", "SWAP_START"]); + // v2: intent is verified once the bridge_intent_submission step completes, + // OR implicitly if any source/destination step is already done + const intentVerified = + hasAny(STEP_TYPES.INTENT_VERIFICATION) || + hasAny(STEP_TYPES.SOURCE_STEP_TYPES) || + hasAny(STEP_TYPES.DESTINATION_STEP_TYPES); // If the flow does not include SOURCE_* steps, consider it implicitly collected - const collectedOnSources = (sawAny(STEP_TYPES.SOURCE_STEP_TYPES) && hasAny(STEP_TYPES.SOURCE_TRANSACTION)) || @@ -106,7 +109,7 @@ const TransactionProgress: FC = ({ }, ]; - // Mark overall completion ONLY when the SDK reports SWAP_COMPLETE + // Mark overall completion ONLY when the SDK reports the final destination step const done = hasAny(STEP_TYPES.TRANSACTION_COMPLETE); const current = displaySteps.findIndex((st) => !st.completed); return { diff --git a/registry/nexus-elements/swaps/components/view-transaction.tsx b/registry/nexus-elements/swaps/components/view-transaction.tsx index 30aee75..4e1a656 100644 --- a/registry/nexus-elements/swaps/components/view-transaction.tsx +++ b/registry/nexus-elements/swaps/components/view-transaction.tsx @@ -5,11 +5,8 @@ import { DialogContent, DialogHeader, } from "../../ui/dialog"; -import { - type SwapStepType, - type OnSwapIntentHookData, - formatTokenBalance, -} from "@avail-project/nexus-core"; +import { type OnSwapIntentHookData } from "@avail-project/nexus-sdk-v2"; +import { formatTokenBalance } from "@avail-project/nexus-sdk-v2/utils"; import { ChevronDown, ChevronUp, Info, MoveDown, XIcon } from "lucide-react"; import { TokenIcon } from "./token-icon"; import { StackedTokenIcons } from "./stacked-token-icons"; @@ -71,7 +68,8 @@ function formatImpactPercent(value: number): string { } interface ViewTransactionProps { - steps: GenericStep[]; + // v2: SwapStepType renamed to SwapPlanStep; use generic record for GenericStep + steps: GenericStep<{ type?: string; [key: string]: unknown }>[]; status: TransactionStatus; swapMode: SwapMode; swapIntent: RefObject; @@ -90,24 +88,23 @@ interface ViewTransactionProps { txError: string | null; } -interface TokenBreakdownProps - extends Omit< - ViewTransactionProps, - | "swapIntent" - | "continueSwap" - | "status" - | "explorerUrls" - | "steps" - | "reset" - | "txError" - | "swapMode" - | "nexusSDK" - | "exactOutSourceOptions" - | "exactOutSelectedKeys" - | "toggleExactOutSource" - | "isExactOutSourceSelectionDirty" - | "updatingExactOutSources" - > { +interface TokenBreakdownProps extends Omit< + ViewTransactionProps, + | "swapIntent" + | "continueSwap" + | "status" + | "explorerUrls" + | "steps" + | "reset" + | "txError" + | "swapMode" + | "nexusSDK" + | "exactOutSourceOptions" + | "exactOutSelectedKeys" + | "toggleExactOutSource" + | "isExactOutSourceSelectionDirty" + | "updatingExactOutSources" +> { tokenLogo: string; chainLogo: string; symbol: string; @@ -311,10 +308,7 @@ const ViewTransaction: FC = ({ const bridgeUsd = bridgeExplicitTotal > 0 ? bridgeExplicitTotal : bridgeComponentTotal; const knownBridgeRowsUsd = - gasSponsorshipUsd + - executionGasFeeUsd + - protocolFeeUsd + - solverFeeUsd; + gasSponsorshipUsd + executionGasFeeUsd + protocolFeeUsd + solverFeeUsd; const otherBridgeFeeUsd = Math.max(0, bridgeUsd - knownBridgeRowsUsd); const bufferUsd = parseNonNegativeNumber(feesAndBuffer?.buffer); @@ -365,17 +359,16 @@ const ViewTransaction: FC = ({ }, [transactionIntent, getFiatValue, sources]); const feeDetailRows = useMemo( - () => - [ - { label: "Gas sponsorship", amountUsd: feeBreakdown.gasSponsorshipUsd }, - { - label: "Execution Gas fee", - amountUsd: - feeBreakdown.executionGasFeeUsd + feeBreakdown.otherBridgeFeeUsd, - }, - { label: "Protocol fee", amountUsd: feeBreakdown.protocolFeeUsd }, - { label: "Solver fee", amountUsd: feeBreakdown.solverFeeUsd }, - ], + () => [ + { label: "Gas sponsorship", amountUsd: feeBreakdown.gasSponsorshipUsd }, + { + label: "Execution Gas fee", + amountUsd: + feeBreakdown.executionGasFeeUsd + feeBreakdown.otherBridgeFeeUsd, + }, + { label: "Protocol fee", amountUsd: feeBreakdown.protocolFeeUsd }, + { label: "Solver fee", amountUsd: feeBreakdown.solverFeeUsd }, + ], [feeBreakdown], ); diff --git a/registry/nexus-elements/swaps/config/destination.ts b/registry/nexus-elements/swaps/config/destination.ts index 92312c5..ded86ef 100644 --- a/registry/nexus-elements/swaps/config/destination.ts +++ b/registry/nexus-elements/swaps/config/destination.ts @@ -1,4 +1,4 @@ -import { SUPPORTED_CHAINS } from "@avail-project/nexus-core"; +const SUPPORTED_CHAINS = { OPTIMISM: 10, ETHEREUM: 1, MONAD: 143, MEGAETH: 4326, ARBITRUM: 42161, SCROLL: 534352, BASE: 8453, BNB: 56 } as const; export const DESTINATION_SWAP_TOKENS = new Map< number, diff --git a/registry/nexus-elements/swaps/hooks/useSwaps.ts b/registry/nexus-elements/swaps/hooks/useSwaps.ts index 24c8812..e043d6e 100644 --- a/registry/nexus-elements/swaps/hooks/useSwaps.ts +++ b/registry/nexus-elements/swaps/hooks/useSwaps.ts @@ -7,21 +7,19 @@ import { useRef, useState, } from "react"; -import { - NexusSDK, - type SUPPORTED_CHAINS_IDS, - type ExactInSwapInput, - type ExactOutSwapInput, - NEXUS_EVENTS, - type SwapStepType, - type OnSwapIntentHookData, - type Source as SwapSource, - type UserAsset, - sortSourcesByPriority, - parseUnits, - formatTokenBalance, -} from "@avail-project/nexus-core"; -import { padHex, type Hex } from "viem"; +import type { createNexusClient } from "@avail-project/nexus-sdk-v2"; +import type { + SwapExactInParams, + SwapExactOutParams, + SwapEvent, + SwapPlanStep, + OnSwapIntentHookData, + Source as SwapSource, + TokenBalance, + ChainBalance, +} from "@avail-project/nexus-sdk-v2"; +import { formatTokenBalance } from "@avail-project/nexus-sdk-v2/utils"; +import { padHex, parseUnits, type Hex } from "viem"; import { useTransactionSteps, SWAP_EXPECTED_STEPS, @@ -35,6 +33,8 @@ import { getIntentSourcesSignature, } from "../utils/source-matching"; +type NexusClient = ReturnType; + const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; const EVM_NATIVE_PLACEHOLDER = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; @@ -54,12 +54,12 @@ function toComparableSdkAddress(address: string): string { } } -type AssetBreakdownWithOptionalIcon = UserAsset["breakdown"][number] & { +type AssetBreakdownWithOptionalIcon = ChainBalance & { icon?: string; }; function getBreakdownTokenIcon( - breakdown: UserAsset["breakdown"][number], + breakdown: ChainBalance, ): string { const icon = (breakdown as AssetBreakdownWithOptionalIcon).icon; return typeof icon === "string" && icon.length > 0 ? icon : ""; @@ -109,10 +109,10 @@ export type TransactionStatus = export type SwapMode = "exactIn" | "exactOut"; export interface SwapInputs { - fromChainID?: SUPPORTED_CHAINS_IDS; + fromChainID?: number; fromToken?: SourceTokenInfo; fromAmount?: string; - toChainID?: SUPPORTED_CHAINS_IDS; + toChainID?: number; toToken?: DestinationTokenInfo; toAmount?: string; } @@ -134,9 +134,9 @@ type Action = | { type: "setError"; payload: string | null } | { type: "setSwapMode"; payload: SwapMode } | { - type: "setExplorerUrls"; - payload: Partial; - } + type: "setExplorerUrls"; + payload: Partial; + } | { type: "reset" }; const initialState: SwapState = { @@ -187,15 +187,23 @@ function reducer(state: SwapState, action: Action): SwapState { } interface UseSwapsProps { - nexusSDK: NexusSDK | null; + nexusSDK: NexusClient | null; swapIntent: RefObject; - swapBalance: UserAsset[] | null; + swapBalance: TokenBalance[] | null; fetchBalance: () => Promise; onComplete?: (amount?: string) => void; onStart?: () => void; onError?: (message: string) => void; } +// v2 swap step shape (minimal, used for step-tracking) +type SwapStep = { + typeID?: string; + type?: string; + explorerURL?: string; + [key: string]: unknown; +}; + const useSwaps = ({ nexusSDK, swapIntent, @@ -211,7 +219,7 @@ const useSwaps = ({ seed, onStepComplete, reset: resetSteps, - } = useTransactionSteps(); + } = useTransactionSteps(); const swapRunIdRef = useRef(0); const lastSyncedIntentSourcesSignatureRef = useRef(""); const lastSyncedIntentSelectionKeyRef = useRef(""); @@ -231,7 +239,7 @@ const useSwaps = ({ }; for (const asset of swapBalance ?? []) { - for (const entry of asset.breakdown ?? []) { + for (const entry of asset.chainBalances ?? []) { const balance = entry.balance ?? "0"; const parsed = Number.parseFloat(balance); if (!Number.isFinite(parsed) || parsed <= 0) continue; @@ -250,7 +258,8 @@ const useSwaps = ({ chainName: entry.chain.name, chainLogo: entry.chain.logo, tokenAddress, - tokenSymbol: entry.symbol, + // v2: breakdown has no .symbol — use parent asset.symbol + tokenSymbol: asset.symbol, tokenLogo: getBreakdownTokenIcon(entry), balance, decimals: entry.decimals ?? asset.decimals, @@ -285,10 +294,11 @@ const useSwaps = ({ const options = [...optionsByKey.values()]; + // v2: sortSourcesByPriority removed — sort by balance descending const destinationChainId = state.inputs.toChainID; const destinationToken = state.inputs.toToken; if (!destinationChainId || !destinationToken || !swapBalance?.length) { - return options.sort((a, b) => { + return options.sort((a: ExactOutSourceOption, b: ExactOutSourceOption) => { if (a.tokenSymbol === b.tokenSymbol) { return a.chainName.localeCompare(b.chainName); } @@ -296,47 +306,12 @@ const useSwaps = ({ }); } - const priorityByOptionKey = new Map(); - const sortedSources = sortSourcesByPriority(swapBalance, { - chainID: destinationChainId, - tokenAddress: destinationToken.tokenAddress, - symbol: destinationToken.symbol, - }); - - sortedSources.forEach((source, index) => { - const sourceComparableAddress = toComparableSdkAddress( - source.tokenAddress, - ); - - for (const option of options) { - if (option.chainId !== source.chainID) continue; - const optionComparableAddress = toComparableSdkAddress( - option.tokenAddress, - ); - if (optionComparableAddress !== sourceComparableAddress) continue; - if (!priorityByOptionKey.has(option.key)) { - priorityByOptionKey.set(option.key, index); - } - } - }); - - return options.sort((a, b) => { - const aPriority = - priorityByOptionKey.get(a.key) ?? Number.MAX_SAFE_INTEGER; - const bPriority = - priorityByOptionKey.get(b.key) ?? Number.MAX_SAFE_INTEGER; - if (aPriority !== bPriority) { - return aPriority - bPriority; - } - + return options.sort((a: ExactOutSourceOption, b: ExactOutSourceOption) => { const aBalance = Number.parseFloat(a.balance); const bBalance = Number.parseFloat(b.balance); if (Number.isFinite(aBalance) && Number.isFinite(bBalance)) { - if (aBalance !== bBalance) { - return bBalance - aBalance; - } + if (aBalance !== bBalance) return bBalance - aBalance; } - if (a.tokenSymbol === b.tokenSymbol) { return a.chainName.localeCompare(b.chainName); } @@ -474,6 +449,7 @@ const useSwaps = ({ return sources.length > 0 ? sources : undefined; }, [state.swapMode, effectiveExactOutSelectedKeys, exactOutSourceOptions]); + const isExactOutSourceSelectionDirty = useMemo(() => { return ( state.swapMode === "exactOut" && @@ -514,27 +490,95 @@ const useSwaps = ({ const handleNexusError = useNexusError(); - // Event handler shared between exact-in and exact-out - const handleSwapEvent = (event: { name: string; args: SwapStepType }) => { - if (event.name === NEXUS_EVENTS.SWAP_STEP_COMPLETE) { - const step = event.args; - if (step?.type === "SOURCE_SWAP_HASH" && step.explorerURL) { + /** + * v2 swap event handler + * SwapEvent is a typed discriminated union: { type: 'plan_preview' | 'plan_progress' | ... } + * Explorer URLs are on the event itself (not on step.explorerURL) + */ + const handleSwapEvent = useCallback((event: SwapEvent, runId: number, completedFromEventRef: { current: boolean }) => { + if (swapRunIdRef.current !== runId) return; + + if (event.type === "plan_preview") { + // Seed step tracker from plan; cast to our internal step shape + const planSteps = (event as { type: string; plan: { steps: SwapPlanStep[] } }).plan?.steps ?? []; + seed(planSteps.map((s, i) => { + const stepType = (s as { type?: string }).type ?? `step-${i}`; + return { + typeID: `step-${i}`, + stepType, + ...s, + }; + })); + return; + } + + if (event.type === "plan_progress") { + const progressEvent = event as { + type: string; + stepType: string; + state: string; + step: SwapPlanStep; + explorerUrl?: string; + error?: string; + }; + + // v2: explorerUrl is on the event, not step.explorerURL + const explorerUrl = progressEvent.explorerUrl; + + if ( + progressEvent.stepType === "source_swap" && + (progressEvent.state === "submitted" || progressEvent.state === "confirmed") && + explorerUrl + ) { dispatch({ type: "setExplorerUrls", - payload: { sourceExplorerUrl: step.explorerURL }, + payload: { sourceExplorerUrl: explorerUrl }, }); } - if (step?.type === "DESTINATION_SWAP_HASH" && step.explorerURL) { + + if ( + progressEvent.stepType === "destination_swap" && + (progressEvent.state === "submitted" || progressEvent.state === "confirmed") && + explorerUrl + ) { dispatch({ type: "setExplorerUrls", - payload: { destinationExplorerUrl: step.explorerURL }, + payload: { destinationExplorerUrl: explorerUrl }, }); } - onStepComplete(step); + + const step = progressEvent.step as SwapStep; + onStepComplete({ + typeID: progressEvent.stepType, + type: progressEvent.stepType, + ...step, + explorerURL: explorerUrl, + }); + + // Drive success/failure from events, not from the SDK promise resolution + if (progressEvent.state === "failed" && !completedFromEventRef.current) { + completedFromEventRef.current = true; + const errorMessage = progressEvent.error ?? "Swap failed"; + dispatch({ type: "setStatus", payload: "error" }); + dispatch({ type: "setError", payload: errorMessage }); + onError?.(errorMessage); + return; + } + + if ( + progressEvent.stepType === "destination_swap" && + progressEvent.state === "completed" && + !completedFromEventRef.current + ) { + completedFromEventRef.current = true; + dispatch({ type: "setStatus", payload: "success" }); + onComplete?.(swapIntent.current?.intent?.destination?.amount); + void fetchBalance(); + } } - }; + }, [seed, onStepComplete, dispatch, onError, onComplete, fetchBalance, swapRunIdRef, swapIntent]); - const handleExactInSwap = async (runId: number) => { + const handleExactInSwap = async (runId: number, completedFromEventRef: { current: boolean }) => { const fromToken = state.inputs.fromToken; const toToken = state.inputs.toToken; const fromAmount = state.inputs.fromAmount; @@ -553,12 +597,12 @@ const useSwaps = ({ return; const sourceBalance = swapBalance - ?.flatMap((token) => token.breakdown ?? []) + ?.flatMap((token) => token.chainBalances ?? []) ?.find( (chain) => chain.chain?.id === fromChainID && normalizeAddress(chain.contractAddress) === - normalizeAddress(fromToken.contractAddress), + normalizeAddress(fromToken.contractAddress), ); if ( !sourceBalance || @@ -570,31 +614,32 @@ const useSwaps = ({ } const amountBigInt = parseUnits(fromAmount, fromToken.decimals); - const swapInput: ExactInSwapInput = { - from: [ + + // v2: SwapExactInParams — sources replaces `from`, amountRaw in source + const swapInput: SwapExactInParams = { + sources: [ { chainId: fromChainID, - amount: amountBigInt, tokenAddress: fromToken.contractAddress, + amountRaw: amountBigInt, }, ], toChainId: toChainID, toTokenAddress: toToken.tokenAddress, }; - const result = await nexusSDK.swapWithExactIn(swapInput, { - onEvent: (event) => { - if (swapRunIdRef.current !== runId) return; - handleSwapEvent(event as { name: string; args: SwapStepType }); + // v2: returns SuccessfulSwapResult directly; throws on error (no .success wrapper) + await nexusSDK.swapWithExactIn(swapInput, { + onEvent: (event) => handleSwapEvent(event, runId, completedFromEventRef), + hooks: { + onIntent: (data) => { + swapIntent.current = data; + }, }, }); - - if (!result?.success) { - throw new Error(result?.error || "Swap failed"); - } }; - const handleExactOutSwap = async (runId: number) => { + const handleExactOutSwap = async (runId: number, completedFromEventRef: { current: boolean }) => { const toToken = state.inputs.toToken; const toAmount = state.inputs.toAmount; const toChainID = state.inputs.toChainID; @@ -617,27 +662,32 @@ const useSwaps = ({ } const amountBigInt = parseUnits(toAmount, toToken.decimals); - const swapInput: ExactOutSwapInput = { - toAmount: amountBigInt, + + // v2: SwapExactOutParams — toAmountRaw replaces toAmount + const swapInput: SwapExactOutParams = { + toAmountRaw: amountBigInt, toChainId: toChainID, toTokenAddress: toToken.tokenAddress, - ...(exactOutFromSources ? { fromSources: exactOutFromSources } : {}), + ...(exactOutFromSources ? { sources: exactOutFromSources } : {}), }; - const result = await nexusSDK.swapWithExactOut(swapInput, { - onEvent: (event) => { - if (swapRunIdRef.current !== runId) return; - handleSwapEvent(event as { name: string; args: SwapStepType }); + // v2: returns SuccessfulSwapResult directly; throws on error + await nexusSDK.swapWithExactOut(swapInput, { + onEvent: (event) => handleSwapEvent(event, runId, completedFromEventRef), + hooks: { + onIntent: (data) => { + swapIntent.current = data; + }, }, }); - if (!result?.success) { - throw new Error(result?.error || "Swap failed"); - } }; const runSwap = async (runId: number) => { if (!nexusSDK || !areInputsValid || !swapBalance) return; + // Used by handleSwapEvent to signal completion without waiting for the promise + const completedFromEventRef = { current: false }; + try { onStart?.(); dispatch({ type: "setStatus", payload: "simulating" }); @@ -651,17 +701,21 @@ const useSwaps = ({ } if (state.swapMode === "exactIn") { - await handleExactInSwap(runId); + await handleExactInSwap(runId, completedFromEventRef); } else { - await handleExactOutSwap(runId); + await handleExactOutSwap(runId, completedFromEventRef); } if (swapRunIdRef.current !== runId) return; - dispatch({ type: "setStatus", payload: "success" }); - onComplete?.(swapIntent.current?.intent?.destination?.amount); - await fetchBalance(); + if (!completedFromEventRef.current) { + // Fallback: SDK resolved but terminal event never arrived (single-step flows) + dispatch({ type: "setStatus", payload: "success" }); + onComplete?.(swapIntent.current?.intent?.destination?.amount); + await fetchBalance(); + } } catch (error) { if (swapRunIdRef.current !== runId) return; + if (completedFromEventRef.current) return; // event already handled failure const { message } = handleNexusError(error); dispatch({ type: "setStatus", payload: "error" }); dispatch({ type: "setError", payload: message }); @@ -746,12 +800,12 @@ const useSwaps = ({ return undefined; return ( swapBalance - ?.flatMap((token) => token.breakdown ?? []) + ?.flatMap((token) => token.chainBalances ?? []) ?.find( (chain) => chain.chain?.id === state.inputs?.fromChainID && normalizeAddress(chain.contractAddress) === - normalizeAddress(state.inputs?.fromToken?.contractAddress ?? ""), + normalizeAddress(state.inputs?.fromToken?.contractAddress ?? ""), ) ?? undefined ); }, [ @@ -771,12 +825,12 @@ const useSwaps = ({ return undefined; return ( swapBalance - ?.flatMap((token) => token.breakdown ?? []) + ?.flatMap((token) => token.chainBalances ?? []) ?.find( (chain) => chain.chain?.id === state?.inputs?.toChainID && normalizeAddress(chain.contractAddress) === - normalizeAddress(state?.inputs?.toToken?.tokenAddress ?? ""), + normalizeAddress(state?.inputs?.toToken?.tokenAddress ?? ""), ) ?? undefined ); }, [state?.inputs?.toToken, state?.inputs?.toChainID, swapBalance, nexusSDK]); @@ -784,10 +838,9 @@ const useSwaps = ({ const availableStables = useMemo(() => { if (!nexusSDK || !swapBalance) return []; const stableSymbols = new Set(["USDT", "USDC", "ETH", "DAI", "WBTC"]); + // v2: breakdown has no .symbol — use token.symbol from the parent TokenBalance const filteredToken = swapBalance.filter((token) => - (token.breakdown ?? []).some((entry) => - stableSymbols.has(entry.symbol.toUpperCase()), - ), + stableSymbols.has(token.symbol.toUpperCase()), ); return filteredToken ?? []; }, [swapBalance, nexusSDK]); @@ -815,15 +868,15 @@ const useSwaps = ({ const isValidForCurrentMode = state.swapMode === "exactIn" ? areExactInInputsValid && - state?.inputs?.fromAmount && - state?.inputs?.fromChainID && - state?.inputs?.fromToken && - state?.inputs?.toChainID && - state?.inputs?.toToken + state?.inputs?.fromAmount && + state?.inputs?.fromChainID && + state?.inputs?.fromToken && + state?.inputs?.toChainID && + state?.inputs?.toToken : areExactOutInputsValid && - state?.inputs?.toAmount && - state?.inputs?.toChainID && - state?.inputs?.toToken; + state?.inputs?.toAmount && + state?.inputs?.toChainID && + state?.inputs?.toToken; if (!isValidForCurrentMode) { swapIntent.current?.deny(); diff --git a/registry/nexus-elements/swaps/swap-widget.tsx b/registry/nexus-elements/swaps/swap-widget.tsx index fd2217c..54fb845 100644 --- a/registry/nexus-elements/swaps/swap-widget.tsx +++ b/registry/nexus-elements/swaps/swap-widget.tsx @@ -22,7 +22,7 @@ function SwapWidget({ }>) { const sourceContainer = useRef(null); const destinationContainer = useRef(null); - const { nexusSDK, swapIntent, swapBalance, fetchSwapBalance, getFiatValue } = + const { nexusSDK, swapWidgetIntent, swapBalance, fetchSwapBalance, getFiatValue } = useNexus(); const { status, @@ -47,7 +47,7 @@ function SwapWidget({ updatingExactOutSources, } = useSwaps({ nexusSDK, - swapIntent, + swapIntent: swapWidgetIntent, swapBalance, fetchBalance: fetchSwapBalance, onComplete, @@ -58,8 +58,8 @@ function SwapWidget({ const destinationHovered = useHover(destinationContainer); const handleInputSwitch = useCallback(() => { - swapIntent.current?.deny(); - swapIntent.current = null; + swapWidgetIntent.current?.deny(); + swapWidgetIntent.current = null; // Always reset to exactIn mode and clear amounts when switching setSwapMode("exactIn"); @@ -77,7 +77,7 @@ function SwapWidget({ return; } const isValidSource = swapBalance?.some((asset) => - (asset.breakdown ?? []).some( + (asset.chainBalances ?? []).some( (entry) => entry.chain?.id === inputs.toChainID && entry.contractAddress.toLowerCase() === @@ -123,7 +123,7 @@ function SwapWidget({ toAmount: undefined, }; setInputs(switched); - }, [inputs, swapIntent, swapBalance, setSwapMode, setInputs]); + }, [inputs, swapWidgetIntent, swapBalance, setSwapMode, setInputs]); const buttonIcons = useMemo(() => { if (status === "simulating") { @@ -151,7 +151,7 @@ function SwapWidget({ availableBalance={availableBalance} swapBalance={swapBalance} swapMode={swapMode} - swapIntent={swapIntent} + swapIntent={swapWidgetIntent} setInputs={setInputs} setSwapMode={setSwapMode} setTxError={setTxError} @@ -182,7 +182,7 @@ function SwapWidget({ destinationHovered={destinationHovered} inputs={inputs} setInputs={setInputs} - swapIntent={swapIntent} + swapIntent={swapWidgetIntent} destinationBalance={destinationBalance} swapBalance={swapBalance} availableStables={availableStables} @@ -206,7 +206,7 @@ function SwapWidget({ steps={steps} status={status} swapMode={swapMode} - swapIntent={swapIntent} + swapIntent={swapWidgetIntent} getFiatValue={getFiatValue} continueSwap={continueSwap} exactOutSourceOptions={exactOutSourceOptions} diff --git a/registry/nexus-elements/transfer/components/allowance-modal.tsx b/registry/nexus-elements/transfer/components/allowance-modal.tsx index 8e008ee..9f84cfe 100644 --- a/registry/nexus-elements/transfer/components/allowance-modal.tsx +++ b/registry/nexus-elements/transfer/components/allowance-modal.tsx @@ -12,11 +12,10 @@ import { Input } from "../../ui/input"; import { Label } from "../../ui/label"; import { type AllowanceHookSource, - CHAIN_METADATA, - formatTokenBalance, type OnAllowanceHookData, - parseUnits, -} from "@avail-project/nexus-core"; +} from "@avail-project/nexus-sdk-v2"; +import { formatTokenBalance } from "@avail-project/nexus-sdk-v2/utils"; +import { parseUnits } from "viem"; import { useNexusError } from "../../common"; interface AllowanceModalProps { @@ -228,7 +227,8 @@ const AllowanceModal: FC = ({
{source.chain.name} = ({
- {bridgableBalance?.breakdown.map((chain) => { + {bridgableBalance?.chainBalances?.map((chain: any) => { if (Number.parseFloat(chain.balance) === 0) return null; if (inputs?.chain === chain.chain.id) return null; return ( diff --git a/registry/nexus-elements/transfer/components/chain-select.tsx b/registry/nexus-elements/transfer/components/chain-select.tsx index 3dde1dd..1c84a20 100644 --- a/registry/nexus-elements/transfer/components/chain-select.tsx +++ b/registry/nexus-elements/transfer/components/chain-select.tsx @@ -8,7 +8,7 @@ import { SelectTrigger, SelectValue, } from "../../ui/select"; -import { type SUPPORTED_CHAINS_IDS } from "@avail-project/nexus-core"; +// v2: SUPPORTED_CHAINS_IDS removed — use plain number import { cn } from "@/lib/utils"; import { useNexus } from "../../nexus/NexusProvider"; @@ -18,7 +18,7 @@ interface ChainSelectProps { hidden?: boolean; className?: string; label?: string; - handleSelect: (chainId: SUPPORTED_CHAINS_IDS) => void; + handleSelect: (chainId: number) => void; } const ChainSelect: FC = ({ @@ -41,7 +41,7 @@ const ChainSelect: FC = ({ value={selectedChain?.toString() ?? ""} onValueChange={(value) => { if (!disabled) { - handleSelect(Number.parseInt(value) as SUPPORTED_CHAINS_IDS); + handleSelect(Number.parseInt(value)); } }} > diff --git a/registry/nexus-elements/transfer/components/fee-breakdown.tsx b/registry/nexus-elements/transfer/components/fee-breakdown.tsx index 0d1cd00..9e14ae1 100644 --- a/registry/nexus-elements/transfer/components/fee-breakdown.tsx +++ b/registry/nexus-elements/transfer/components/fee-breakdown.tsx @@ -5,19 +5,16 @@ import { AccordionItem, AccordionTrigger, } from "../../ui/accordion"; -import { - formatTokenBalance, - SUPPORTED_TOKENS, - type ReadableIntent, -} from "@avail-project/nexus-core"; +import { type BridgeIntent } from "@avail-project/nexus-sdk-v2"; +import { formatTokenBalance } from "@avail-project/nexus-sdk-v2/utils"; import { Skeleton } from "../../ui/skeleton"; import { useNexus } from "../../nexus/NexusProvider"; import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/tooltip"; import { MessageCircleQuestion } from "lucide-react"; interface FeeBreakdownProps { - intent: ReadableIntent; - tokenSymbol: SUPPORTED_TOKENS; + intent: BridgeIntent; + tokenSymbol?: string; // v2: was SUPPORTED_TOKENS isLoading?: boolean; } @@ -37,9 +34,9 @@ const FeeBreakdown: FC = ({ "Gas cost required to execute the transfer on the destination chain.", }, { - key: "gasSupplied", + key: "caGas", label: "Gas Supplied", - value: intent?.fees?.gasSupplied, + value: intent?.fees?.caGas, description: "Extra gas tokens supplied to ensure the transfer completes on-chain.", }, @@ -71,7 +68,7 @@ const FeeBreakdown: FC = ({

{formatTokenBalance(intent.fees?.total, { symbol: tokenSymbol, - decimals: intent?.token?.decimals, + decimals: intent?.availableSources?.[0]?.token?.decimals, })}

)} @@ -103,7 +100,7 @@ const FeeBreakdown: FC = ({

{formatTokenBalance(value, { symbol: tokenSymbol, - decimals: intent?.token?.decimals, + decimals: intent?.availableSources?.[0]?.token?.decimals, })}

)} diff --git a/registry/nexus-elements/transfer/components/recipient-address.tsx b/registry/nexus-elements/transfer/components/recipient-address.tsx index e0a5dbd..91176ad 100644 --- a/registry/nexus-elements/transfer/components/recipient-address.tsx +++ b/registry/nexus-elements/transfer/components/recipient-address.tsx @@ -5,7 +5,7 @@ import { Check, Edit } from "lucide-react"; import { Button } from "../../ui/button"; import { useNexus } from "../../nexus/NexusProvider"; import { type Address } from "viem"; -import { truncateAddress } from "@avail-project/nexus-core"; +import { truncateAddress } from "@avail-project/nexus-sdk-v2/utils"; interface RecipientAddressProps { address?: Address; diff --git a/registry/nexus-elements/transfer/components/source-breakdown.tsx b/registry/nexus-elements/transfer/components/source-breakdown.tsx index 1f3e4b6..9a8fc96 100644 --- a/registry/nexus-elements/transfer/components/source-breakdown.tsx +++ b/registry/nexus-elements/transfer/components/source-breakdown.tsx @@ -1,9 +1,5 @@ -import { - formatTokenBalance, - type ReadableIntent, - type SUPPORTED_TOKENS, - type UserAsset, -} from "@avail-project/nexus-core"; +import { type BridgeIntent, type ChainBalance } from "@avail-project/nexus-sdk-v2"; +import { formatTokenBalance } from "@avail-project/nexus-sdk-v2/utils"; import { Accordion, AccordionContent, @@ -18,11 +14,12 @@ import { cn } from "@/lib/utils"; type SourceCoverageState = "healthy" | "warning" | "error"; interface SourceBreakdownProps { - intent?: ReadableIntent; - tokenSymbol: SUPPORTED_TOKENS; + intent?: BridgeIntent; + tokenSymbol?: string; isLoading?: boolean; requiredAmount?: string; - availableSources: UserAsset["breakdown"]; + // v2: ChainBalance replaces UserAssetDatum["breakdown"] items + availableSources: ChainBalance[]; selectedSourceChains: number[]; onToggleSourceChain: (chainId: number) => void; onSourceMenuOpenChange?: (open: boolean) => void; @@ -53,7 +50,7 @@ const SourceBreakdown = ({ requiredTotal, requiredSafetyTotal, }: SourceBreakdownProps) => { - const displayTokenSymbol = availableSources[0]?.symbol ?? tokenSymbol; + const displayTokenSymbol = tokenSymbol ?? availableSources[0]?.chain?.name; const normalizedCoverage = Math.max(0, Math.min(100, sourceCoveragePercent)); const progressRadius = 16; const progressCircumference = 2 * Math.PI * progressRadius; @@ -110,9 +107,9 @@ const SourceBreakdown = ({ }; const spendOnSources = useMemo(() => { - if (!intent || (intent?.sources?.length ?? 0) < 2) + if (!intent || (intent?.availableSources?.length ?? 0) < 2) return `1 asset on 1 chain`; - return `${intent?.sources?.length} Assets on ${intent?.sources?.length} chains`; + return `${intent?.availableSources?.length} Assets on ${intent?.availableSources?.length} chains`; }, [intent]); const amountSpend = useMemo(() => { @@ -154,9 +151,10 @@ const SourceBreakdown = ({

- {formatTokenBalance(amountSpend, { + {formatTokenBalance(amountSpend, { symbol: displayTokenSymbol, - decimals: intent?.token?.decimals, + // v2: BridgeIntent.availableSources replaces allSources + decimals: intent?.availableSources?.[0]?.token?.decimals, })}

@@ -279,8 +277,8 @@ const SourceBreakdown = ({ ? selectedSourceChains.length === 1 : false; - const willUseFromIntent = intent?.sources?.find( - (s) => s.chainID === chainId, + const willUseFromIntent = intent?.availableSources?.find( + (s: any) => s.chain.id === chainId, )?.amount; return ( @@ -318,7 +316,7 @@ const SourceBreakdown = ({ aria-label={`Select ${source.chain.name} as a source`} /> {source.chain.name}

{formatTokenBalance(source.balance, { - symbol: source.symbol, + symbol: tokenSymbol ?? source.chain?.name, decimals: source.decimals, })}

@@ -340,8 +338,10 @@ const SourceBreakdown = ({

Estimated to use:{" "} {formatTokenBalance(willUseFromIntent, { - symbol: source.symbol, - decimals: intent?.token?.decimals, + symbol: tokenSymbol ?? source.chain?.name, + // v2: BridgeIntent.availableSources replaces allSources + decimals: + intent?.availableSources?.[0]?.token?.decimals, })}

)} diff --git a/registry/nexus-elements/transfer/components/token-select.tsx b/registry/nexus-elements/transfer/components/token-select.tsx index e869231..8dfb147 100644 --- a/registry/nexus-elements/transfer/components/token-select.tsx +++ b/registry/nexus-elements/transfer/components/token-select.tsx @@ -1,7 +1,4 @@ -import { - type SUPPORTED_CHAINS_IDS, - type SUPPORTED_TOKENS, -} from "@avail-project/nexus-core"; +// v2: SUPPORTED_CHAINS_IDS, SUPPORTED_TOKENS removed — use plain string/number import { Select, SelectContent, @@ -15,9 +12,9 @@ import { useNexus } from "../../nexus/NexusProvider"; import { useMemo } from "react"; interface TokenSelectProps { - selectedToken?: SUPPORTED_TOKENS; - selectedChain: SUPPORTED_CHAINS_IDS; - handleTokenSelect: (token: SUPPORTED_TOKENS) => void; + selectedToken?: string; + selectedChain: number; + handleTokenSelect: (token: string) => void; isTestnet?: boolean; disabled?: boolean; label?: string; @@ -46,7 +43,7 @@ const TokenSelect = ({