diff --git a/examples/node/package.json b/examples/node/package.json index 9cc1e49e..1becc4aa 100644 --- a/examples/node/package.json +++ b/examples/node/package.json @@ -7,12 +7,12 @@ "author": "LI.FI ", "license": "MIT", "dependencies": { - "@lifi/data-types": "^5.23.0", - "@lifi/sdk": "^3.5.2", - "@wagmi/connectors": "^5.7.5", + "@lifi/data-types": "^5.24.0", + "@lifi/sdk": "^3.5.3", + "@wagmi/connectors": "^5.7.6", "@wagmi/core": "^2.16.3", "dotenv": "^16.4.7", - "viem": "^2.22.21" + "viem": "^2.22.22" }, "scripts": { "example:swap": "tsx examples/swap.ts", @@ -27,5 +27,6 @@ "@types/node": "^22.13.1", "tsx": "^4.19.2", "typescript": "^5.7.3" - } + }, + "packageManager": "pnpm@10.2.0" } diff --git a/examples/node/pnpm-lock.yaml b/examples/node/pnpm-lock.yaml index c2cd2342..4f4f3188 100644 --- a/examples/node/pnpm-lock.yaml +++ b/examples/node/pnpm-lock.yaml @@ -9,23 +9,23 @@ importers: .: dependencies: '@lifi/data-types': - specifier: ^5.23.0 - version: 5.23.0 + specifier: ^5.24.0 + version: 5.24.0 '@lifi/sdk': - specifier: ^3.5.2 - version: 3.5.2(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)(viem@2.22.21(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)) + specifier: ^3.5.3 + version: 3.5.3(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)(viem@2.22.22(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)) '@wagmi/connectors': - specifier: ^5.7.5 - version: 5.7.5(@wagmi/core@2.16.3(typescript@5.7.3)(use-sync-external-store@1.2.0)(viem@2.22.21(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)(viem@2.22.21(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)) + specifier: ^5.7.6 + version: 5.7.6(@wagmi/core@2.16.3(typescript@5.7.3)(use-sync-external-store@1.2.0)(viem@2.22.22(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)(viem@2.22.22(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)) '@wagmi/core': specifier: ^2.16.3 - version: 2.16.3(typescript@5.7.3)(use-sync-external-store@1.2.0)(viem@2.22.21(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)) + version: 2.16.3(typescript@5.7.3)(use-sync-external-store@1.2.0)(viem@2.22.22(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)) dotenv: specifier: ^16.4.7 version: 16.4.7 viem: - specifier: ^2.22.21 - version: 2.22.21(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10) + specifier: ^2.22.22 + version: 2.22.22(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10) devDependencies: '@types/node': specifier: ^22.13.1 @@ -221,16 +221,19 @@ packages: resolution: {integrity: sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==} engines: {node: '>=14'} - '@lifi/data-types@5.23.0': - resolution: {integrity: sha512-I+PXWcaKOleeQQj/ezoqk5EmbJe8lM6eJ/+EDPuXgYqA4SQnMjMOQsJmkC6/3xQyiZc92vNORUqtenPwgFrx6g==} + '@lifi/data-types@5.24.0': + resolution: {integrity: sha512-JTnsNMQdHBirPROhUp91EvyXAXUIzKaktCngHZoKeZpnQjC1LLQih1yQDnOJ/+zkFHIDa/koqStgCRTQ7jOung==} - '@lifi/sdk@3.5.2': - resolution: {integrity: sha512-EUnGcFclymkateB+dHK8LLhe1TvHXldJQhoIvfGSmMOkqlOR6vwFf+ULPFIxNyDubdTwql218j5ajaTfn28fcg==} + '@lifi/sdk@3.5.3': + resolution: {integrity: sha512-KlU5sMp8aEi+ZYdGZMEn7Let4UlIpuVHsqV3+cmeqLwzMDb1Ty/TWSs+6par69WftD1UBNEWH2czi+r5Am2qCg==} peerDependencies: viem: ^2.21.0 - '@lifi/types@16.6.0': - resolution: {integrity: sha512-7X0M+O9EvXepsODkvUS5f31CWskNcv8NeG1G0RRTctGe3p79cRPKfVmYTJOkbKibX00nhdzMBsVyafzedazsnA==} + '@lifi/types@16.7.0': + resolution: {integrity: sha512-t5By15YOMAndL8VKa0mE51nI5F1O5KEx5mS4yRjnipq+lfJi8XjkkktacLJOXZv7cnIubtBvCeq1TatqZ8QTlQ==} + + '@lifi/types@16.8.0': + resolution: {integrity: sha512-PBu5TDo1hFJEjBgnSZfQq/2g2BJo9Z181EVoKCgWLQxB/r7AuDjF5xTFyX776fsosaQNGPwooSGYEWJzeqUmew==} '@lit-labs/ssr-dom-shim@1.3.0': resolution: {integrity: sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==} @@ -276,8 +279,8 @@ packages: resolution: {integrity: sha512-5yb2gMI1BDm0JybZezeoX/3XhPDOtTbcFvpTXM9kxsoZjPZFh4XciqRbpD6N86HYZqWDhEaKUDuOyR0sQHEjMA==} engines: {node: '>=12.0.0'} - '@metamask/sdk-communication-layer@0.31.0': - resolution: {integrity: sha512-V9CxdzabDPjQVgmKGHsyU3SYt4Af27g+4DbGCx0fLoHqN/i1RBDZqs/LYbJX3ykJCANzE+llz/MolMCMrzM2RA==} + '@metamask/sdk-communication-layer@0.32.0': + resolution: {integrity: sha512-dmj/KFjMi1fsdZGIOtbhxdg3amxhKL/A5BqSU4uh/SyDKPub/OT+x5pX8bGjpTL1WPWY/Q0OIlvFyX3VWnT06Q==} peerDependencies: cross-fetch: ^4.0.0 eciesjs: '*' @@ -285,11 +288,11 @@ packages: readable-stream: ^3.6.2 socket.io-client: ^4.5.1 - '@metamask/sdk-install-modal-web@0.31.5': - resolution: {integrity: sha512-ZfrVkPAabfH4AIxcTlxQN5oyyzzVXFTLZrm1/BJ+X632d9MiyAVHNtiqa9EZpZYkZGk2icmDVP+xCpvJmVOVpQ==} + '@metamask/sdk-install-modal-web@0.32.0': + resolution: {integrity: sha512-TFoktj0JgfWnQaL3yFkApqNwcaqJ+dw4xcnrJueMP3aXkSNev2Ido+WVNOg4IIMxnmOrfAC9t0UJ0u/dC9MjOQ==} - '@metamask/sdk@0.31.5': - resolution: {integrity: sha512-i7wteqO/fU2JWQrMZz+addHokYThHYznp4nYXviv+QysdxGVgAYvcW/PBA+wpeP3veX7QGfNqMPgSsZbBrASYw==} + '@metamask/sdk@0.32.0': + resolution: {integrity: sha512-WmGAlP1oBuD9hk4CsdlG1WJFuPtYJY+dnTHJMeCyohTWD2GgkcLMUUuvu9lO1/NVzuOoSi1OrnjbuY1O/1NZ1g==} '@metamask/superstruct@3.1.0': resolution: {integrity: sha512-N08M56HdOgBfRKkrgCMZvQppkZGcArEop3kixNEtVbJKm6P9Cfg0YkI6X0s1g78sNrj2fWUwvJADdZuzJgFttA==} @@ -486,8 +489,8 @@ packages: '@types/ws@8.5.14': resolution: {integrity: sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==} - '@wagmi/connectors@5.7.5': - resolution: {integrity: sha512-btqHHUSTzg4BZe9at/7SnRPv4cz8O3pisbeZBh0qxKz7PVm+9vRxY0bSala3xQPDcS0PRTB30Vn/+lM73GCjbw==} + '@wagmi/connectors@5.7.6': + resolution: {integrity: sha512-Kl6UbvPlWJY/NgWj7M1869Wdx0kLQxZrC0Hv1Lc+RhgiHspHvhpTW/+0OGJOAEQm3XtN20P98n3KcdglvB+19A==} peerDependencies: '@wagmi/core': 2.16.3 typescript: '>=5.0.4' @@ -1552,8 +1555,8 @@ packages: varuint-bitcoin@2.0.0: resolution: {integrity: sha512-6QZbU/rHO2ZQYpWFDALCDSRsXbAs1VOEmXAxtbtjLtKuMJ/FQ8YbhfxlaiKv5nklci0M6lZtlZyxo9Q+qNnyog==} - viem@2.22.21: - resolution: {integrity: sha512-CujapStF+F3VP+bKBQOGFk5YHyJKZOY2TGvD1e04CAm8VrtLo3sfTydYW2Rri6LMktqp6ilGB9GvSiZczxvOBQ==} + viem@2.22.22: + resolution: {integrity: sha512-0z7EFheP+paC/KRlVpu7zXYiqkTX6GR86G0p84LnYr5NgaVxGz0mGsiERy41ThERX1ahkTdEWGGiNgfi6wVqBQ==} peerDependencies: typescript: '>=5.0.4' peerDependenciesMeta: @@ -1670,7 +1673,7 @@ snapshots: bech32: 2.0.0 bitcoinjs-lib: 7.0.0-rc.0(typescript@5.7.3) bs58: 6.0.0 - viem: 2.22.21(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10) + viem: 2.22.22(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - typescript @@ -1794,21 +1797,21 @@ snapshots: ethereum-cryptography: 2.2.1 micro-ftch: 0.3.1 - '@lifi/data-types@5.23.0': + '@lifi/data-types@5.24.0': dependencies: - '@lifi/types': 16.6.0 + '@lifi/types': 16.8.0 - '@lifi/sdk@3.5.2(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)(viem@2.22.21(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10))': + '@lifi/sdk@3.5.3(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)(viem@2.22.22(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10))': dependencies: '@bigmi/core': 0.1.1(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10) - '@lifi/types': 16.6.0 + '@lifi/types': 16.7.0 '@noble/curves': 1.8.1 '@solana/wallet-adapter-base': 0.9.23(@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/web3.js': 1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) bech32: 2.0.0 bitcoinjs-lib: 7.0.0-rc.0(typescript@5.7.3) bs58: 6.0.0 - viem: 2.22.21(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10) + viem: 2.22.22(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - encoding @@ -1816,7 +1819,9 @@ snapshots: - utf-8-validate - zod - '@lifi/types@16.6.0': {} + '@lifi/types@16.7.0': {} + + '@lifi/types@16.8.0': {} '@lit-labs/ssr-dom-shim@1.3.0': {} @@ -1894,7 +1899,7 @@ snapshots: '@metamask/safe-event-emitter@3.1.2': {} - '@metamask/sdk-communication-layer@0.31.0(cross-fetch@4.1.0)(eciesjs@0.4.13)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@metamask/sdk-communication-layer@0.32.0(cross-fetch@4.1.0)(eciesjs@0.4.13)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: bufferutil: 4.0.9 cross-fetch: 4.1.0 @@ -1909,17 +1914,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@metamask/sdk-install-modal-web@0.31.5': + '@metamask/sdk-install-modal-web@0.32.0': dependencies: '@paulmillr/qr': 0.2.1 - '@metamask/sdk@0.31.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@metamask/sdk@0.32.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@babel/runtime': 7.26.7 '@metamask/onboarding': 1.0.1 '@metamask/providers': 16.1.0 - '@metamask/sdk-communication-layer': 0.31.0(cross-fetch@4.1.0)(eciesjs@0.4.13)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@metamask/sdk-install-modal-web': 0.31.5 + '@metamask/sdk-communication-layer': 0.32.0(cross-fetch@4.1.0)(eciesjs@0.4.13)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@metamask/sdk-install-modal-web': 0.32.0 '@paulmillr/qr': 0.2.1 bowser: 2.11.0 cross-fetch: 4.1.0 @@ -2054,7 +2059,7 @@ snapshots: '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)': dependencies: '@safe-global/safe-gateway-typescript-sdk': 3.22.9 - viem: 2.22.21(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10) + viem: 2.22.22(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - typescript @@ -2242,16 +2247,16 @@ snapshots: dependencies: '@types/node': 22.13.1 - '@wagmi/connectors@5.7.5(@wagmi/core@2.16.3(typescript@5.7.3)(use-sync-external-store@1.2.0)(viem@2.22.21(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)(viem@2.22.21(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10))': + '@wagmi/connectors@5.7.6(@wagmi/core@2.16.3(typescript@5.7.3)(use-sync-external-store@1.2.0)(viem@2.22.22(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)(viem@2.22.22(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10))': dependencies: '@coinbase/wallet-sdk': 4.2.3 - '@metamask/sdk': 0.31.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@metamask/sdk': 0.32.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@safe-global/safe-apps-provider': 0.18.5(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10) - '@wagmi/core': 2.16.3(typescript@5.7.3)(use-sync-external-store@1.2.0)(viem@2.22.21(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)) + '@wagmi/core': 2.16.3(typescript@5.7.3)(use-sync-external-store@1.2.0)(viem@2.22.22(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)) '@walletconnect/ethereum-provider': 2.17.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - viem: 2.22.21(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10) + viem: 2.22.22(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.7.3 transitivePeerDependencies: @@ -2281,11 +2286,11 @@ snapshots: - utf-8-validate - zod - '@wagmi/core@2.16.3(typescript@5.7.3)(use-sync-external-store@1.2.0)(viem@2.22.21(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10))': + '@wagmi/core@2.16.3(typescript@5.7.3)(use-sync-external-store@1.2.0)(viem@2.22.22(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.7.3) - viem: 2.22.21(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10) + viem: 2.22.22(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10) zustand: 5.0.0(use-sync-external-store@1.2.0) optionalDependencies: typescript: 5.7.3 @@ -3599,7 +3604,7 @@ snapshots: dependencies: uint8array-tools: 0.0.8 - viem@2.22.21(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10): + viem@2.22.22(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10): dependencies: '@noble/curves': 1.8.1 '@noble/hashes': 1.7.1 diff --git a/package.json b/package.json index 9a2c1832..3780a1ee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lifi/sdk", - "version": "3.5.3", + "version": "3.6.0-beta.5", "description": "LI.FI Any-to-Any Cross-Chain-Swap SDK", "keywords": [ "bridge", @@ -93,14 +93,14 @@ }, "dependencies": { "@bigmi/core": "^0.1.1", - "@lifi/types": "^16.6.0", + "@lifi/types": "^16.8.0", "@noble/curves": "^1.8.1", "@solana/wallet-adapter-base": "^0.9.23", "@solana/web3.js": "^1.98.0", "bech32": "^2.0.0", "bitcoinjs-lib": "^7.0.0-rc.0", "bs58": "^6.0.0", - "viem": "^2.22.21" + "viem": "^2.22.22" }, "devDependencies": { "@biomejs/biome": "^1.9.4", @@ -133,5 +133,5 @@ "postbump": "node scripts/version.js && git add ." } }, - "packageManager": "pnpm@10.2.0" + "packageManager": "pnpm@9.15.5" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dadad673..d085c37b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^0.1.1 version: 0.1.1(bs58@6.0.0)(bufferutil@4.0.8)(typescript@5.7.3)(utf-8-validate@5.0.10) '@lifi/types': - specifier: ^16.6.0 - version: 16.6.0 + specifier: ^16.8.0 + version: 16.8.0 '@noble/curves': specifier: ^1.8.1 version: 1.8.1 @@ -33,8 +33,8 @@ importers: specifier: ^6.0.0 version: 6.0.0 viem: - specifier: ^2.22.21 - version: 2.22.21(bufferutil@4.0.8)(typescript@5.7.3)(utf-8-validate@5.0.10) + specifier: ^2.22.22 + version: 2.22.22(bufferutil@4.0.8)(typescript@5.7.3)(utf-8-validate@5.0.10) devDependencies: '@biomejs/biome': specifier: ^1.9.4 @@ -487,8 +487,8 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@lifi/types@16.6.0': - resolution: {integrity: sha512-7X0M+O9EvXepsODkvUS5f31CWskNcv8NeG1G0RRTctGe3p79cRPKfVmYTJOkbKibX00nhdzMBsVyafzedazsnA==} + '@lifi/types@16.8.0': + resolution: {integrity: sha512-PBu5TDo1hFJEjBgnSZfQq/2g2BJo9Z181EVoKCgWLQxB/r7AuDjF5xTFyX776fsosaQNGPwooSGYEWJzeqUmew==} '@mswjs/interceptors@0.37.3': resolution: {integrity: sha512-USvgCL/uOGFtVa6SVyRrC8kIAedzRohxIXN5LISlg5C5vLZCn7dgMFVSNhSF9cuBEFrm/O2spDWEZeMnw4ZXYg==} @@ -2742,8 +2742,8 @@ packages: varuint-bitcoin@2.0.0: resolution: {integrity: sha512-6QZbU/rHO2ZQYpWFDALCDSRsXbAs1VOEmXAxtbtjLtKuMJ/FQ8YbhfxlaiKv5nklci0M6lZtlZyxo9Q+qNnyog==} - viem@2.22.21: - resolution: {integrity: sha512-CujapStF+F3VP+bKBQOGFk5YHyJKZOY2TGvD1e04CAm8VrtLo3sfTydYW2Rri6LMktqp6ilGB9GvSiZczxvOBQ==} + viem@2.22.22: + resolution: {integrity: sha512-0z7EFheP+paC/KRlVpu7zXYiqkTX6GR86G0p84LnYr5NgaVxGz0mGsiERy41ThERX1ahkTdEWGGiNgfi6wVqBQ==} peerDependencies: typescript: '>=5.0.4' peerDependenciesMeta: @@ -2997,7 +2997,7 @@ snapshots: bech32: 2.0.0 bitcoinjs-lib: 7.0.0-rc.0(typescript@5.7.3) bs58: 6.0.0 - viem: 2.22.21(bufferutil@4.0.8)(typescript@5.7.3)(utf-8-validate@5.0.10) + viem: 2.22.22(bufferutil@4.0.8)(typescript@5.7.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - typescript @@ -3307,7 +3307,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@lifi/types@16.6.0': {} + '@lifi/types@16.8.0': {} '@mswjs/interceptors@0.37.3': dependencies: @@ -5594,7 +5594,7 @@ snapshots: dependencies: uint8array-tools: 0.0.8 - viem@2.22.21(bufferutil@4.0.8)(typescript@5.7.3)(utf-8-validate@5.0.10): + viem@2.22.22(bufferutil@4.0.8)(typescript@5.7.3)(utf-8-validate@5.0.10): dependencies: '@noble/curves': 1.8.1 '@noble/hashes': 1.7.1 diff --git a/src/config.ts b/src/config.ts index 28a0c0ac..9c322fc1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,4 +1,4 @@ -import { ChainId, type ExtendedChain } from '@lifi/types' +import { ChainId, type ChainType, type ExtendedChain } from '@lifi/types' import type { SDKProvider } from './core/types.js' import type { RPCUrls, SDKBaseConfig, SDKConfig } from './types/internal.js' @@ -34,6 +34,9 @@ export const config = (() => { } return _config }, + getProvider(type: ChainType) { + return _config.providers.find((provider) => provider.type === type) + }, setProviders(providers: SDKProvider[]) { const providerMap = new Map( _config.providers.map((provider) => [provider.type, provider]) diff --git a/src/constants.ts b/src/constants.ts index 703a1f7d..7d2f6607 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,10 @@ export const AddressZero = '0x0000000000000000000000000000000000000000' export const AlternativeAddressZero = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' -export const wrappedSolAddress = 'So11111111111111111111111111111111111111112' + export const SolSystemProgram = '11111111111111111111111111111111' + +export const MaxUint48 = BigInt('0xffffffffffff') +export const MaxUint160 = BigInt('0xffffffffffffffffffffffffffffffffffffffff') +export const MaxUint256 = + BigInt(0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn) diff --git a/src/core/EVM/EVM.ts b/src/core/EVM/EVM.ts index 7f77dd54..1ad60796 100644 --- a/src/core/EVM/EVM.ts +++ b/src/core/EVM/EVM.ts @@ -12,12 +12,13 @@ export function EVM(options?: EVMProviderOptions): EVMProvider { get type() { return ChainType.EVM }, - get multisig() { - return _options.multisig + get options() { + return _options }, isAddress, resolveAddress: getENSAddress, getBalance: getEVMBalance, + getWalletClient: _options.getWalletClient, async getStepExecutor( options: StepExecutorOptions ): Promise { @@ -29,7 +30,6 @@ export function EVM(options?: EVMProviderOptions): EVMProvider { const executor = new EVMStepExecutor({ client: walletClient, - multisig: _options.multisig, routeId: options.routeId, executionOptions: { ...options.executionOptions, diff --git a/src/core/EVM/EVMStepExecutor.ts b/src/core/EVM/EVMStepExecutor.ts index 93751742..93046253 100644 --- a/src/core/EVM/EVMStepExecutor.ts +++ b/src/core/EVM/EVMStepExecutor.ts @@ -1,54 +1,70 @@ -import type { Process } from '@lifi/types' +import type { ExtendedChain, LiFiStep } from '@lifi/types' import type { + Address, Client, GetAddressesReturnType, Hash, + Hex, SendTransactionParameters, + TransactionReceipt, } from 'viem' -import { getAddresses, sendTransaction } from 'viem/actions' +import { estimateGas, getAddresses, sendTransaction } from 'viem/actions' +import type { GetCapabilitiesReturnType } from 'viem/experimental' +import { getCapabilities, sendCalls } from 'viem/experimental' import { getAction } from 'viem/utils' import { config } from '../../config.js' import { LiFiErrorCode } from '../../errors/constants.js' -import { TransactionError, ValidationError } from '../../errors/errors.js' -import { getStepTransaction } from '../../services/api.js' -import { isZeroAddress } from '../../utils/isZeroAddress.js' +import { TransactionError } from '../../errors/errors.js' +import { + getRelayerQuote, + getStepTransaction, + relayTransaction, +} from '../../services/api.js' import { BaseStepExecutor } from '../BaseStepExecutor.js' import { checkBalance } from '../checkBalance.js' import { stepComparison } from '../stepComparison.js' import type { LiFiStepExtended, + Process, StepExecutorOptions, TransactionParameters, } from '../types.js' import { waitForDestinationChainTransaction } from '../waitForDestinationChainTransaction.js' import { checkAllowance } from './checkAllowance.js' -import { updateMultisigRouteProcess } from './multisig.js' +import { getNativePermit } from './getNativePermit.js' import { parseEVMErrors } from './parseEVMErrors.js' +import { type PermitSignature, signPermitMessage } from './signPermitMessage.js' import { switchChain } from './switchChain.js' -import type { MultisigConfig, MultisigTransaction } from './types.js' -import { getMaxPriorityFeePerGas } from './utils.js' +import { isRelayerStep } from './typeguards.js' +import { convertExtendedChain, getMaxPriorityFeePerGas } from './utils.js' +import { + type WalletCallReceipt, + waitForBatchTransactionReceipt, +} from './waitForBatchTransactionReceipt.js' +import { waitForRelayedTransactionReceipt } from './waitForRelayedTransactionReceipt.js' import { waitForTransactionReceipt } from './waitForTransactionReceipt.js' +export type Call = { + data?: Hex + to?: Address + value?: bigint + chainId?: number +} + export interface EVMStepExecutorOptions extends StepExecutorOptions { client: Client - multisig?: MultisigConfig } export class EVMStepExecutor extends BaseStepExecutor { private client: Client - private multisig?: MultisigConfig constructor(options: EVMStepExecutorOptions) { super(options) this.client = options.client - this.multisig = options.multisig } // Ensure that we are using the right chain and wallet when executing transactions. - checkClient = async ( - step: LiFiStepExtended, - process?: Process - ): Promise => { + checkClient = async (step: LiFiStepExtended, process?: Process) => { const updatedClient = await switchChain( this.client, this.statusManager, @@ -102,6 +118,78 @@ export class EVMStepExecutor extends BaseStepExecutor { return updatedClient } + waitForTransaction = async ({ + step, + process, + fromChain, + toChain, + atomicBatchSupported, + isRelayerTransaction, + txHash, + isBridgeExecution, + }: { + step: LiFiStepExtended + process: Process + fromChain: ExtendedChain + toChain: ExtendedChain + atomicBatchSupported: boolean + isRelayerTransaction: boolean + txHash: Hash + isBridgeExecution: boolean + }) => { + let transactionReceipt: TransactionReceipt | WalletCallReceipt | undefined + + if (atomicBatchSupported) { + transactionReceipt = await waitForBatchTransactionReceipt( + this.client, + txHash + ) + } else if (isRelayerTransaction) { + transactionReceipt = await waitForRelayedTransactionReceipt(txHash) + } else { + transactionReceipt = await waitForTransactionReceipt({ + client: this.client, + chainId: fromChain.id, + txHash, + onReplaced: (response) => { + this.statusManager.updateProcess(step, process.type, 'PENDING', { + txHash: response.transaction.hash, + txLink: `${fromChain.metamask.blockExplorerUrls[0]}tx/${response.transaction.hash}`, + }) + }, + }) + } + + // Update pending process if the transaction hash from the receipt is different. + // This might happen if the transaction was replaced. + if ( + transactionReceipt?.transactionHash && + transactionReceipt.transactionHash !== txHash + ) { + process = this.statusManager.updateProcess( + step, + process.type, + 'PENDING', + { + txHash: transactionReceipt.transactionHash, + txLink: `${fromChain.metamask.blockExplorerUrls[0]}tx/${transactionReceipt.transactionHash}`, + } + ) + } + + if (isBridgeExecution) { + process = this.statusManager.updateProcess(step, process.type, 'DONE') + } + + await waitForDestinationChainTransaction( + step, + process, + fromChain, + toChain, + this.statusManager + ) + } + executeStep = async (step: LiFiStepExtended): Promise => { step.execution = this.statusManager.initExecutionObject(step) @@ -121,324 +209,390 @@ export class EVMStepExecutor extends BaseStepExecutor { } } - const isMultisigClient = !!this.multisig?.isMultisigWalletClient - const multisigBatchTransactions: MultisigTransaction[] = [] - - const shouldBatchTransactions = - this.multisig?.shouldBatchTransactions && - !!this.multisig.sendBatchTransaction - const fromChain = await config.getChainById(step.action.fromChainId) const toChain = await config.getChainById(step.action.toChainId) + // Check if the wallet supports atomic batch transactions (EIP-5792) + const calls: Call[] = [] + let atomicBatchSupported = false + try { + const capabilities = (await getAction( + this.client, + getCapabilities, + 'getCapabilities' + )(undefined)) as GetCapabilitiesReturnType + atomicBatchSupported = capabilities[fromChain.id]?.atomicBatch?.supported + } catch { + // If the wallet does not support getCapabilities or the call fails, + // we assume that atomic batch is not supported + } + const isBridgeExecution = fromChain.id !== toChain.id const currentProcessType = isBridgeExecution ? 'CROSS_CHAIN' : 'SWAP' - // Check allowance + // Find existing swap/bridge process const existingProcess = step.execution.process.find( (p) => p.type === currentProcessType ) - // Check token approval only if fromToken is not the native token => no approval needed in that case + const isFromNativeToken = + fromChain.nativeToken.address === step.action.fromToken.address + + // Check if step requires permit signature and will be used with relayer service + const isRelayerTransaction = isRelayerStep(step) + + // Check if token requires approval + // Native tokens (like ETH) don't need approval since they're not ERC20 tokens + // We should support different permit types: + // 1. Native permits (EIP-2612) + // 2. Permit2 - Universal permit implementation by Uniswap (limited to certain chains) + // 3. Standard ERC20 approval + const nativePermit = await getNativePermit( + this.client, + fromChain, + step.action.fromToken.address as Address + ) + // Check if proxy contract is available and token supports native permits, not available for atomic batch + const nativePermitSupported = + nativePermit.supported && + !!fromChain.permit2Proxy && + !atomicBatchSupported && + !isRelayerTransaction && // TODO: remove once we support ERC-2771 + !isFromNativeToken + // Check if chain has Permit2 contract deployed. Permit2 should not be available for atomic batch. + const permit2Supported = + !!fromChain.permit2 && + !!fromChain.permit2Proxy && + !atomicBatchSupported && + !isFromNativeToken + // Token supports either native permits or Permit2 + const permitSupported = permit2Supported || nativePermitSupported + const checkForAllowance = + // No existing swap/bridgetransaction is pending !existingProcess?.txHash && - !isZeroAddress(step.action.fromToken.address) && - (shouldBatchTransactions || !isMultisigClient) + // Token is not native (address is not zero) + !isFromNativeToken && + // Token doesn't support native permits + !nativePermitSupported if (checkForAllowance) { - const data = await checkAllowance( - this.client, - fromChain, + // Check if token needs approval and get approval transaction or message data when available + const data = await checkAllowance({ + client: this.client, + chain: fromChain, step, - this.statusManager, - this.executionOptions, - this.allowUserInteraction, - shouldBatchTransactions - ) + statusManager: this.statusManager, + executionOptions: this.executionOptions, + allowUserInteraction: this.allowUserInteraction, + atomicBatchSupported, + permit2Supported, + }) if (data) { - // allowance doesn't need value - const baseTransaction: MultisigTransaction = { - to: step.action.fromToken.address, - data, + // Create approval transaction call + // No value needed since we're only approving ERC20 tokens + if (atomicBatchSupported) { + calls.push({ + chainId: step.action.fromToken.chainId, + to: step.action.fromToken.address as Address, + data, + }) } - - multisigBatchTransactions.push(baseTransaction) } } - let process = this.statusManager.findOrCreateProcess({ - step, - type: currentProcessType, - chainId: fromChain.id, - }) + let process = this.statusManager.findProcess(step, currentProcessType) - if (process.status !== 'DONE') { - const multisigProcess = step.execution.process.find( - (p) => !!p.multisigTxHash + if (process?.status === 'DONE') { + await waitForDestinationChainTransaction( + step, + process, + fromChain, + toChain, + this.statusManager ) - try { - if (isMultisigClient && multisigProcess) { - const multisigTxHash = multisigProcess.multisigTxHash as Hash - if (!multisigTxHash) { - throw new ValidationError( - 'Multisig internal transaction hash is undefined.' - ) - } - await updateMultisigRouteProcess( - multisigTxHash, - step, - process.type, - fromChain, - this.statusManager, - this.multisig - ) + return step + } + + try { + if (process?.txHash) { + // Make sure that the chain is still correct + const updatedClient = await this.checkClient(step, process) + if (!updatedClient) { + return step } - let txHash: Hash - if (process.txHash) { - // Make sure that the chain is still correct - const updatedClient = await this.checkClient(step, process) - if (!updatedClient) { - return step - } + // Wait for exiting transaction + const txHash = process.txHash as Hash - // Wait for exiting transaction - txHash = process.txHash as Hash - } else { - process = this.statusManager.updateProcess( - step, - process.type, - 'STARTED' - ) + await this.waitForTransaction({ + step, + process, + fromChain, + toChain, + atomicBatchSupported, + isRelayerTransaction, + txHash, + isBridgeExecution, + }) - // Check balance - await checkBalance(this.client.account!.address, step) - - // Create new transaction - if (!step.transactionRequest) { - const { execution, ...stepBase } = step - const updatedStep = await getStepTransaction(stepBase) - const comparedStep = await stepComparison( - this.statusManager, - step, - updatedStep, - this.allowUserInteraction, - this.executionOptions - ) - Object.assign(step, { - ...comparedStep, - execution: step.execution, - }) - } + return step + } - if (!step.transactionRequest) { - throw new TransactionError( - LiFiErrorCode.TransactionUnprepared, - 'Unable to prepare transaction.' - ) - } + process = this.statusManager.findOrCreateProcess({ + step, + type: permitSupported ? 'PERMIT' : currentProcessType, + status: 'STARTED', + }) - // Make sure that the chain is still correct - const updatedClient = await this.checkClient(step, process) - if (!updatedClient) { - return step + // Check balance + await checkBalance(this.client.account!.address, step) + + // Create new transaction request + if (!step.transactionRequest) { + const { execution, ...stepBase } = step + let updatedStep: LiFiStep + if (isRelayerTransaction) { + const updatedRelayedStep = await getRelayerQuote({ + fromChain: stepBase.action.fromChainId, + fromToken: stepBase.action.fromToken.address, + fromAddress: stepBase.action.fromAddress!, + fromAmount: stepBase.action.fromAmount, + toChain: stepBase.action.toChainId, + toToken: stepBase.action.toToken.address, + slippage: stepBase.action.slippage, + toAddress: stepBase.action.toAddress, + allowBridges: [stepBase.tool], + }) + updatedStep = { + ...updatedRelayedStep.data.quote.step, + ...updatedRelayedStep.data.quote, + id: stepBase.id, } + } else { + updatedStep = await getStepTransaction(stepBase) + } + const comparedStep = await stepComparison( + this.statusManager, + step, + updatedStep, + this.allowUserInteraction, + this.executionOptions + ) + Object.assign(step, { + ...comparedStep, + execution: step.execution, + }) + } - process = this.statusManager.updateProcess( - step, - process.type, - 'ACTION_REQUIRED' - ) - - if (!this.allowUserInteraction) { - return step - } + if (!step.transactionRequest) { + throw new TransactionError( + LiFiErrorCode.TransactionUnprepared, + 'Unable to prepare transaction.' + ) + } - let transactionRequest: TransactionParameters = { - to: step.transactionRequest.to, - from: step.transactionRequest.from, - data: step.transactionRequest.data, - value: step.transactionRequest.value - ? BigInt(step.transactionRequest.value) + let transactionRequest: TransactionParameters = { + to: step.transactionRequest.to, + from: step.transactionRequest.from, + data: step.transactionRequest.data, + value: step.transactionRequest.value + ? BigInt(step.transactionRequest.value) + : undefined, + gas: step.transactionRequest.gasLimit + ? BigInt(step.transactionRequest.gasLimit) + : undefined, + // gasPrice: step.transactionRequest.gasPrice + // ? BigInt(step.transactionRequest.gasPrice as string) + // : undefined, + // maxFeePerGas: step.transactionRequest.maxFeePerGas + // ? BigInt(step.transactionRequest.maxFeePerGas as string) + // : undefined, + maxPriorityFeePerGas: + this.client.account?.type === 'local' + ? await getMaxPriorityFeePerGas(this.client) + : step.transactionRequest.maxPriorityFeePerGas + ? BigInt(step.transactionRequest.maxPriorityFeePerGas) : undefined, - gas: step.transactionRequest.gasLimit - ? BigInt(step.transactionRequest.gasLimit) - : undefined, - // gasPrice: step.transactionRequest.gasPrice - // ? BigInt(step.transactionRequest.gasPrice as string) - // : undefined, - // maxFeePerGas: step.transactionRequest.maxFeePerGas - // ? BigInt(step.transactionRequest.maxFeePerGas as string) - // : undefined, - maxPriorityFeePerGas: - this.client.account?.type === 'local' - ? await getMaxPriorityFeePerGas(this.client) - : step.transactionRequest.maxPriorityFeePerGas - ? BigInt(step.transactionRequest.maxPriorityFeePerGas) - : undefined, - } + } - if (this.executionOptions?.updateTransactionRequestHook) { - const customizedTransactionRequest: TransactionParameters = - await this.executionOptions.updateTransactionRequestHook({ - requestType: 'transaction', - ...transactionRequest, - }) + if (this.executionOptions?.updateTransactionRequestHook) { + const customizedTransactionRequest: TransactionParameters = + await this.executionOptions.updateTransactionRequestHook({ + requestType: 'transaction', + ...transactionRequest, + }) - transactionRequest = { - ...transactionRequest, - ...customizedTransactionRequest, - } - } + transactionRequest = { + ...transactionRequest, + ...customizedTransactionRequest, + } + } - if (shouldBatchTransactions && this.multisig?.sendBatchTransaction) { - if (transactionRequest.to && transactionRequest.data) { - const populatedTransaction: MultisigTransaction = { - value: transactionRequest.value, - to: transactionRequest.to, - data: transactionRequest.data, - } - multisigBatchTransactions.push(populatedTransaction) - - txHash = await this.multisig?.sendBatchTransaction( - multisigBatchTransactions - ) - } else { - throw new TransactionError( - LiFiErrorCode.TransactionUnprepared, - 'Unable to prepare transaction.' - ) - } - } else { - txHash = await getAction( - this.client, - sendTransaction, - 'sendTransaction' - )({ - to: transactionRequest.to, - account: this.client.account!, - data: transactionRequest.data, - value: transactionRequest.value, - gas: transactionRequest.gas, - gasPrice: transactionRequest.gasPrice, - maxFeePerGas: transactionRequest.maxFeePerGas, - maxPriorityFeePerGas: transactionRequest.maxPriorityFeePerGas, - chain: null, - } as SendTransactionParameters) - } + // Make sure that the chain is still correct + const updatedClient = await this.checkClient(step, process) + if (!updatedClient) { + return step + } - if (isMultisigClient) { - process = this.statusManager.updateProcess( - step, - process.type, - 'ACTION_REQUIRED', - { - multisigTxHash: txHash, - } - ) - } else { - process = this.statusManager.updateProcess( - step, - process.type, - 'PENDING', - { - txHash: txHash, - txLink: `${fromChain.metamask.blockExplorerUrls[0]}tx/${txHash}`, - } - ) - } - } + process = this.statusManager.updateProcess( + step, + process.type, + 'ACTION_REQUIRED' + ) - const transactionReceipt = await waitForTransactionReceipt({ - client: this.client, - chainId: fromChain.id, - txHash, - onReplaced: (response) => { - this.statusManager.updateProcess(step, process.type, 'PENDING', { - txHash: response.transaction.hash, - txLink: `${fromChain.metamask.blockExplorerUrls[0]}tx/${response.transaction.hash}`, - }) - }, - }) + if (!this.allowUserInteraction) { + return step + } - // if it's multisig wallet client and the process is in ACTION_REQUIRED - // then signatures are still needed - if (isMultisigClient && process.status === 'ACTION_REQUIRED') { - await updateMultisigRouteProcess( - transactionReceipt?.transactionHash || txHash, - step, - process.type, - fromChain, - this.statusManager, - this.multisig - ) + let txHash: Hash + let isTransactionRelayed = false + + if (atomicBatchSupported) { + const transferCall: Call = { + chainId: fromChain.id, + data: transactionRequest.data as Hex, + to: transactionRequest.to as Address, + value: transactionRequest.value, } - // Update pending process if the transaction hash from the receipt is different. - // This might happen if the transaction was replaced. - if ( - !isMultisigClient && - transactionReceipt?.transactionHash && - transactionReceipt.transactionHash !== txHash - ) { + calls.push(transferCall) + + txHash = (await getAction( + this.client, + sendCalls, + 'sendCalls' + )({ + account: this.client.account!, + calls, + })) as Address + } else { + let permitSignature: PermitSignature | undefined + if (permitSupported) { + permitSignature = await signPermitMessage(this.client, { + transactionRequest, + chain: fromChain, + tokenAddress: step.action.fromToken.address as Address, + amount: BigInt(step.action.fromAmount), + nativePermit, + permitData: isRelayerTransaction ? step.permitData : undefined, + useWitness: isRelayerTransaction, + }) + transactionRequest.to = fromChain.permit2Proxy + this.statusManager.updateProcess(step, process.type, 'DONE') + } + if (isRelayerTransaction && permitSignature) { + process = this.statusManager.findOrCreateProcess({ + step, + type: currentProcessType, + status: 'PENDING', + }) + const relayedTransaction = await relayTransaction({ + tokenOwner: this.client.account!.address, + chainId: fromChain.id, + permit: step.permit, + witness: step.witness, + signedPermitData: permitSignature.signature, + callData: transactionRequest.data!, + }) + txHash = relayedTransaction.data.taskId + isTransactionRelayed = true + } else { + process = this.statusManager.findOrCreateProcess({ + step, + type: currentProcessType, + status: 'STARTED', + }) + if (permitSignature) { + // If we have a permit signature, we need to use updated data + transactionRequest.data = permitSignature.data + try { + // Try to re-estimate the gas due to additional Permit data + const estimatedGas = await estimateGas(this.client, { + account: this.client.account!, + to: transactionRequest.to as Address, + data: transactionRequest.data as Hex, + value: transactionRequest.value, + }) + transactionRequest.gas = + transactionRequest.gas && transactionRequest.gas > estimatedGas + ? transactionRequest.gas + : estimatedGas + } catch { + // Let the wallet estimate the gas in case of failure + transactionRequest.gas = undefined + } + } process = this.statusManager.updateProcess( step, process.type, - 'PENDING', - { - txHash: transactionReceipt.transactionHash, - txLink: `${fromChain.metamask.blockExplorerUrls[0]}tx/${transactionReceipt.transactionHash}`, - } + 'ACTION_REQUIRED' ) + txHash = await getAction( + this.client, + sendTransaction, + 'sendTransaction' + )({ + to: transactionRequest.to as Address, + account: this.client.account!, + data: transactionRequest.data as Hex, + value: transactionRequest.value, + gas: transactionRequest.gas, + gasPrice: transactionRequest.gasPrice, + maxFeePerGas: transactionRequest.maxFeePerGas, + maxPriorityFeePerGas: transactionRequest.maxPriorityFeePerGas, + chain: convertExtendedChain(fromChain), + } as SendTransactionParameters) } - - if (isBridgeExecution) { - process = this.statusManager.updateProcess(step, process.type, 'DONE') - } - } catch (e: any) { - const error = await parseEVMErrors(e, step, process) - process = this.statusManager.updateProcess( - step, - process.type, - 'FAILED', - { - error: { - message: error.cause.message, - code: error.code, - }, - } - ) - this.statusManager.updateExecution(step, 'FAILED') - - throw error } - } + process = this.statusManager.updateProcess( + step, + process.type, + 'PENDING', + // When atomic batch is supported, txHash represents the batch hash rather than an individual transaction hash at this point + atomicBatchSupported + ? { + atomicBatchSupported, + } + : isTransactionRelayed + ? undefined + : { + txHash: txHash, + txLink: `${fromChain.metamask.blockExplorerUrls[0]}tx/${txHash}`, + } + ) - // Wait for the transaction status on the destination chain - const transactionHash = process.txHash - if (!transactionHash) { - throw new Error('Transaction hash is undefined.') - } - if (isBridgeExecution) { - process = this.statusManager.findOrCreateProcess({ + await this.waitForTransaction({ step, - type: 'RECEIVING_CHAIN', - status: 'PENDING', - chainId: toChain.id, + process, + fromChain, + toChain, + atomicBatchSupported, + isRelayerTransaction, + txHash, + isBridgeExecution, }) - } - await waitForDestinationChainTransaction( - step, - process.type, - transactionHash, - toChain, - this.statusManager - ) + // DONE + return step + } catch (e: any) { + const error = await parseEVMErrors(e, step, process) + process = this.statusManager.updateProcess( + step, + process?.type || currentProcessType, + 'FAILED', + { + error: { + message: error.cause.message, + code: error.code, + }, + } + ) + this.statusManager.updateExecution(step, 'FAILED') - // DONE - return step + throw error + } } } diff --git a/src/core/EVM/abi.ts b/src/core/EVM/abi.ts index 840ae040..2a747eb9 100644 --- a/src/core/EVM/abi.ts +++ b/src/core/EVM/abi.ts @@ -1,47 +1,44 @@ -import type { Abi } from 'viem' +import { parseAbi } from 'viem' -export const approveAbi: Abi = [ - { - name: 'approve', - inputs: [ - { internalType: 'address', name: 'spender', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - outputs: [{ internalType: 'bool', name: 'approved', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', - }, -] +export const permit2ProxyAbi = parseAbi([ + 'function callDiamondWithPermit2(bytes, ((address, uint256), uint256, uint256), bytes) external', + 'function callDiamondWithEIP2612Signature(address, uint256, uint256, uint8, bytes32, bytes32, bytes) external payable', + 'function nextNonce(address) external view returns (uint256)', + 'function callDiamondWithPermit2Witness(bytes, address, ((address, uint256), uint256, uint256), bytes) external payable', +]) -export const allowanceAbi: Abi = [ - { - name: 'allowance', - inputs: [ - { internalType: 'address', name: 'owner', type: 'address' }, - { internalType: 'address', name: 'spender', type: 'address' }, - ], - outputs: [{ internalType: 'uint256', name: 'allowance', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, -] +export const eip2612Abi = parseAbi([ + 'function permit(address, address, uint256, uint256, uint8, bytes32, bytes32) external', + 'function DOMAIN_SEPARATOR() external view returns (bytes32)', + 'function nonces(address) external view returns (uint256)', + 'function name() external view returns (string)', + 'function version() external view returns (string)', +]) -export const getEthBalanceAbi: Abi = [ - { - inputs: [{ name: '_owner', type: 'address' }], - name: 'getEthBalance', - outputs: [{ name: 'balance', type: 'uint256' }], - type: 'function', - stateMutability: 'view', - }, -] +export const approveAbi = parseAbi([ + 'function approve(address, uint256) external returns (bool)', +]) -export const balanceOfAbi: Abi = [ - { - inputs: [{ name: '_owner', type: 'address' }], - name: 'balanceOf', - outputs: [{ name: 'balance', type: 'uint256' }], - type: 'function', - stateMutability: 'view', - }, -] +export const allowanceAbi = parseAbi([ + 'function allowance(address, address) external view returns (uint256)', +]) + +export const getEthBalanceAbi = parseAbi([ + 'function getEthBalance(address) external view returns (uint256)', +]) + +export const balanceOfAbi = parseAbi([ + 'function balanceOf(address) external view returns (uint256)', +]) + +// EIP-2612 types +// https://eips.ethereum.org/EIPS/eip-2612 +export const eip2612Types = { + Permit: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], +} as const diff --git a/src/core/EVM/checkAllowance.ts b/src/core/EVM/checkAllowance.ts index b1d4b665..8c4299d2 100644 --- a/src/core/EVM/checkAllowance.ts +++ b/src/core/EVM/checkAllowance.ts @@ -1,114 +1,114 @@ -import type { Chain, LiFiStep, Process, ProcessType } from '@lifi/types' +import type { ExtendedChain, LiFiStep } from '@lifi/types' import type { Address, Client, Hash } from 'viem' +import { MaxUint256 } from '../../constants.js' import type { StatusManager } from '../StatusManager.js' -import type { ExecutionOptions } from '../types.js' +import type { ExecutionOptions, Process, ProcessType } from '../types.js' import { getAllowance } from './getAllowance.js' import { parseEVMErrors } from './parseEVMErrors.js' import { setAllowance } from './setAllowance.js' import { waitForTransactionReceipt } from './waitForTransactionReceipt.js' -export const checkAllowance = async ( - client: Client, - chain: Chain, - step: LiFiStep, - statusManager: StatusManager, - settings?: ExecutionOptions, +export type CheckAllowanceParams = { + client: Client + chain: ExtendedChain + step: LiFiStep + statusManager: StatusManager + executionOptions?: ExecutionOptions + allowUserInteraction?: boolean + atomicBatchSupported?: boolean + permit2Supported?: boolean +} + +export const checkAllowance = async ({ + client, + chain, + step, + statusManager, + executionOptions, allowUserInteraction = false, - shouldBatchTransactions = false -): Promise => { - // Ask the user to set an allowance - let allowanceProcess: Process = statusManager.findOrCreateProcess({ + atomicBatchSupported = false, + permit2Supported = false, +}: CheckAllowanceParams): Promise => { + // Find existing or create new allowance process + const allowanceProcess: Process = statusManager.findOrCreateProcess({ step, type: 'TOKEN_ALLOWANCE', chainId: step.action.fromChainId, }) - // Check allowance try { + // Handle existing pending transaction if (allowanceProcess.txHash && allowanceProcess.status !== 'DONE') { await waitForApprovalTransaction( client, - allowanceProcess.txHash! as Address, + allowanceProcess.txHash as Address, allowanceProcess.type, step, chain, statusManager ) - } else { - allowanceProcess = statusManager.updateProcess( - step, - allowanceProcess.type, - 'STARTED' - ) + return + } - const approved = await getAllowance( - chain.id, - step.action.fromToken.address, - client.account!.address, - step.estimate.approvalAddress - ) + // Start new allowance check + statusManager.updateProcess(step, allowanceProcess.type, 'STARTED') - const fromAmount = BigInt(step.action.fromAmount) - - if (fromAmount > approved) { - if (!allowUserInteraction) { - return - } - - if (shouldBatchTransactions) { - const approveTxHash = await setAllowance( - client, - step.action.fromToken.address, - step.estimate.approvalAddress, - fromAmount, - settings, - true - ) - - allowanceProcess = statusManager.updateProcess( - step, - allowanceProcess.type, - 'DONE' - ) - - return approveTxHash - } - - const approveTxHash = await setAllowance( - client, - step.action.fromToken.address, - step.estimate.approvalAddress, - fromAmount - ) - await waitForApprovalTransaction( - client, - approveTxHash, - allowanceProcess.type, - step, - chain, - statusManager - ) - } else { - allowanceProcess = statusManager.updateProcess( - step, - allowanceProcess.type, - 'DONE' - ) - } + const spenderAddress = permit2Supported + ? chain.permit2 + : step.estimate.approvalAddress + const fromAmount = BigInt(step.action.fromAmount) + + const approved = await getAllowance( + chain.id, + step.action.fromToken.address as Address, + client.account!.address, + spenderAddress as Address + ) + + // Return early if already approved + if (fromAmount <= approved) { + statusManager.updateProcess(step, allowanceProcess.type, 'DONE') + return } - } catch (e: any) { - const error = await parseEVMErrors(e, step, allowanceProcess) - allowanceProcess = statusManager.updateProcess( - step, + + if (!allowUserInteraction) { + return + } + + statusManager.updateProcess(step, allowanceProcess.type, 'ACTION_REQUIRED') + + // Set new allowance + const approveAmount = permit2Supported ? MaxUint256 : fromAmount + const approveTxHash = await setAllowance( + client, + step.action.fromToken.address as Address, + spenderAddress as Address, + approveAmount, + executionOptions, + atomicBatchSupported + ) + + if (atomicBatchSupported) { + statusManager.updateProcess(step, allowanceProcess.type, 'DONE') + return approveTxHash + } + + await waitForApprovalTransaction( + client, + approveTxHash, allowanceProcess.type, - 'FAILED', - { - error: { - message: error.cause.message, - code: error.code, - }, - } + step, + chain, + statusManager ) + } catch (e: any) { + const error = await parseEVMErrors(e, step, allowanceProcess) + statusManager.updateProcess(step, allowanceProcess.type, 'FAILED', { + error: { + message: error.cause.message, + code: error.code, + }, + }) statusManager.updateExecution(step, 'FAILED') throw error } @@ -119,29 +119,33 @@ const waitForApprovalTransaction = async ( txHash: Hash, processType: ProcessType, step: LiFiStep, - chain: Chain, + chain: ExtendedChain, statusManager: StatusManager ) => { + const baseExplorerUrl = chain.metamask.blockExplorerUrls[0] + const getTxLink = (hash: Hash) => `${baseExplorerUrl}tx/${hash}` + statusManager.updateProcess(step, processType, 'PENDING', { txHash, - txLink: `${chain.metamask.blockExplorerUrls[0]}tx/${txHash}`, + txLink: getTxLink(txHash), }) const transactionReceipt = await waitForTransactionReceipt({ - client: client, + client, chainId: chain.id, - txHash: txHash, + txHash, onReplaced(response) { + const newHash = response.transaction.hash statusManager.updateProcess(step, processType, 'PENDING', { - txHash: response.transaction.hash, - txLink: `${chain.metamask.blockExplorerUrls[0]}tx/${response.transaction.hash}`, + txHash: newHash, + txLink: getTxLink(newHash), }) }, }) - const transactionHash = transactionReceipt?.transactionHash || txHash + const finalHash = transactionReceipt?.transactionHash || txHash statusManager.updateProcess(step, processType, 'DONE', { - txHash: transactionHash, - txLink: `${chain.metamask.blockExplorerUrls[0]}tx/${transactionHash}`, + txHash: finalHash, + txLink: getTxLink(finalHash), }) } diff --git a/src/core/EVM/checkPermitSupport.ts b/src/core/EVM/checkPermitSupport.ts new file mode 100644 index 00000000..22d3c7ca --- /dev/null +++ b/src/core/EVM/checkPermitSupport.ts @@ -0,0 +1,73 @@ +import type { ExtendedChain } from '@lifi/types' +import { ChainType } from '@lifi/types' +import type { Address } from 'viem' +import { config } from '../../config.js' +import { getAllowance } from './getAllowance.js' +import { type NativePermitData, getNativePermit } from './getNativePermit.js' +import { getPublicClient } from './publicClient.js' +import type { EVMProvider } from './types.js' + +export type PermitSupport = { + /** Whether the token supports EIP-2612 native permits */ + nativePermitSupported: boolean + /** Whether Permit2 is available and has sufficient allowance */ + permit2AllowanceSufficient: boolean +} + +/** + * Checks what permit types are supported for a token on a specific chain. + * Checks in order: + * 1. Native permit (EIP-2612) support + * 2. Permit2 availability and allowance + * + * @param chain - The chain to check permit support on + * @param tokenAddress - The token address to check + * @param ownerAddress - The address that would sign the permit + * @param amount - The amount to check allowance against for Permit2 + * @returns Object indicating which permit types are supported + */ +export const checkPermitSupport = async ({ + chain, + tokenAddress, + ownerAddress, + amount, +}: { + chain: ExtendedChain + tokenAddress: Address + ownerAddress: Address + amount: bigint +}): Promise => { + const provider = config.getProvider(ChainType.EVM) as EVMProvider | undefined + + let client = await provider?.getWalletClient?.() + + if (!client) { + client = await getPublicClient(chain.id) + } + + let nativePermit: NativePermitData + // Try with wallet client first, fallback to public client + try { + nativePermit = await getNativePermit(client, chain, tokenAddress) + } catch { + client = await getPublicClient(chain.id) + nativePermit = await getNativePermit(client, chain, tokenAddress) + } + + let permit2Allowance: bigint | undefined + // Check Permit2 allowance if available on chain + if (chain.permit2) { + permit2Allowance = await getAllowance( + chain.id, + tokenAddress, + ownerAddress, + chain.permit2 as Address + ) + } + + return { + nativePermitSupported: nativePermit.supported, + permit2AllowanceSufficient: + !!permit2Allowance && permit2Allowance >= amount, + } +} diff --git a/src/core/EVM/getAllowance.int.spec.ts b/src/core/EVM/getAllowance.int.spec.ts index 16ad4b5c..81471a1f 100644 --- a/src/core/EVM/getAllowance.int.spec.ts +++ b/src/core/EVM/getAllowance.int.spec.ts @@ -1,4 +1,5 @@ import { ChainId, CoinKey } from '@lifi/types' +import type { Address } from 'viem' import { beforeAll, describe, expect, it } from 'vitest' import { setupTestEnvironment } from '../../../tests/setup.js' import { findDefaultToken } from '../../../tests/tokens.js' @@ -27,58 +28,55 @@ const timeout = 10000 beforeAll(setupTestEnvironment) -describe('allowance integration tests', () => { - it( - 'should work for ERC20 on POL', - async () => { - const allowance = await getAllowance( - memeToken.chainId, - memeToken.address, - defaultWalletAddress, - defaultSpenderAddress - ) +describe('allowance integration tests', { retry: retryTimes, timeout }, () => { + it('should work for ERC20 on POL', async () => { + const allowance = await getAllowance( + memeToken.chainId, + memeToken.address as Address, + defaultWalletAddress, + defaultSpenderAddress + ) - expect(allowance).toBeGreaterThanOrEqual(defaultMemeAllowance) - }, - { retry: retryTimes, timeout } - ) + expect(allowance).toBeGreaterThanOrEqual(defaultMemeAllowance) + }) it( 'should work for MATIC on POL', + { retry: retryTimes, timeout }, async () => { const token = findDefaultToken(CoinKey.MATIC, ChainId.POL) const allowance = await getAllowance( token.chainId, - token.address, + token.address as Address, defaultWalletAddress, defaultSpenderAddress ) expect(allowance).toBe(0n) - }, - { retry: retryTimes, timeout } + } ) it( 'should return even with invalid data on POL', + { retry: retryTimes, timeout }, async () => { const invalidToken = findDefaultToken(CoinKey.MATIC, ChainId.POL) invalidToken.address = '0x2170ed0880ac9a755fd29b2688956bd959f933f8' const allowance = await getAllowance( invalidToken.chainId, - invalidToken.address, + invalidToken.address as Address, defaultWalletAddress, defaultSpenderAddress ) expect(allowance).toBe(0n) - }, - { retry: retryTimes, timeout } + } ) it( 'should handle empty lists with multicall', + { retry: retryTimes, timeout }, async () => { const allowances = await getAllowanceMulticall( 137, @@ -86,12 +84,12 @@ describe('allowance integration tests', () => { defaultWalletAddress ) expect(allowances.length).toBe(0) - }, - { retry: retryTimes, timeout } + } ) it( 'should handle token lists with more than 10 tokens', + { retry: retryTimes, timeout }, async () => { const { tokens } = await getTokens({ chains: [ChainId.POL], @@ -122,7 +120,6 @@ describe('allowance integration tests', () => { expect(token?.allowance).toBeGreaterThanOrEqual(defaultMemeAllowance) } - }, - { retry: retryTimes, timeout } + } ) }) diff --git a/src/core/EVM/getAllowance.ts b/src/core/EVM/getAllowance.ts index 630267d4..c0e2a7f7 100644 --- a/src/core/EVM/getAllowance.ts +++ b/src/core/EVM/getAllowance.ts @@ -13,9 +13,9 @@ import { getMulticallAddress } from './utils.js' export const getAllowance = async ( chainId: ChainId, - tokenAddress: string, - ownerAddress: string, - spenderAddress: string + tokenAddress: Address, + ownerAddress: Address, + spenderAddress: Address ): Promise => { const client = await getPublicClient(chainId) try { @@ -34,7 +34,7 @@ export const getAllowance = async ( export const getAllowanceMulticall = async ( chainId: ChainId, tokens: TokenSpender[], - ownerAddress: string + ownerAddress: Address ): Promise => { if (!tokens.length) { return [] @@ -80,8 +80,8 @@ export const getAllowanceMulticall = async ( */ export const getTokenAllowance = async ( token: BaseToken, - ownerAddress: string, - spenderAddress: string + ownerAddress: Address, + spenderAddress: Address ): Promise => { // native token don't need approval if (isNativeTokenAddress(token.address)) { @@ -90,7 +90,7 @@ export const getTokenAllowance = async ( const approved = await getAllowance( token.chainId, - token.address, + token.address as Address, ownerAddress, spenderAddress ) @@ -104,7 +104,7 @@ export const getTokenAllowance = async ( * @returns Returns array of tokens and their allowance */ export const getTokenAllowanceMulticall = async ( - ownerAddress: string, + ownerAddress: Address, tokens: TokenSpender[] ): Promise => { // filter out native tokens diff --git a/src/core/EVM/getEVMBalance.int.spec.ts b/src/core/EVM/getEVMBalance.int.spec.ts index 67e44585..78702e23 100644 --- a/src/core/EVM/getEVMBalance.int.spec.ts +++ b/src/core/EVM/getEVMBalance.int.spec.ts @@ -1,5 +1,6 @@ import type { StaticToken, Token } from '@lifi/types' import { ChainId, CoinKey } from '@lifi/types' +import type { Address } from 'viem' import { beforeAll, describe, expect, it } from 'vitest' import { setupTestEnvironment } from '../../../tests/setup.js' import { findDefaultToken } from '../../../tests/tokens.js' @@ -18,7 +19,10 @@ describe('getBalances integration tests', () => { walletAddress: string, tokens: StaticToken[] ) => { - const tokenBalances = await getEVMBalance(walletAddress, tokens as Token[]) + const tokenBalances = await getEVMBalance( + walletAddress as Address, + tokens as Token[] + ) expect(tokenBalances.length).toEqual(tokens.length) @@ -41,6 +45,7 @@ describe('getBalances integration tests', () => { it( 'should work for ERC20 on POL', + { retry: retryTimes, timeout }, async () => { const walletAddress = defaultWalletAddress const tokens = [ @@ -49,12 +54,12 @@ describe('getBalances integration tests', () => { ] await loadAndCompareTokenAmounts(walletAddress, tokens) - }, - { retry: retryTimes, timeout } + } ) it( 'should work for MATIC on POL', + { retry: retryTimes, timeout }, async () => { const walletAddress = defaultWalletAddress const tokens = [ @@ -63,12 +68,12 @@ describe('getBalances integration tests', () => { ] await loadAndCompareTokenAmounts(walletAddress, tokens) - }, - { retry: retryTimes, timeout } + } ) it( 'should return even with invalid data on POL', + { retry: retryTimes, timeout }, async () => { const walletAddress = defaultWalletAddress const invalidToken = findDefaultToken(CoinKey.MATIC, ChainId.POL) @@ -87,32 +92,28 @@ describe('getBalances integration tests', () => { ) expect(invalidBalance).toBeDefined() expect(invalidBalance!.amount).toBeUndefined() - }, - { retry: retryTimes, timeout } + } ) it( 'should fallback to a direct call if only one token is requested', + { retry: retryTimes, timeout }, async () => { const walletAddress = defaultWalletAddress const tokens = [findDefaultToken(CoinKey.DAI, ChainId.BSC)] await loadAndCompareTokenAmounts(walletAddress, tokens) - }, - { retry: retryTimes, timeout } + } ) - it( - 'should handle empty lists', - async () => { - const walletAddress = defaultWalletAddress - const tokens: Token[] = [] - await loadAndCompareTokenAmounts(walletAddress, tokens) - }, - { retry: retryTimes, timeout } - ) + it('should handle empty lists', { retry: retryTimes, timeout }, async () => { + const walletAddress = defaultWalletAddress + const tokens: Token[] = [] + await loadAndCompareTokenAmounts(walletAddress, tokens) + }) it( 'should handle token lists with more than 100 tokens', + { retry: retryTimes, timeout }, async () => { const walletAddress = defaultWalletAddress const { tokens } = await getTokens({ @@ -125,7 +126,6 @@ describe('getBalances integration tests', () => { tokens[ChainId.OPT].slice(0, 150) ) // chunk limit is 100 } - }, - { retry: retryTimes, timeout } + } ) }) diff --git a/src/core/EVM/getEVMBalance.ts b/src/core/EVM/getEVMBalance.ts index 14799664..b1caf64f 100644 --- a/src/core/EVM/getEVMBalance.ts +++ b/src/core/EVM/getEVMBalance.ts @@ -12,7 +12,7 @@ import { getPublicClient } from './publicClient.js' import { getMulticallAddress } from './utils.js' export const getEVMBalance = async ( - walletAddress: string, + walletAddress: Address, tokens: Token[] ): Promise => { if (tokens.length === 0) { @@ -85,7 +85,7 @@ const getEVMBalanceMulticall = async ( const getEVMBalanceDefault = async ( chainId: ChainId, tokens: Token[], - walletAddress: string + walletAddress: Address ): Promise => { const client = await getPublicClient(chainId) const blockNumber = await getBlockNumber(client) diff --git a/src/core/EVM/getNativePermit.ts b/src/core/EVM/getNativePermit.ts new file mode 100644 index 00000000..fd32a8d3 --- /dev/null +++ b/src/core/EVM/getNativePermit.ts @@ -0,0 +1,267 @@ +import type { ExtendedChain } from '@lifi/types' +import { + encodeAbiParameters, + keccak256, + pad, + parseAbiParameters, + toBytes, + toHex, +} from 'viem' +import type { Address, Client, Hex } from 'viem' +import type { TypedDataDomain } from 'viem' +import { multicall, readContract } from 'viem/actions' +import { eip2612Abi } from './abi.js' +import { getMulticallAddress } from './utils.js' + +export type NativePermitData = { + name: string + version: string + nonce: bigint + supported: boolean + domain: TypedDataDomain +} + +/** + * EIP-712 domain typehash with chainId + * @link https://eips.ethereum.org/EIPS/eip-712#specification + * + * keccak256(toBytes( + * 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' + * )) + */ +const EIP712_DOMAIN_TYPEHASH = + '0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f' as Hex + +/** + * EIP-712 domain typehash with salt (e.g. USDC.e on Polygon) + * @link https://eips.ethereum.org/EIPS/eip-712#specification + * + * keccak256(toBytes( + * 'EIP712Domain(string name,string version,address verifyingContract,bytes32 salt)' + * )) + */ +const EIP712_DOMAIN_TYPEHASH_WITH_SALT = + '0x36c25de3e541d5d970f66e4210d728721220fff5c077cc6cd008b3a0c62adab7' as Hex + +function makeDomainSeparator({ + name, + version, + chainId, + verifyingContract, + withSalt = false, +}: { + name: string + version: string + chainId: bigint + verifyingContract: Address + withSalt?: boolean +}): Hex { + const nameHash = keccak256(toBytes(name)) + const versionHash = keccak256(toBytes(version)) + + const encoded = withSalt + ? encodeAbiParameters( + parseAbiParameters('bytes32, bytes32, bytes32, address, bytes32'), + [ + EIP712_DOMAIN_TYPEHASH_WITH_SALT, + nameHash, + versionHash, + verifyingContract, + pad(toHex(chainId), { size: 32 }), + ] + ) + : encodeAbiParameters( + parseAbiParameters('bytes32, bytes32, bytes32, uint256, address'), + [ + EIP712_DOMAIN_TYPEHASH, + nameHash, + versionHash, + chainId, + verifyingContract, + ] + ) + + return keccak256(encoded) +} + +// TODO: Add support for EIP-5267 when adoption increases +// This EIP provides a standard way to query domain separator and permit type hash +// via eip712Domain() function, which would simplify permit validation +// https://eips.ethereum.org/EIPS/eip-5267 +function validateDomainSeparator({ + name, + version, + chainId, + verifyingContract, + domainSeparator, +}: { + name: string + version: string + chainId: bigint + verifyingContract: Address + domainSeparator: Hex +}): { isValid: boolean; domain: TypedDataDomain } { + if (!name || !domainSeparator) { + return { + isValid: false, + domain: {}, + } + } + + for (const withSalt of [false, true]) { + const computedDS = makeDomainSeparator({ + name, + version, + chainId, + verifyingContract, + withSalt, + }) + if (domainSeparator.toLowerCase() === computedDS.toLowerCase()) { + return { + isValid: true, + domain: withSalt + ? { + name, + version, + verifyingContract, + salt: pad(toHex(chainId), { size: 32 }), + } + : { + name, + version, + chainId, + verifyingContract, + }, + } + } + } + + return { + isValid: false, + domain: {}, + } +} + +const defaultPermit: NativePermitData = { + name: '', + version: '1', + nonce: 0n, + supported: false, + domain: {}, +} + +/** + * Retrieves native permit data (EIP-2612) for a token on a specific chain + * @link https://eips.ethereum.org/EIPS/eip-2612 + * @param client - The Viem client instance + * @param chain - The extended chain object containing chain details + * @param tokenAddress - The address of the token to check for permit support + * @returns {Promise} Object containing permit data including name, version, nonce and support status + */ +export const getNativePermit = async ( + client: Client, + chain: ExtendedChain, + tokenAddress: Address +): Promise => { + try { + const multicallAddress = await getMulticallAddress(chain.id) + + const contractCalls = [ + { + address: tokenAddress, + abi: eip2612Abi, + functionName: 'name', + }, + { + address: tokenAddress, + abi: eip2612Abi, + functionName: 'DOMAIN_SEPARATOR', + }, + { + address: tokenAddress, + abi: eip2612Abi, + functionName: 'nonces', + args: [client.account!.address], + }, + { + address: tokenAddress, + abi: eip2612Abi, + functionName: 'version', + }, + ] as const + + if (multicallAddress) { + const [nameResult, domainSeparatorResult, noncesResult, versionResult] = + await multicall(client, { + contracts: contractCalls, + multicallAddress, + }) + + if ( + nameResult.status !== 'success' || + domainSeparatorResult.status !== 'success' || + noncesResult.status !== 'success' || + !nameResult.result || + !domainSeparatorResult.result || + noncesResult.result === undefined + ) { + return defaultPermit + } + + const { isValid, domain } = validateDomainSeparator({ + name: nameResult.result, + version: versionResult.result ?? '1', + chainId: BigInt(chain.id), + verifyingContract: tokenAddress, + domainSeparator: domainSeparatorResult.result, + }) + + return { + name: nameResult.result, + version: versionResult.result ?? '1', + nonce: noncesResult.result, + supported: isValid, + domain, + } + } + + const [nameResult, domainSeparatorResult, noncesResult, versionResult] = + (await Promise.allSettled( + contractCalls.map((call) => readContract(client, call)) + )) as [ + PromiseSettledResult, + PromiseSettledResult, + PromiseSettledResult, + PromiseSettledResult, + ] + + if ( + nameResult.status !== 'fulfilled' || + domainSeparatorResult.status !== 'fulfilled' || + noncesResult.status !== 'fulfilled' + ) { + return defaultPermit + } + + const name = nameResult.value + const version = + versionResult.status === 'fulfilled' ? versionResult.value : '1' + const { isValid, domain } = validateDomainSeparator({ + name, + version, + chainId: BigInt(chain.id), + verifyingContract: tokenAddress, + domainSeparator: domainSeparatorResult.value, + }) + + return { + name, + version, + nonce: noncesResult.value, + supported: isValid, + domain, + } + } catch { + return defaultPermit + } +} diff --git a/src/core/EVM/multisig.ts b/src/core/EVM/multisig.ts deleted file mode 100644 index a4a895c1..00000000 --- a/src/core/EVM/multisig.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { ExtendedChain, LiFiStep, ProcessType } from '@lifi/types' -import type { Hash } from 'viem' -import { LiFiErrorCode } from '../../errors/constants.js' -import { TransactionError } from '../../errors/errors.js' -import type { StatusManager } from '../StatusManager.js' -import type { MultisigConfig, MultisigTxDetails } from './types.js' - -export const updateMultisigRouteProcess = async ( - internalTxHash: Hash, - step: LiFiStep, - processType: ProcessType, - fromChain: ExtendedChain, - statusManager: StatusManager, - multisig?: MultisigConfig -) => { - if (!multisig?.getMultisigTransactionDetails) { - throw new Error( - 'getMultisigTransactionDetails is missing in multisig config.' - ) - } - - const updateIntermediateMultisigStatus = () => { - statusManager.updateProcess(step, processType, 'PENDING') - } - - const multisigStatusResponse: MultisigTxDetails = - await multisig?.getMultisigTransactionDetails( - internalTxHash, - fromChain.id, - updateIntermediateMultisigStatus - ) - - if (multisigStatusResponse.status === 'DONE') { - statusManager.updateProcess(step, processType, 'PENDING', { - txHash: multisigStatusResponse.txHash, - multisigTxHash: undefined, - txLink: `${fromChain.metamask.blockExplorerUrls[0]}tx/${multisigStatusResponse.txHash}`, - }) - } - - if (multisigStatusResponse.status === 'FAILED') { - throw new TransactionError( - LiFiErrorCode.TransactionFailed, - 'Multisig transaction failed.' - ) - } - - if (multisigStatusResponse.status === 'CANCELLED') { - throw new TransactionError( - LiFiErrorCode.SignatureRejected, - 'Transaction was rejected by user.' - ) - } -} diff --git a/src/core/EVM/parseEVMErrors.ts b/src/core/EVM/parseEVMErrors.ts index 093e7a05..026daa17 100644 --- a/src/core/EVM/parseEVMErrors.ts +++ b/src/core/EVM/parseEVMErrors.ts @@ -1,9 +1,10 @@ -import type { LiFiStep, Process } from '@lifi/types' +import type { LiFiStep } from '@lifi/types' import { SDKError } from '../../errors/SDKError.js' import { BaseError } from '../../errors/baseError.js' import { ErrorMessage, LiFiErrorCode } from '../../errors/constants.js' import { TransactionError, UnknownError } from '../../errors/errors.js' -import { fetchTxErrorDetails } from '../../helpers.js' +import { fetchTxErrorDetails } from '../../utils/fetchTxErrorDetails.js' +import type { Process } from '../types.js' export const parseEVMErrors = async ( e: Error, @@ -26,7 +27,18 @@ const handleSpecificErrors = async ( step?: LiFiStep, process?: Process ) => { - if (e.cause?.name === 'UserRejectedRequestError') { + if ( + e.name === 'UserRejectedRequestError' || + e.cause?.name === 'UserRejectedRequestError' + ) { + return new TransactionError(LiFiErrorCode.SignatureRejected, e.message, e) + } + // Safe Wallet via WalletConnect returns -32000 code when user rejects the signature + // { + // code: -32000, + // message: 'User rejected transaction', + // } + if (e.cause?.code === -32000) { return new TransactionError(LiFiErrorCode.SignatureRejected, e.message, e) } diff --git a/src/core/EVM/parseEVMErrors.unit.spec.ts b/src/core/EVM/parseEVMErrors.unit.spec.ts index a3cd8d4c..4a25ca2d 100644 --- a/src/core/EVM/parseEVMErrors.unit.spec.ts +++ b/src/core/EVM/parseEVMErrors.unit.spec.ts @@ -1,4 +1,4 @@ -import type { LiFiStep, Process } from '@lifi/types' +import type { LiFiStep } from '@lifi/types' import { beforeAll, describe, expect, it, vi } from 'vitest' import { buildStepObject } from '../../../tests/fixtures.js' import { setupTestEnvironment } from '../../../tests/setup.js' @@ -10,7 +10,8 @@ import { LiFiErrorCode, } from '../../errors/constants.js' import { TransactionError } from '../../errors/errors.js' -import * as helpers from '../../helpers.js' +import * as helpers from '../../utils/fetchTxErrorDetails.js' +import type { Process } from '../types.js' import { parseEVMErrors } from './parseEVMErrors.js' beforeAll(setupTestEnvironment) diff --git a/src/core/EVM/permit2/allowanceTransfer.ts b/src/core/EVM/permit2/allowanceTransfer.ts new file mode 100644 index 00000000..aa05419b --- /dev/null +++ b/src/core/EVM/permit2/allowanceTransfer.ts @@ -0,0 +1,168 @@ +import { + type Address, + type TypedData, + type TypedDataDomain, + hashTypedData, +} from 'viem' +import { MaxUint48, MaxUint160 } from '../../../constants.js' +import { invariant } from '../../../utils/invariant.js' +import { MaxSigDeadline } from './constants.js' +import { permit2Domain } from './domain.js' + +export const MaxAllowanceTransferAmount = MaxUint160 +export const MaxAllowanceExpiration = MaxUint48 +export const MaxOrderedNonce = MaxUint48 + +export interface PermitDetails { + token: Address + amount: bigint + expiration: number + nonce: number +} + +export interface PermitSingle { + details: PermitDetails + spender: Address + sigDeadline: bigint +} + +export interface PermitBatch { + details: PermitDetails[] + spender: Address + sigDeadline: bigint +} + +export type PermitSingleData = { + domain: TypedDataDomain + types: TypedData + values: PermitSingle +} + +export type PermitBatchData = { + domain: TypedDataDomain + types: TypedData + values: PermitBatch +} + +const PERMIT_DETAILS = [ + { name: 'token', type: 'address' }, + { name: 'amount', type: 'uint160' }, + { name: 'expiration', type: 'uint48' }, + { name: 'nonce', type: 'uint48' }, +] as const + +const PERMIT_TYPES = { + PermitDetails: PERMIT_DETAILS, + PermitSingle: [ + { name: 'details', type: 'PermitDetails' }, + { name: 'spender', type: 'address' }, + { name: 'sigDeadline', type: 'uint256' }, + ], +} as const + +const PERMIT_BATCH_TYPES = { + PermitDetails: PERMIT_DETAILS, + PermitBatch: [ + { name: 'details', type: 'PermitDetails[]' }, + { name: 'spender', type: 'address' }, + { name: 'sigDeadline', type: 'uint256' }, + ], +} as const + +function isPermit(permit: PermitSingle | PermitBatch): permit is PermitSingle { + return !Array.isArray(permit.details) +} + +export function getPermitSingleData( + permit: PermitSingle, + permit2Address: Address, + chainId: number +) { + invariant(MaxSigDeadline >= permit.sigDeadline, 'SIG_DEADLINE_OUT_OF_RANGE') + + const domain = permit2Domain(permit2Address, chainId) + validatePermitDetails(permit.details) + + return { + domain, + values: permit, + } +} + +export function getPermitBatchData( + permit: PermitBatch, + permit2Address: Address, + chainId: number +) { + invariant(MaxSigDeadline >= permit.sigDeadline, 'SIG_DEADLINE_OUT_OF_RANGE') + + const domain = permit2Domain(permit2Address, chainId) + permit.details.forEach(validatePermitDetails) + + return { + domain, + values: permit, + } +} + +export function getPermitData( + permit: PermitSingle | PermitBatch, + permit2Address: Address, + chainId: number +): PermitSingleData | PermitBatchData { + invariant(MaxSigDeadline >= permit.sigDeadline, 'SIG_DEADLINE_OUT_OF_RANGE') + + const domain = permit2Domain(permit2Address, chainId) + if (isPermit(permit)) { + validatePermitDetails(permit.details) + return { + domain, + types: PERMIT_TYPES, + values: permit, + } + } + permit.details.forEach(validatePermitDetails) + return { + domain, + types: PERMIT_BATCH_TYPES, + values: permit, + } +} + +export function hash( + permit: PermitSingle | PermitBatch, + permit2Address: Address, + chainId: number +): string { + if (isPermit(permit)) { + const { domain, values } = getPermitSingleData( + permit, + permit2Address, + chainId + ) + + return hashTypedData({ + domain, + types: PERMIT_TYPES, + primaryType: 'PermitSingle', + message: values, + }) + } + const { domain, values } = getPermitBatchData(permit, permit2Address, chainId) + + return hashTypedData({ + domain, + types: PERMIT_BATCH_TYPES, + primaryType: 'PermitBatch', + message: values, + }) +} + +function validatePermitDetails(details: PermitDetails) { + invariant(MaxOrderedNonce >= details.nonce, 'NONCE_OUT_OF_RANGE') + invariant(MaxAllowanceTransferAmount >= details.amount, 'AMOUNT_OUT_OF_RANGE') + invariant( + MaxAllowanceExpiration >= details.expiration, + 'EXPIRATION_OUT_OF_RANGE' + ) +} diff --git a/src/core/EVM/permit2/constants.ts b/src/core/EVM/permit2/constants.ts new file mode 100644 index 00000000..9fe607b9 --- /dev/null +++ b/src/core/EVM/permit2/constants.ts @@ -0,0 +1,11 @@ +import { MaxUint48, MaxUint160, MaxUint256 } from '../../../constants.js' + +export const MaxAllowanceTransferAmount = MaxUint160 +export const MaxAllowanceExpiration = MaxUint48 +export const MaxOrderedNonce = MaxUint48 + +export const MaxSignatureTransferAmount = MaxUint256 +export const MaxUnorderedNonce = MaxUint256 +export const MaxSigDeadline = MaxUint256 + +export const InstantExpiration = 0n diff --git a/src/core/EVM/permit2/domain.ts b/src/core/EVM/permit2/domain.ts new file mode 100644 index 00000000..941583cb --- /dev/null +++ b/src/core/EVM/permit2/domain.ts @@ -0,0 +1,20 @@ +import type { Address, TypedData, TypedDataDomain } from 'viem' + +const PERMIT2_DOMAIN_NAME = 'Permit2' + +export function permit2Domain( + permit2Address: Address, + chainId: number +): TypedDataDomain { + return { + name: PERMIT2_DOMAIN_NAME, + chainId, + verifyingContract: permit2Address, + } +} + +export type PermitData = { + domain: TypedDataDomain + types: TypedData + values: any +} diff --git a/src/core/EVM/permit2/signatureTransfer.ts b/src/core/EVM/permit2/signatureTransfer.ts new file mode 100644 index 00000000..07d385c7 --- /dev/null +++ b/src/core/EVM/permit2/signatureTransfer.ts @@ -0,0 +1,214 @@ +import { hashTypedData } from 'viem' +import type { Address, TypedData, TypedDataDomain } from 'viem' +import { invariant } from '../../../utils/invariant.js' +import { + MaxSigDeadline, + MaxSignatureTransferAmount, + MaxUnorderedNonce, +} from './constants.js' +import { permit2Domain } from './domain.js' + +export interface Witness { + witness: any + witnessTypeName: string + witnessType: Record +} + +export interface TokenPermissions { + token: Address + amount: bigint +} + +export interface PermitTransferFrom { + permitted: TokenPermissions + spender: Address + nonce: bigint + deadline: bigint +} + +export interface PermitBatchTransferFrom { + permitted: TokenPermissions[] + spender: Address + nonce: bigint + deadline: bigint +} + +export type PermitTransferFromData = { + domain: TypedDataDomain + types: TypedData + values: PermitTransferFrom +} + +export type PermitBatchTransferFromData = { + domain: TypedDataDomain + types: TypedData + values: PermitBatchTransferFrom +} + +const TOKEN_PERMISSIONS = [ + { name: 'token', type: 'address' }, + { name: 'amount', type: 'uint256' }, +] as const + +const PERMIT_TRANSFER_FROM_TYPES = { + TokenPermissions: TOKEN_PERMISSIONS, + PermitTransferFrom: [ + { name: 'permitted', type: 'TokenPermissions' }, + { name: 'spender', type: 'address' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], +} as const + +const PERMIT_BATCH_TRANSFER_FROM_TYPES = { + TokenPermissions: TOKEN_PERMISSIONS, + PermitBatchTransferFrom: [ + { name: 'permitted', type: 'TokenPermissions[]' }, + { name: 'spender', type: 'address' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], +} as const + +function isPermitTransferFrom( + permit: PermitTransferFrom | PermitBatchTransferFrom +): permit is PermitTransferFrom { + return !Array.isArray(permit.permitted) +} + +export function getPermitTransferData( + permit: PermitTransferFrom, + permit2Address: Address, + chainId: number, + witness?: Witness +): PermitTransferFromData { + invariant(MaxSigDeadline >= permit.deadline, 'SIG_DEADLINE_OUT_OF_RANGE') + invariant(MaxUnorderedNonce >= permit.nonce, 'NONCE_OUT_OF_RANGE') + + const domain = permit2Domain(permit2Address, chainId) + + validateTokenPermissions(permit.permitted) + + const types = witness + ? ({ + TokenPermissions: TOKEN_PERMISSIONS, + ...witness.witnessType, + PermitWitnessTransferFrom: [ + { name: 'permitted', type: 'TokenPermissions' }, + { name: 'spender', type: 'address' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + { name: 'witness', type: witness.witnessTypeName }, + ], + } as const) + : PERMIT_TRANSFER_FROM_TYPES + + const values = witness + ? Object.assign(permit, { witness: witness.witness }) + : permit + + return { + domain, + types, + values, + } +} + +export function getPermitBatchTransferData( + permit: PermitBatchTransferFrom, + permit2Address: Address, + chainId: number, + witness?: Witness +): PermitBatchTransferFromData { + invariant(MaxSigDeadline >= permit.deadline, 'SIG_DEADLINE_OUT_OF_RANGE') + invariant(MaxUnorderedNonce >= permit.nonce, 'NONCE_OUT_OF_RANGE') + + const domain = permit2Domain(permit2Address, chainId) + + permit.permitted.forEach(validateTokenPermissions) + + const types = witness + ? { + ...witness.witnessType, + TokenPermissions: TOKEN_PERMISSIONS, + PermitBatchWitnessTransferFrom: [ + { name: 'permitted', type: 'TokenPermissions[]' }, + { name: 'spender', type: 'address' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + { name: 'witness', type: witness.witnessTypeName }, + ], + } + : PERMIT_BATCH_TRANSFER_FROM_TYPES + + const values = witness + ? Object.assign(permit, { witness: witness.witness }) + : permit + + return { + domain, + types, + values, + } +} + +// return the data to be sent in a eth_signTypedData RPC call +// for signing the given permit data +export function getPermitData( + permit: PermitTransferFrom | PermitBatchTransferFrom, + permit2Address: Address, + chainId: number, + witness?: Witness +): PermitTransferFromData | PermitBatchTransferFromData { + if (isPermitTransferFrom(permit)) { + return getPermitTransferData(permit, permit2Address, chainId, witness) + } + return getPermitBatchTransferData(permit, permit2Address, chainId, witness) +} + +export function hash( + permit: PermitTransferFrom | PermitBatchTransferFrom, + permit2Address: Address, + chainId: number, + witness?: Witness +) { + if (isPermitTransferFrom(permit)) { + const { domain, types, values } = getPermitTransferData( + permit, + permit2Address, + chainId, + witness + ) + + return hashTypedData({ + domain, + types, + primaryType: witness ? 'PermitWitnessTransferFrom' : 'PermitTransferFrom', + message: { + ...values, + }, + }) + } + const { domain, types, values } = getPermitBatchTransferData( + permit, + permit2Address, + chainId, + witness + ) + + return hashTypedData({ + domain, + types, + primaryType: witness + ? 'PermitBatchWitnessTransferFrom' + : 'PermitBatchTransferFrom', + message: { ...values }, + }) +} + +function validateTokenPermissions(permissions: TokenPermissions) { + invariant( + MaxSignatureTransferAmount >= permissions.amount, + 'AMOUNT_OUT_OF_RANGE' + ) +} diff --git a/src/core/EVM/publicClient.ts b/src/core/EVM/publicClient.ts index ad94b9eb..85d4c5d6 100644 --- a/src/core/EVM/publicClient.ts +++ b/src/core/EVM/publicClient.ts @@ -1,9 +1,10 @@ -import { ChainId } from '@lifi/types' +import { ChainId, ChainType } from '@lifi/types' import type { Client } from 'viem' import { http, createClient, fallback, webSocket } from 'viem' import { type Chain, mainnet } from 'viem/chains' import { config } from '../../config.js' import { getRpcUrls } from '../rpc.js' +import type { EVMProvider } from './types.js' // cached providers const publicClients: Record = {} @@ -14,46 +15,48 @@ const publicClients: Record = {} * @returns The public client for the given chain */ export const getPublicClient = async (chainId: number): Promise => { - if (!publicClients[chainId]) { - const urls = await getRpcUrls(chainId) - const fallbackTransports = urls.map((url) => - url.startsWith('wss') - ? webSocket(url) - : http(url, { - batch: { - batchSize: 64, - }, - }) - ) - const _chain = await config.getChainById(chainId) - const chain: Chain = { - ..._chain, - ..._chain.metamask, - name: _chain.metamask.chainName, - rpcUrls: { - default: { http: _chain.metamask.rpcUrls }, - public: { http: _chain.metamask.rpcUrls }, - }, - } - // Add ENS contracts - if (chain.id === ChainId.ETH) { - chain.contracts = { - ...mainnet.contracts, - ...chain.contracts, - } - } - publicClients[chainId] = createClient({ - chain: chain, - transport: fallback(fallbackTransports), - batch: { - multicall: true, - }, - }) + if (publicClients[chainId]) { + return publicClients[chainId] } - if (!publicClients[chainId]) { - throw new Error(`Unable to configure provider for chain ${chainId}`) + const urls = await getRpcUrls(chainId) + const fallbackTransports = urls.map((url) => + url.startsWith('wss') + ? webSocket(url) + : http(url, { + batch: { + batchSize: 64, + }, + }) + ) + const _chain = await config.getChainById(chainId) + const chain: Chain = { + ..._chain, + ..._chain.metamask, + name: _chain.metamask.chainName, + rpcUrls: { + default: { http: _chain.metamask.rpcUrls }, + public: { http: _chain.metamask.rpcUrls }, + }, + } + // Add ENS contracts + if (chain.id === ChainId.ETH) { + chain.contracts = { + ...mainnet.contracts, + ...chain.contracts, + } } + const provider = config.getProvider(ChainType.EVM) as EVMProvider | undefined + publicClients[chainId] = createClient({ + chain: chain, + transport: fallback( + fallbackTransports, + provider?.options?.fallbackTransportConfig + ), + batch: { + multicall: true, + }, + }) return publicClients[chainId] } diff --git a/src/core/EVM/setAllowance.ts b/src/core/EVM/setAllowance.ts index ac6b3544..c825e9d4 100644 --- a/src/core/EVM/setAllowance.ts +++ b/src/core/EVM/setAllowance.ts @@ -1,4 +1,4 @@ -import type { Client, Hash, SendTransactionParameters } from 'viem' +import type { Address, Client, Hash, SendTransactionParameters } from 'viem' import { encodeFunctionData } from 'viem' import { sendTransaction } from 'viem/actions' import { getAction } from 'viem/utils' @@ -11,10 +11,10 @@ import { getMaxPriorityFeePerGas } from './utils.js' export const setAllowance = async ( client: Client, - tokenAddress: string, - contractAddress: string, + tokenAddress: Address, + contractAddress: Address, amount: bigint, - settings?: ExecutionOptions, + executionOptions?: ExecutionOptions, returnPopulatedTransaction?: boolean ): Promise => { const data = encodeFunctionData({ @@ -36,9 +36,9 @@ export const setAllowance = async ( : undefined, } - if (settings?.updateTransactionRequestHook) { + if (executionOptions?.updateTransactionRequestHook) { const customizedTransactionRequest: TransactionParameters = - await settings.updateTransactionRequestHook({ + await executionOptions.updateTransactionRequestHook({ requestType: 'approve', ...transactionRequest, }) @@ -61,7 +61,6 @@ export const setAllowance = async ( gasPrice: transactionRequest.gasPrice, maxFeePerGas: transactionRequest.maxFeePerGas, maxPriorityFeePerGas: transactionRequest.maxPriorityFeePerGas, - chain: null, } as SendTransactionParameters) } @@ -86,16 +85,16 @@ export const setTokenAllowance = async ({ } const approvedAmount = await getAllowance( token.chainId, - token.address, + token.address as Address, walletClient.account!.address, - spenderAddress + spenderAddress as Address ) if (amount > approvedAmount) { const approveTx = await setAllowance( walletClient, - token.address, - spenderAddress, + token.address as Address, + spenderAddress as Address, amount ) @@ -122,15 +121,15 @@ export const revokeTokenApproval = async ({ } const approvedAmount = await getAllowance( token.chainId, - token.address, + token.address as Address, walletClient.account!.address, - spenderAddress + spenderAddress as Address ) if (approvedAmount > 0) { const approveTx = await setAllowance( walletClient, - token.address, - spenderAddress, + token.address as Address, + spenderAddress as Address, 0n ) diff --git a/src/core/EVM/signPermitMessage.ts b/src/core/EVM/signPermitMessage.ts new file mode 100644 index 00000000..1dd34d4e --- /dev/null +++ b/src/core/EVM/signPermitMessage.ts @@ -0,0 +1,273 @@ +import type { ExtendedChain } from '@lifi/types' +import type { Address, Client, Hex } from 'viem' +import { encodeFunctionData, keccak256, parseSignature } from 'viem' +import { readContract, signTypedData } from 'viem/actions' +import { getAction } from 'viem/utils' +import type { TransactionParameters } from '../types.js' +import { eip2612Types, permit2ProxyAbi } from './abi.js' +import { type NativePermitData, getNativePermit } from './getNativePermit.js' +import { + type PermitBatchTransferFromData, + type PermitTransferFrom, + type PermitTransferFromData, + getPermitData, +} from './permit2/signatureTransfer.js' + +export interface PermitSignature { + signature: Hex + data: Hex +} + +export const signNativePermitMessage = async ( + client: Client, + transactionRequest: TransactionParameters, + chain: ExtendedChain, + tokenAddress: Address, + amount: bigint, + nativePermit: NativePermitData +): Promise => { + const deadline = BigInt(Math.floor(Date.now() / 1000) + 30 * 60) // 30 minutes + + const message = { + owner: client.account!.address, + spender: chain.permit2Proxy as Address, + value: amount, + nonce: nativePermit.nonce, + deadline, + } + + const signature = await getAction( + client, + signTypedData, + 'signTypedData' + )({ + account: client.account!, + domain: nativePermit.domain, + types: eip2612Types, + primaryType: 'Permit', + message, + }) + + const { v, r, s } = parseSignature(signature) + + const data = encodeFunctionData({ + abi: permit2ProxyAbi, + functionName: 'callDiamondWithEIP2612Signature', + args: [ + tokenAddress, + amount, + deadline, + Number(v), + r, + s, + transactionRequest.data as Hex, + ], + }) + + return { + signature, + data, + } +} + +export const signPermit2Message = async ( + client: Client, + transactionRequest: TransactionParameters, + chain: ExtendedChain, + tokenAddress: Address, + amount: bigint +) => { + const nonce = await readContract(client, { + address: chain.permit2Proxy as Address, + abi: permit2ProxyAbi, + functionName: 'nextNonce', + args: [client.account!.address], + }) + + const permitTransferFrom: PermitTransferFrom = { + permitted: { + token: tokenAddress, + amount: amount, + }, + spender: chain.permit2Proxy as Address, + nonce: nonce, + deadline: BigInt(Math.floor(Date.now() / 1000) + 30 * 60), // 30 minutes + } + + const { domain, types, values } = getPermitData( + permitTransferFrom, + chain.permit2 as Address, + chain.id + ) + + const signature = await getAction( + client, + signTypedData, + 'signTypedData' + )({ + account: client.account!, + primaryType: 'PermitTransferFrom', + domain, + types, + message: { ...values }, + }) + + const data = encodeFunctionData({ + abi: permit2ProxyAbi, + functionName: 'callDiamondWithPermit2', + args: [ + transactionRequest.data as Hex, + [ + [tokenAddress, amount], + permitTransferFrom.nonce, + permitTransferFrom.deadline, + ], + signature as Hex, + ], + }) + + return { + signature, + data, + } +} + +export const signPermit2WitnessMessage = async ( + client: Client, + transactionRequest: TransactionParameters, + chain: ExtendedChain, + tokenAddress: Address, + amount: bigint, + permitData?: PermitTransferFromData | PermitBatchTransferFromData +): Promise => { + let _permitData = permitData + if (!_permitData) { + const nonce = await readContract(client, { + address: chain.permit2Proxy as Address, + abi: permit2ProxyAbi, + functionName: 'nextNonce', + args: [client.account!.address], + }) + + const permitTransferFrom: PermitTransferFrom = { + permitted: { + token: tokenAddress, + amount: amount, + }, + spender: chain.permit2Proxy as Address, + nonce: nonce, + deadline: BigInt(Math.floor(Date.now() / 1000) + 30 * 60), // 30 minutes + } + + // Create witness data for the LI.FI call + const witness = { + witness: { + diamondAddress: chain.diamondAddress as Address, + diamondCalldataHash: keccak256(transactionRequest.data as Hex), + }, + witnessTypeName: 'LiFiCall', + witnessType: { + LiFiCall: [ + { name: 'diamondAddress', type: 'address' }, + { name: 'diamondCalldataHash', type: 'bytes32' }, + ], + }, + } + + _permitData = getPermitData( + permitTransferFrom, + chain.permit2 as Address, + chain.id, + witness + ) + } + + const signature = await getAction( + client, + signTypedData, + 'signTypedData' + )({ + account: client.account!, + primaryType: 'PermitWitnessTransferFrom', + domain: _permitData.domain, + types: _permitData.types, + message: { ..._permitData.values }, + }) + + const data = encodeFunctionData({ + abi: permit2ProxyAbi, + functionName: 'callDiamondWithPermit2Witness', + args: [ + transactionRequest.data as Hex, + client.account!.address, + [ + [tokenAddress, amount], + _permitData.values.nonce, + _permitData.values.deadline, + ], + signature as Hex, + ], + }) + + return { + signature, + data, + } +} + +export const signPermitMessage = async ( + client: Client, + { + transactionRequest, + chain, + tokenAddress, + amount, + nativePermit, + permitData, + useWitness = false, + }: { + transactionRequest: TransactionParameters + chain: ExtendedChain + tokenAddress: Address + amount: bigint + nativePermit?: NativePermitData + permitData?: PermitTransferFromData | PermitBatchTransferFromData + useWitness?: boolean + } +): Promise => { + if (useWitness) { + return signPermit2WitnessMessage( + client, + transactionRequest, + chain, + tokenAddress, + amount, + permitData + ) + } + + let _nativePermit = nativePermit + + if (!_nativePermit) { + _nativePermit = await getNativePermit(client, chain, tokenAddress) + } + + if (_nativePermit.supported) { + return signNativePermitMessage( + client, + transactionRequest, + chain, + tokenAddress, + amount, + _nativePermit + ) + } + + return signPermit2Message( + client, + transactionRequest, + chain, + tokenAddress, + amount + ) +} diff --git a/src/core/EVM/typeguards.ts b/src/core/EVM/typeguards.ts new file mode 100644 index 00000000..72d88e37 --- /dev/null +++ b/src/core/EVM/typeguards.ts @@ -0,0 +1,10 @@ +import type { LiFiStep } from '@lifi/types' +import type { LiFiStepExtended } from '../types.js' +import type { EVMPermitStep } from './types.js' + +export function isRelayerStep( + step: LiFiStepExtended | LiFiStep +): step is EVMPermitStep { + const evmStep = step as EVMPermitStep + return 'permit' in evmStep && 'permitData' in evmStep && 'witness' in evmStep +} diff --git a/src/core/EVM/types.ts b/src/core/EVM/types.ts index 7451450e..5433f231 100644 --- a/src/core/EVM/types.ts +++ b/src/core/EVM/types.ts @@ -1,17 +1,23 @@ import { type BaseToken, ChainType } from '@lifi/types' -import type { Client, Hash } from 'viem' -import type { SwitchChainHook } from '../types.js' +import type { Client, FallbackTransportConfig } from 'viem' +import type { LiFiStepExtended, SwitchChainHook } from '../types.js' import type { SDKProvider } from '../types.js' +import type { PermitData } from './permit2/domain.js' +import type { + PermitTransferFrom, + Witness, +} from './permit2/signatureTransfer.js' export interface EVMProviderOptions { getWalletClient?: () => Promise switchChain?: SwitchChainHook - multisig?: MultisigConfig + fallbackTransportConfig?: FallbackTransportConfig } export interface EVMProvider extends SDKProvider { + options: EVMProviderOptions setOptions(options: EVMProviderOptions): void - multisig?: MultisigConfig + getWalletClient?(): Promise } export function isEVM(provider: SDKProvider): provider is EVMProvider { @@ -51,26 +57,8 @@ export interface RevokeApprovalRequest { spenderAddress: string } -export interface MultisigTxDetails { - status: 'DONE' | 'FAILED' | 'PENDING' | 'CANCELLED' - txHash?: Hash -} - -export interface MultisigTransaction { - to: string - value?: bigint - data: string -} - -export interface MultisigConfig { - isMultisigWalletClient: boolean - getMultisigTransactionDetails: ( - txHash: Hash, - fromChainId: number, - updateIntermediateStatus?: () => void - ) => Promise - sendBatchTransaction?: ( - batchTransactions: MultisigTransaction[] - ) => Promise - shouldBatchTransactions?: boolean +export interface EVMPermitStep extends LiFiStepExtended { + permit: PermitTransferFrom + permitData: PermitData + witness: Witness } diff --git a/src/core/EVM/utils.ts b/src/core/EVM/utils.ts index 5761dcd5..7c0a52ad 100644 --- a/src/core/EVM/utils.ts +++ b/src/core/EVM/utils.ts @@ -1,9 +1,60 @@ -import type { ChainId } from '@lifi/types' -import type { Client, Transaction } from 'viem' +import type { ChainId, ExtendedChain } from '@lifi/types' +import type { Address, Chain, Client, Transaction } from 'viem' import { getBlock } from 'viem/actions' import { config } from '../../config.js' import { median } from '../../utils/median.js' +type ChainBlockExplorer = { + name: string + url: string +} + +type ChainBlockExplorers = { + [key: string]: ChainBlockExplorer + default: ChainBlockExplorer +} + +export const convertExtendedChain = (chain: ExtendedChain): Chain => ({ + ...chain, + ...chain.metamask, + blockExplorers: chain.metamask.blockExplorerUrls.reduce( + (blockExplorers, blockExplorer, index) => { + blockExplorers[index === 0 ? 'default' : `${index}`] = { + name: blockExplorer, + url: blockExplorer, + } + return blockExplorers + }, + {} as ChainBlockExplorers + ), + name: chain.metamask.chainName, + rpcUrls: { + default: { http: chain.metamask.rpcUrls }, + public: { http: chain.metamask.rpcUrls }, + }, + contracts: { + ...(chain.multicallAddress + ? { multicall3: { address: chain.multicallAddress as Address } } + : undefined), + }, +}) + +export function isExtendedChain(chain: any): chain is ExtendedChain { + return ( + typeof chain === 'object' && + chain !== null && + 'key' in chain && + 'chainType' in chain && + 'coin' in chain && + 'mainnet' in chain && + 'logoURI' in chain && + typeof chain.metamask === 'object' && + chain.metamask !== null && + typeof chain.nativeToken === 'object' && + chain.nativeToken !== null + ) +} + export const getMaxPriorityFeePerGas = async ( client: Client ): Promise => { @@ -37,9 +88,10 @@ export const getMaxPriorityFeePerGas = async ( // Multicall export const getMulticallAddress = async ( chainId: ChainId -): Promise => { +): Promise
=> { const chains = await config.getChains() - return chains.find((chain) => chain.id === chainId)?.multicallAddress + return chains.find((chain) => chain.id === chainId) + ?.multicallAddress as Address } // Modified viem retryDelay exponential backoff function. diff --git a/src/core/EVM/waitForBatchTransactionReceipt.ts b/src/core/EVM/waitForBatchTransactionReceipt.ts new file mode 100644 index 00000000..2c11227e --- /dev/null +++ b/src/core/EVM/waitForBatchTransactionReceipt.ts @@ -0,0 +1,61 @@ +import type { + Client, + Hash, + WalletCallReceipt as _WalletCallReceipt, +} from 'viem' +import { getCallsStatus } from 'viem/experimental' +import { getAction } from 'viem/utils' +import { LiFiErrorCode } from '../../errors/constants.js' +import { TransactionError } from '../../errors/errors.js' +import { waitForResult } from '../../utils/waitForResult.js' + +export type WalletCallReceipt = _WalletCallReceipt< + bigint, + 'success' | 'reverted' +> + +export const waitForBatchTransactionReceipt = async ( + client: Client, + batchHash: Hash +): Promise => { + return waitForResult( + async () => { + const callsDetails = await getAction( + client, + getCallsStatus, + 'getCallsStatus' + )({ + id: batchHash, + }) + + if (callsDetails.status === 'PENDING') { + return undefined + } + + if (callsDetails.status === 'CONFIRMED') { + if ( + !callsDetails.receipts?.length || + !callsDetails.receipts.every((receipt) => receipt.transactionHash) || + callsDetails.receipts.some((receipt) => receipt.status === 'reverted') + ) { + throw new TransactionError( + LiFiErrorCode.TransactionFailed, + 'Transaction was reverted.' + ) + } + const transactionReceipt = callsDetails.receipts.at(-1)! + return transactionReceipt + } + + throw new TransactionError( + LiFiErrorCode.TransactionNotFound, + 'Transaction not found.' + ) + }, + 5000, + 3, + (_, error) => { + return !(error instanceof TransactionError) + } + ) +} diff --git a/src/core/EVM/waitForRelayedTransactionReceipt.ts b/src/core/EVM/waitForRelayedTransactionReceipt.ts new file mode 100644 index 00000000..265f2236 --- /dev/null +++ b/src/core/EVM/waitForRelayedTransactionReceipt.ts @@ -0,0 +1,57 @@ +import type { ExtendedTransactionInfo } from '@lifi/types' +import type { Hash, WalletCallReceipt as _WalletCallReceipt } from 'viem' +import { LiFiErrorCode } from '../../errors/constants.js' +import { TransactionError } from '../../errors/errors.js' +import { getRelayedTransactionStatus } from '../../services/api.js' +import { waitForResult } from '../../utils/waitForResult.js' + +export type WalletCallReceipt = _WalletCallReceipt< + bigint, + 'success' | 'reverted' +> + +export const waitForRelayedTransactionReceipt = async ( + taskId: Hash +): Promise => { + return waitForResult( + async () => { + const result = await getRelayedTransactionStatus({ + taskId, + }).catch((e) => { + if (process.env.NODE_ENV === 'development') { + console.debug('Fetching status from relayer failed.', e) + } + return undefined + }) + + switch (result?.data.status) { + case 'PENDING': + return undefined + case 'DONE': { + const sending: ExtendedTransactionInfo | undefined = result?.data + .transactionStatus?.sending as ExtendedTransactionInfo + return { + status: 'success', + gasUsed: sending?.gasUsed, + transactionHash: result?.data.metadata.txHash, + } as unknown as WalletCallReceipt + } + case 'FAILED': + throw new TransactionError( + LiFiErrorCode.TransactionFailed, + 'Transaction was reverted.' + ) + default: + throw new TransactionError( + LiFiErrorCode.TransactionNotFound, + 'Transaction not found.' + ) + } + }, + 5000, + 3, + (_, error) => { + return !(error instanceof TransactionError) + } + ) +} diff --git a/src/core/Solana/SolanaStepExecutor.ts b/src/core/Solana/SolanaStepExecutor.ts index b2e16763..3f9b1490 100644 --- a/src/core/Solana/SolanaStepExecutor.ts +++ b/src/core/Solana/SolanaStepExecutor.ts @@ -217,24 +217,10 @@ export class SolanaStepExecutor extends BaseStepExecutor { } } - // Wait for the transaction status on the destination chain - const transactionHash = process.txHash - if (!transactionHash) { - throw new Error('Transaction hash is undefined.') - } - if (isBridgeExecution) { - process = this.statusManager.findOrCreateProcess({ - step, - type: 'RECEIVING_CHAIN', - status: 'PENDING', - chainId: toChain.id, - }) - } - await waitForDestinationChainTransaction( step, - process.type, - transactionHash, + process, + fromChain, toChain, this.statusManager ) diff --git a/src/core/Solana/getSolanaBalance.int.spec.ts b/src/core/Solana/getSolanaBalance.int.spec.ts index fc180748..fcde49e6 100644 --- a/src/core/Solana/getSolanaBalance.int.spec.ts +++ b/src/core/Solana/getSolanaBalance.int.spec.ts @@ -41,18 +41,15 @@ describe.sequential('Solana token balance', async () => { } } - it( - 'should handle empty lists', - async () => { - const walletAddress = defaultWalletAddress - const tokens: Token[] = [] - await loadAndCompareTokenAmounts(walletAddress, tokens) - }, - { retry: retryTimes, timeout } - ) + it('should handle empty lists', { retry: retryTimes, timeout }, async () => { + const walletAddress = defaultWalletAddress + const tokens: Token[] = [] + await loadAndCompareTokenAmounts(walletAddress, tokens) + }) it( 'should work for stables on SOL', + { retry: retryTimes, timeout }, async () => { const walletAddress = defaultWalletAddress const tokens = [ @@ -61,12 +58,12 @@ describe.sequential('Solana token balance', async () => { ] await loadAndCompareTokenAmounts(walletAddress, tokens) - }, - { retry: retryTimes, timeout } + } ) it( 'should return even with invalid data', + { retry: retryTimes, timeout }, async () => { const walletAddress = defaultWalletAddress const invalidToken = findDefaultToken(CoinKey.MATIC, ChainId.SOL) @@ -85,8 +82,7 @@ describe.sequential('Solana token balance', async () => { ) expect(invalidBalance).toBeDefined() expect(invalidBalance!.amount).toBeUndefined() - }, - { retry: retryTimes, timeout } + } ) // it( diff --git a/src/core/Solana/parseSolanaErrors.ts b/src/core/Solana/parseSolanaErrors.ts index fb5b8a11..5cdba7a0 100644 --- a/src/core/Solana/parseSolanaErrors.ts +++ b/src/core/Solana/parseSolanaErrors.ts @@ -1,8 +1,9 @@ -import type { LiFiStep, Process } from '@lifi/types' +import type { LiFiStep } from '@lifi/types' import { SDKError } from '../../errors/SDKError.js' import { BaseError } from '../../errors/baseError.js' import { ErrorMessage, LiFiErrorCode } from '../../errors/constants.js' import { TransactionError, UnknownError } from '../../errors/errors.js' +import type { Process } from '../types.js' export const parseSolanaErrors = async ( e: Error, diff --git a/src/core/StatusManager.ts b/src/core/StatusManager.ts index 452825ef..2f0ecc6f 100644 --- a/src/core/StatusManager.ts +++ b/src/core/StatusManager.ts @@ -1,15 +1,14 @@ +import type { ChainId, LiFiStep } from '@lifi/types' +import { executionState } from './executionState.js' +import { getProcessMessage } from './processMessages.js' import type { - ChainId, Execution, ExecutionStatus, - LiFiStep, + LiFiStepExtended, Process, ProcessStatus, ProcessType, -} from '@lifi/types' -import { executionState } from './executionState.js' -import { getProcessMessage } from './processMessages.js' -import type { LiFiStepExtended } from './types.js' +} from './types.js' export type FindOrCreateProcessProps = { step: LiFiStepExtended @@ -80,17 +79,38 @@ export class StatusManager { return step } + /** + * Finds a process of the specified type in the step's execution + * @param step The step to search in + * @param type The process type to find + * @param status Optional status to update the process with if found + * @returns The found process or undefined if not found + */ + findProcess( + step: LiFiStepExtended, + type: ProcessType, + status?: ProcessStatus + ): Process | undefined { + if (!step.execution?.process) { + throw new Error("Execution hasn't been initialized.") + } + + const process = step.execution.process.find((p) => p.type === type) + + if (process && status && process.status !== status) { + process.status = status + this.updateStepInRoute(step) + } + + return process + } + /** * Create and push a new process into the execution. - * @param step.step The step that should contain the new process. - * @param step.type Type of the process. Used to identify already existing processes. - * @param step.chainId Chain Id of the process. - * @param step.status By default created procces is set to the STARTED status. We can override new process with the needed status. - * @param root0 - * @param root0.step - * @param root0.type - * @param root0.chainId - * @param root0.status + * @param step The step that should contain the new process. + * @param type Type of the process. Used to identify already existing processes. + * @param chainId Chain Id of the process. + * @param status By default created procces is set to the STARTED status. We can override new process with the needed status. * @returns Returns process. */ findOrCreateProcess = ({ @@ -99,17 +119,9 @@ export class StatusManager { chainId, status, }: FindOrCreateProcessProps): Process => { - if (!step.execution?.process) { - throw new Error("Execution hasn't been initialized.") - } - - const process = step.execution.process.find((p) => p.type === type) + const process = this.findProcess(step, type, status) if (process) { - if (status && process.status !== status) { - process.status = status - this.updateStepInRoute(step) - } return process } @@ -121,7 +133,7 @@ export class StatusManager { chainId: chainId, } - step.execution.process.push(newProcess) + step.execution!.process.push(newProcess) this.updateStepInRoute(step) return newProcess } @@ -143,7 +155,7 @@ export class StatusManager { if (!step.execution) { throw new Error("Can't update an empty step execution.") } - const currentProcess = step?.execution?.process.find((p) => p.type === type) + const currentProcess = this.findProcess(step, type) if (!currentProcess) { throw new Error("Can't find a process for the given type.") diff --git a/src/core/StatusManager.unit.spec.ts b/src/core/StatusManager.unit.spec.ts index 14d679c7..de8b3c7d 100644 --- a/src/core/StatusManager.unit.spec.ts +++ b/src/core/StatusManager.unit.spec.ts @@ -1,11 +1,15 @@ -import type { ExecutionStatus, ProcessStatus, Route } from '@lifi/types' +import type { Route } from '@lifi/types' import type { Mock } from 'vitest' import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import { buildRouteObject, buildStepObject } from '../../tests/fixtures.js' import { setupTestEnvironment } from '../../tests/setup.js' import { StatusManager } from './StatusManager.js' import { executionState } from './executionState.js' -import type { LiFiStepExtended } from './types.js' +import type { + ExecutionStatus, + LiFiStepExtended, + ProcessStatus, +} from './types.js' // Note: using structuredClone when passing objects to the StatusManager shall make sure that we are not facing any unknown call-by-reference-issues anymore @@ -164,7 +168,7 @@ describe('StatusManager', () => { expect(process.type).toEqual('CROSS_CHAIN') expect(process.status).toEqual('STARTED') - expect(process.message).toEqual('Preparing bridge transaction.') + expect(process.message).toEqual('Preparing bridge transaction') const updatedExecution = Object.assign({}, step.execution, { process: [...step.execution!.process, process], diff --git a/src/core/UTXO/UTXOStepExecutor.ts b/src/core/UTXO/UTXOStepExecutor.ts index 6f61e081..a89972a8 100644 --- a/src/core/UTXO/UTXOStepExecutor.ts +++ b/src/core/UTXO/UTXOStepExecutor.ts @@ -263,24 +263,10 @@ export class UTXOStepExecutor extends BaseStepExecutor { } } - // Wait for the transaction status on the destination chain - const transactionHash = process.txHash - if (!transactionHash) { - throw new Error('Transaction hash is undefined.') - } - if (isBridgeExecution) { - process = this.statusManager.findOrCreateProcess({ - step, - type: 'RECEIVING_CHAIN', - status: 'PENDING', - chainId: toChain.id, - }) - } - await waitForDestinationChainTransaction( step, - process.type, - transactionHash, + process, + fromChain, toChain, this.statusManager, 10_000 diff --git a/src/core/UTXO/getUTXOAPIPublicClient.ts b/src/core/UTXO/getUTXOAPIPublicClient.ts index eccc34da..7037c8f1 100644 --- a/src/core/UTXO/getUTXOAPIPublicClient.ts +++ b/src/core/UTXO/getUTXOAPIPublicClient.ts @@ -49,15 +49,15 @@ export const getUTXOAPIPublicClient = async (chainId: number) => { key: 'blockchair', includeChainToURL: true, }), - utxo('https://rpc.ankr.com/http/btc_blockbook/api/v2', { - key: 'ankr', - }), utxo('https://api.blockcypher.com/v1/btc/main', { key: 'blockcypher', }), utxo('https://mempool.space/api', { key: 'mempool', }), + utxo('https://rpc.ankr.com/http/btc_blockbook/api/v2', { + key: 'ankr', + }), ]), }).extend(UTXOAPIActions) publicAPIClients[chainId] = client diff --git a/src/core/UTXO/getUTXOBalance.int.spec.ts b/src/core/UTXO/getUTXOBalance.int.spec.ts index 759b8f0c..a2ff13d9 100644 --- a/src/core/UTXO/getUTXOBalance.int.spec.ts +++ b/src/core/UTXO/getUTXOBalance.int.spec.ts @@ -40,6 +40,7 @@ describe('getBalances integration tests', () => { it( 'should work for ERC20 on POL', + { retry: retryTimes, timeout }, async () => { const walletAddress = defaultWalletAddress const tokens = [ @@ -48,8 +49,7 @@ describe('getBalances integration tests', () => { ] await loadAndCompareTokenAmounts(walletAddress, tokens) - }, - { retry: retryTimes, timeout } + } ) // it( diff --git a/src/core/UTXO/parseUTXOErrors.ts b/src/core/UTXO/parseUTXOErrors.ts index bdfd911b..d865f2db 100644 --- a/src/core/UTXO/parseUTXOErrors.ts +++ b/src/core/UTXO/parseUTXOErrors.ts @@ -1,8 +1,9 @@ -import type { LiFiStep, Process } from '@lifi/types' +import type { LiFiStep } from '@lifi/types' import { SDKError } from '../../errors/SDKError.js' import { BaseError } from '../../errors/baseError.js' import { ErrorMessage, LiFiErrorCode } from '../../errors/constants.js' import { TransactionError, UnknownError } from '../../errors/errors.js' +import type { Process } from '../types.js' export const parseUTXOErrors = async ( e: Error, diff --git a/src/core/checkBalance.ts b/src/core/checkBalance.ts index b1e48d18..4cf813b5 100644 --- a/src/core/checkBalance.ts +++ b/src/core/checkBalance.ts @@ -28,13 +28,16 @@ export const checkBalance = async ( } else { const neeeded = formatUnits(neededBalance, token.decimals) const current = formatUnits(currentBalance, token.decimals) - let _errorMessage = `Your ${token.symbol} balance is too low, you try to transfer ${neeeded} ${token.symbol}, but your wallet only holds ${current} ${token.symbol}. No funds have been sent.` + let errorMessage = `Your ${token.symbol} balance is too low, you try to transfer ${neeeded} ${token.symbol}, but your wallet only holds ${current} ${token.symbol}. No funds have been sent.` if (currentBalance !== 0n) { - _errorMessage += `If the problem consists, please delete this transfer and start a new one with a maximum of ${current} ${token.symbol}.` + errorMessage += `If the problem consists, please delete this transfer and start a new one with a maximum of ${current} ${token.symbol}.` } - throw new BalanceError('The balance is too low.') + throw new BalanceError( + 'The balance is too low.', + new Error(errorMessage) + ) } } } diff --git a/src/core/processMessages.ts b/src/core/processMessages.ts index 13541980..da23bb0b 100644 --- a/src/core/processMessages.ts +++ b/src/core/processMessages.ts @@ -1,38 +1,42 @@ -import type { - ProcessStatus, - ProcessType, - StatusMessage, - Substatus, -} from '@lifi/types' +import type { StatusMessage, Substatus } from '@lifi/types' +import type { ProcessStatus } from './types.js' +import type { ProcessType } from './types.js' const processMessages: Record< ProcessType, Partial> > = { TOKEN_ALLOWANCE: { - STARTED: 'Setting token allowance.', - PENDING: 'Waiting for token allowance.', - DONE: 'Token allowance set.', + STARTED: 'Setting token allowance', + PENDING: 'Waiting for token allowance', + DONE: 'Token allowance set', }, SWITCH_CHAIN: { - PENDING: 'Chain switch required.', - DONE: 'Chain switched successfully.', + ACTION_REQUIRED: 'Chain switch required', + PENDING: 'Waiting for chain switch', + DONE: 'Chain switched', }, SWAP: { - STARTED: 'Preparing swap transaction.', - ACTION_REQUIRED: 'Please sign the transaction.', - PENDING: 'Waiting for swap transaction.', - DONE: 'Swap completed.', + STARTED: 'Preparing swap transaction', + ACTION_REQUIRED: 'Please sign the transaction', + PENDING: 'Waiting for swap transaction', + DONE: 'Swap completed', }, CROSS_CHAIN: { - STARTED: 'Preparing bridge transaction.', - ACTION_REQUIRED: 'Please sign the transaction.', - PENDING: 'Waiting for bridge transaction.', - DONE: 'Bridge transaction confirmed.', + STARTED: 'Preparing bridge transaction', + ACTION_REQUIRED: 'Please sign the transaction', + PENDING: 'Waiting for bridge transaction', + DONE: 'Bridge transaction confirmed', }, RECEIVING_CHAIN: { - PENDING: 'Waiting for destination chain.', - DONE: 'Bridge completed.', + PENDING: 'Waiting for destination chain', + DONE: 'Bridge completed', + }, + PERMIT: { + STARTED: 'Preparing transaction', + ACTION_REQUIRED: 'Sign permit message', + PENDING: 'Waiting for permit message', + DONE: 'Permit message signed', }, TRANSACTION: {}, } diff --git a/src/core/types.ts b/src/core/types.ts index c3997964..4270b1eb 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -1,9 +1,11 @@ import type { ChainType, - Execution, + FeeCost, + GasCost, LiFiStep, Route, Step, + Substatus, Token, TokenAmount, } from '@lifi/types' @@ -117,3 +119,54 @@ export interface ExecutionOptions { */ infiniteApproval?: boolean } + +export type ExecutionStatus = 'ACTION_REQUIRED' | 'PENDING' | 'FAILED' | 'DONE' + +export type ProcessStatus = + | 'STARTED' + | 'ACTION_REQUIRED' + | 'PENDING' + | 'FAILED' + | 'DONE' + | 'CANCELLED' + +export type ProcessType = + | 'TOKEN_ALLOWANCE' + | 'PERMIT' + | 'SWITCH_CHAIN' + | 'SWAP' + | 'CROSS_CHAIN' + | 'RECEIVING_CHAIN' + | 'TRANSACTION' + +export type Process = { + type: ProcessType + status: ProcessStatus + substatus?: Substatus + chainId?: number + txHash?: string + multisigTxHash?: string + txLink?: string + startedAt: number + doneAt?: number + failedAt?: number + message?: string + error?: { + code: string | number + message: string + htmlMessage?: string + } + + // additional information + [key: string]: any +} + +export interface Execution { + status: ExecutionStatus + process: Array + fromAmount?: string + toAmount?: string + toToken?: Token + feeCosts?: FeeCost[] + gasCosts?: GasCost[] +} diff --git a/src/core/waitForDestinationChainTransaction.ts b/src/core/waitForDestinationChainTransaction.ts index 5102cbe9..d5b8daa0 100644 --- a/src/core/waitForDestinationChainTransaction.ts +++ b/src/core/waitForDestinationChainTransaction.ts @@ -2,23 +2,40 @@ import type { ExtendedChain, ExtendedTransactionInfo, FullStatusData, - ProcessType, } from '@lifi/types' import { LiFiErrorCode } from '../errors/constants.js' import { getTransactionFailedMessage } from '../utils/getTransactionMessage.js' import type { StatusManager } from './StatusManager.js' -import type { LiFiStepExtended } from './types.js' +import type { LiFiStepExtended, Process } from './types.js' import { waitForTransactionStatus } from './waitForTransactionStatus.js' export async function waitForDestinationChainTransaction( step: LiFiStepExtended, - processType: ProcessType, - transactionHash: string, + process: Process, + fromChain: ExtendedChain, toChain: ExtendedChain, statusManager: StatusManager, pollingInterval?: number ): Promise { + const transactionHash = process.txHash + let processType = process.type try { + // Wait for the transaction status on the destination chain + if (!transactionHash) { + throw new Error('Transaction hash is undefined.') + } + + const isBridgeExecution = fromChain.id !== toChain.id + if (isBridgeExecution) { + const receivingChainProcess = statusManager.findOrCreateProcess({ + step, + type: 'RECEIVING_CHAIN', + status: 'PENDING', + chainId: toChain.id, + }) + processType = receivingChainProcess.type + } + const statusResponse = (await waitForTransactionStatus( transactionHash, statusManager, diff --git a/src/core/waitForTransactionStatus.ts b/src/core/waitForTransactionStatus.ts index a3c4eac6..cf04c697 100644 --- a/src/core/waitForTransactionStatus.ts +++ b/src/core/waitForTransactionStatus.ts @@ -1,14 +1,10 @@ -import type { - FullStatusData, - LiFiStep, - ProcessType, - StatusResponse, -} from '@lifi/types' +import type { FullStatusData, LiFiStep, StatusResponse } from '@lifi/types' import { ServerError } from '../errors/errors.js' import { getStatus } from '../services/api.js' import { waitForResult } from '../utils/waitForResult.js' import type { StatusManager } from './StatusManager.js' import { getSubstatusMessage } from './processMessages.js' +import type { ProcessType } from './types.js' const TRANSACTION_HASH_OBSERVERS: Record> = {} @@ -49,7 +45,9 @@ export async function waitForTransactionStatus( } }) .catch((e) => { - console.debug('Fetching status from backend failed.', e) + if (process.env.NODE_ENV === 'development') { + console.debug('Fetching status from backend failed.', e) + } return undefined }) } @@ -64,7 +62,9 @@ export async function waitForTransactionStatus( const resolvedStatus = await status if (!('receiving' in resolvedStatus)) { - throw new ServerError("Status doesn't contain receiving information.") + throw new ServerError( + "Status doesn't contain destination chain information." + ) } return resolvedStatus diff --git a/src/createConfig.ts b/src/createConfig.ts index b26096a2..71784471 100644 --- a/src/createConfig.ts +++ b/src/createConfig.ts @@ -1,8 +1,8 @@ import { ChainType } from '@lifi/types' import { config } from './config.js' -import { checkPackageUpdates } from './helpers.js' import { getChains } from './services/api.js' import type { SDKConfig } from './types/internal.js' +import { checkPackageUpdates } from './utils/checkPackageUpdates.js' import { name, version } from './version.js' function createBaseConfig(options: SDKConfig) { diff --git a/src/errors/SDKError.ts b/src/errors/SDKError.ts index c8b16329..2dc29fb8 100644 --- a/src/errors/SDKError.ts +++ b/src/errors/SDKError.ts @@ -1,4 +1,5 @@ -import type { LiFiStep, Process } from '@lifi/types' +import type { LiFiStep } from '@lifi/types' +import type { Process } from '../core/types.js' import { version } from '../version.js' import type { BaseError } from './baseError.js' import type { ErrorCode } from './constants.js' diff --git a/src/errors/constants.ts b/src/errors/constants.ts index 8675b685..a4804c1f 100644 --- a/src/errors/constants.ts +++ b/src/errors/constants.ts @@ -35,6 +35,7 @@ export enum LiFiErrorCode { TransactionExpired = 1018, TransactionSimulationFailed = 1019, TransactionConflict = 1020, + TransactionNotFound = 1021, } export enum ErrorMessage { diff --git a/src/helper.unit.spec.ts b/src/helper.unit.spec.ts deleted file mode 100644 index ef95cad0..00000000 --- a/src/helper.unit.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { - type Mock, - afterEach, - beforeEach, - describe, - expect, - it, - vi, -} from 'vitest' -import { checkPackageUpdates } from './helpers.js' - -const latestVersion = '2.5.6' - -describe('helpers', () => { - describe('checkPackageUpdates', () => { - beforeEach(() => { - vi.spyOn(global, 'fetch').mockResolvedValue({ - json: () => Promise.resolve({ version: latestVersion }), - } as Response) - - vi.spyOn(console, 'warn').mockImplementation(() => {}) - }) - - afterEach(() => { - vi.resetAllMocks() - }) - - it('should be able to check the version number against npm', async () => { - const packageName = '@lifi/sdk' - const currentVersion = '0.0.0' - - await checkPackageUpdates(packageName, currentVersion) - - expect(global.fetch as Mock).toBeCalledWith( - `https://registry.npmjs.org/${packageName}/latest` - ) - - expect(console.warn).toBeCalledWith( - `${packageName}: new package version is available. Please update as soon as possible to enjoy the newest features. Current version: ${currentVersion}. Latest version: ${latestVersion}.` - ) - }) - - it('should not report if version matchs the latest on npm', async () => { - const packageName = '@lifi/sdk' - const currentVersion = '2.5.6' - - await checkPackageUpdates(packageName, currentVersion) - - expect(global.fetch as Mock).toBeCalledWith( - `https://registry.npmjs.org/${packageName}/latest` - ) - - expect(console.warn).not.toBeCalled() - }) - - it('should fail sliently if it encounters a problem', async () => { - vi.spyOn(global, 'fetch').mockRejectedValue({ - json: () => Promise.resolve({ version: latestVersion }), - } as Response) - - const packageName = '@lifi/sdk' - const currentVersion = '0.0.0' - - await checkPackageUpdates(packageName, currentVersion) - - expect(global.fetch as Mock).toBeCalledWith( - `https://registry.npmjs.org/${packageName}/latest` - ) - - expect(console.warn).not.toBeCalled() - }) - }) -}) diff --git a/src/helpers.ts b/src/helpers.ts deleted file mode 100644 index 5f8a0f3d..00000000 --- a/src/helpers.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { LiFiStep, Route } from '@lifi/types' -import { SDKError } from './errors/SDKError.js' -import { ValidationError } from './errors/errors.js' -import { name, version } from './version.js' - -export const checkPackageUpdates = async ( - packageName?: string, - packageVersion?: string -) => { - try { - const pkgName = packageName ?? name - const response = await fetch(`https://registry.npmjs.org/${pkgName}/latest`) - const reponseBody = await response.json() - const latestVersion = reponseBody.version - const currentVersion = packageVersion ?? version - - if (latestVersion > currentVersion) { - console.warn( - `${pkgName}: new package version is available. Please update as soon as possible to enjoy the newest features. Current version: ${currentVersion}. Latest version: ${latestVersion}.` - ) - } - } catch (_error) { - // Cannot verify version, might be network error etc. We don't bother showing anything in that case - } -} - -/** - * Converts a quote to Route - * @param step - Step returned from the quote endpoint. - * @param txHash - * @param chainId - * @returns - The route to be executed. - * @throws {BaseError} Throws a ValidationError if the step has missing values. - */ -export const convertQuoteToRoute = (step: LiFiStep): Route => { - if (!step.estimate.fromAmountUSD) { - throw new SDKError( - new ValidationError("Missing 'fromAmountUSD' in step estimate.") - ) - } - - if (!step.estimate.toAmountUSD) { - throw new SDKError( - new ValidationError("Missing 'toAmountUSD' in step estimate.") - ) - } - - const route: Route = { - fromToken: step.action.fromToken, - toToken: step.action.toToken, - fromAmount: step.action.fromAmount, - toAmount: step.estimate.toAmount, - id: step.id, - fromChainId: step.action.fromToken.chainId, - toChainId: step.action.toToken.chainId, - fromAmountUSD: step.estimate.fromAmountUSD, - toAmountUSD: step.estimate.toAmountUSD, - steps: [step], - toAmountMin: step.estimate.toAmountMin, - insurance: { state: 'NOT_INSURABLE', feeAmountUsd: '0' }, - } - - return route -} - -export const fetchTxErrorDetails = async (txHash: string, chainId: number) => { - try { - const response = await fetch( - `https://api.tenderly.co/api/v1/public-contract/${chainId}/tx/${txHash}` - ) - const reponseBody = await response.json() - - return reponseBody - } catch (_) {} -} diff --git a/src/index.ts b/src/index.ts index 3294e6f0..8de86443 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,12 +12,11 @@ export { setTokenAllowance, } from './core/EVM/setAllowance.js' export { isEVM } from './core/EVM/types.js' +export { isExtendedChain, convertExtendedChain } from './core/EVM/utils.js' +export { isRelayerStep } from './core/EVM/typeguards.js' export type { EVMProvider, EVMProviderOptions, - MultisigConfig, - MultisigTransaction, - MultisigTxDetails, } from './core/EVM/types.js' export { StatusManager } from './core/StatusManager.js' export { @@ -33,9 +32,14 @@ export type { AcceptSlippageUpdateHook, AcceptSlippageUpdateHookParams, ExchangeRateUpdateParams, + Execution, ExecutionOptions, + ExecutionStatus, InteractionSettings, LiFiStepExtended, + Process, + ProcessStatus, + ProcessType, RouteExecutionData, RouteExecutionDataDictionary, RouteExecutionDictionary, @@ -65,17 +69,16 @@ export { UTXO } from './core/UTXO/UTXO.js' export { isUTXO } from './core/UTXO/types.js' export type { UTXOProvider, UTXOProviderOptions } from './core/UTXO/types.js' export { createConfig } from './createConfig.js' -export { - checkPackageUpdates, - convertQuoteToRoute, - fetchTxErrorDetails, -} from './helpers.js' +export { checkPackageUpdates } from './utils/checkPackageUpdates.js' +export { convertQuoteToRoute } from './utils/convertQuoteToRoute.js' +export { fetchTxErrorDetails } from './utils/fetchTxErrorDetails.js' export { getChains, getConnections, getContractCallsQuote, getGasRecommendation, getQuote, + getRelayerQuote, getRoutes, getStatus, getStepTransaction, diff --git a/src/services/api.int.spec.ts b/src/services/api.int.spec.ts index 7c37f0d1..ff3b56aa 100644 --- a/src/services/api.int.spec.ts +++ b/src/services/api.int.spec.ts @@ -2,20 +2,16 @@ import { describe, expect, it } from 'vitest' import { getQuote } from './api.js' describe('ApiService Integration Tests', () => { - it( - 'should successfully request a quote', - async () => { - const quote = await getQuote({ - fromChain: '1', - fromToken: '0x0000000000000000000000000000000000000000', - fromAddress: '0x552008c0f6870c2f77e5cC1d2eb9bdff03e30Ea0', - fromAmount: '1000000000000000000', - toChain: '137', - toToken: '0x0000000000000000000000000000000000000000', - // allowBridges: ['hop', 'multichain'], - }) - expect(quote).toBeDefined() - }, - { timeout: 100000 } - ) + it('should successfully request a quote', async () => { + const quote = await getQuote({ + fromChain: '1', + fromToken: '0x0000000000000000000000000000000000000000', + fromAddress: '0x552008c0f6870c2f77e5cC1d2eb9bdff03e30Ea0', + fromAmount: '1000000000000000000', + toChain: '137', + toToken: '0x0000000000000000000000000000000000000000', + // allowBridges: ['hop', 'multichain'], + }) + expect(quote).toBeDefined() + }, 100000) }) diff --git a/src/services/api.ts b/src/services/api.ts index 95888ad0..aa104789 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -26,44 +26,56 @@ import { isContractCallsRequestWithFromAmount, isContractCallsRequestWithToAmount, } from '@lifi/types' +import type { Address, Hash, Hex } from 'viem' import { config } from '../config.js' +import type { PermitData } from '../core/EVM/permit2/domain.js' +import type { + PermitTransferFrom, + Witness, +} from '../core/EVM/permit2/signatureTransfer.js' import { SDKError } from '../errors/SDKError.js' import { ValidationError } from '../errors/errors.js' import { request } from '../request.js' import { isRoutesRequest, isStep } from '../typeguards.js' import { withDedupe } from '../utils/withDedupe.js' -/** - * Fetch information about a Token - * @param chain - Id or key of the chain that contains the token - * @param token - Address or symbol of the token on the requested chain - * @param options - Request options - * @throws {LiFiError} - Throws a LiFiError if request fails - * @returns Token information - */ -export const getToken = async ( - chain: ChainKey | ChainId, - token: string, - options?: RequestOptions -): Promise => { - if (!chain) { - throw new SDKError( - new ValidationError('Required parameter "chain" is missing.') - ) - } - if (!token) { - throw new SDKError( - new ValidationError('Required parameter "token" is missing.') - ) + +interface TaskStatus { + data: { + status: 'DONE' | 'PENDING' | 'FAILED' + message?: string + metadata: { chainId: number; txHash?: Hash } + transactionStatus?: StatusResponse } - return await request( - `${config.get().apiUrl}/token?${new URLSearchParams({ - chain, - token, - } as Record)}`, - { - signal: options?.signal, +} + +interface RelayStatusRequest { + taskId: Hash +} + +interface RelayRequest { + tokenOwner: Address + chainId: number + permit: PermitTransferFrom + witness: Witness + signedPermitData: Hex + callData: string +} + +interface RelayResponse { + data: { taskId: Hash } +} + +interface RelayerQuoteResponse { + data: { + quote: { + step: LiFiStep + permit: PermitTransferFrom + witness: Witness + permitData: PermitData + tokenOwner: Address + chainId: number } - ) + } } /** @@ -124,6 +136,38 @@ export const getQuote = async ( ) } +/** + * Get a set of routes for a request that describes a transfer of tokens. + * @param params - A description of the transfer. + * @param options - Request options + * @returns The resulting routes that can be used to realize the described transfer of tokens. + * @throws {LiFiError} Throws a LiFiError if request fails. + */ +export const getRoutes = async ( + params: RoutesRequest, + options?: RequestOptions +): Promise => { + if (!isRoutesRequest(params)) { + throw new SDKError(new ValidationError('Invalid routes request.')) + } + const _config = config.get() + // apply defaults + params.options = { + integrator: _config.integrator, + ..._config.routeOptions, + ...params.options, + } + + return await request(`${_config.apiUrl}/advanced/routes`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + signal: options?.signal, + }) +} + /** * Get a quote for a destination contract call * @param params - The configuration of the requested destination call @@ -187,6 +231,35 @@ export const getContractCallsQuote = async ( }) } +/** + * Get the transaction data for a single step of a route + * @param step - The step object. + * @param options - Request options + * @returns The step populated with the transaction data. + * @throws {LiFiError} Throws a LiFiError if request fails. + */ +export const getStepTransaction = async ( + step: LiFiStep, + options?: RequestOptions +): Promise => { + if (!isStep(step)) { + // While the validation fails for some users we should not enforce it + console.warn('SDK Validation: Invalid Step', step) + } + + return await request( + `${config.get().apiUrl}/advanced/stepTransaction`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(step), + signal: options?.signal, + } + ) +} + /** * Check the status of a transfer. For cross chain transfers, the "bridge" parameter is required. * @param params - Configuration of the requested status @@ -215,125 +288,165 @@ export const getStatus = async ( } /** - * Get all available chains - * @param params - The configuration of the requested chains + * Get a relayer quote for a token transfer + * @param params - The configuration of the requested quote * @param options - Request options - * @returns A list of all available chains - * @throws {LiFiError} Throws a LiFiError if request fails. + * @throws {LiFiError} - Throws a LiFiError if request fails + * @returns Relayer quote for a token transfer */ -export const getChains = async ( - params?: ChainsRequest, +export const getRelayerQuote = async ( + params: QuoteRequest, options?: RequestOptions -): Promise => { - if (params) { - for (const key of Object.keys(params)) { - if (!params[key as keyof ChainsRequest]) { - delete params[key as keyof ChainsRequest] - } +): Promise => { + const requiredParameters: Array = [ + 'fromChain', + 'fromToken', + 'fromAddress', + 'fromAmount', + 'toChain', + 'toToken', + ] + for (const requiredParameter of requiredParameters) { + if (!params[requiredParameter]) { + throw new SDKError( + new ValidationError( + `Required parameter "${requiredParameter}" is missing.` + ) + ) } } - const urlSearchParams = new URLSearchParams( - params as Record - ).toString() - const response = await withDedupe( - () => - request( - `${config.get().apiUrl}/chains?${urlSearchParams}`, - { - signal: options?.signal, - } - ), - { id: `${getChains.name}.${urlSearchParams}` } + const _config = config.get() + // apply defaults + params.integrator ??= _config.integrator + params.order ??= _config.routeOptions?.order + params.slippage ??= _config.routeOptions?.slippage + params.referrer ??= _config.routeOptions?.referrer + params.fee ??= _config.routeOptions?.fee + params.allowBridges ??= _config.routeOptions?.bridges?.allow + params.denyBridges ??= _config.routeOptions?.bridges?.deny + params.preferBridges ??= _config.routeOptions?.bridges?.prefer + params.allowExchanges ??= _config.routeOptions?.exchanges?.allow + params.denyExchanges ??= _config.routeOptions?.exchanges?.deny + params.preferExchanges ??= _config.routeOptions?.exchanges?.prefer + + for (const key of Object.keys(params)) { + if (!params[key as keyof QuoteRequest]) { + delete params[key as keyof QuoteRequest] + } + } + + return await request( + `${config.get().apiUrl}/relayer/quote?${new URLSearchParams( + params as unknown as Record + )}`, + { + signal: options?.signal, + } ) - return response.chains } /** - * Get a set of routes for a request that describes a transfer of tokens. - * @param params - A description of the transfer. + * Relay a transaction through the relayer service + * @param params - The configuration for the relay request * @param options - Request options - * @returns The resulting routes that can be used to realize the described transfer of tokens. - * @throws {LiFiError} Throws a LiFiError if request fails. + * @throws {LiFiError} - Throws a LiFiError if request fails + * @returns Task ID for the relayed transaction */ -export const getRoutes = async ( - params: RoutesRequest, +export const relayTransaction = async ( + params: RelayRequest, options?: RequestOptions -): Promise => { - if (!isRoutesRequest(params)) { - throw new SDKError(new ValidationError('Invalid routes request.')) - } - const _config = config.get() - // apply defaults - params.options = { - integrator: _config.integrator, - ..._config.routeOptions, - ...params.options, +): Promise => { + const requiredParameters: Array = [ + 'tokenOwner', + 'chainId', + 'permit', + 'witness', + 'signedPermitData', + 'callData', + ] + + for (const requiredParameter of requiredParameters) { + if (!params[requiredParameter]) { + throw new SDKError( + new ValidationError( + `Required parameter "${requiredParameter}" is missing.` + ) + ) + } } - return await request(`${_config.apiUrl}/advanced/routes`, { + return await request(`${config.get().apiUrl}/relayer/relay`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify(params), + body: JSON.stringify(params, (_, value) => { + if (typeof value === 'bigint') { + return value.toString() + } + return value + }), signal: options?.signal, }) } /** - * Get the transaction data for a single step of a route - * @param step - The step object. + * Get the status of a relayed transaction + * @param params - Parameters for the relay status request * @param options - Request options - * @returns The step populated with the transaction data. - * @throws {LiFiError} Throws a LiFiError if request fails. + * @throws {LiFiError} - Throws a LiFiError if request fails + * @returns Status of the relayed transaction */ -export const getStepTransaction = async ( - step: LiFiStep, +export const getRelayedTransactionStatus = async ( + params: RelayStatusRequest, options?: RequestOptions -): Promise => { - if (!isStep(step)) { - // While the validation fails for some users we should not enforce it - console.warn('SDK Validation: Invalid Step', step) +): Promise => { + if (!params.taskId) { + throw new SDKError( + new ValidationError('Required parameter "taskId" is missing.') + ) } - return await request( - `${config.get().apiUrl}/advanced/stepTransaction`, + return await request( + `${config.get().apiUrl}/relayer/status/${params.taskId}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(step), signal: options?.signal, } ) } /** - * Get the available tools to bridge and swap tokens. - * @param params - The configuration of the requested tools + * Get all available chains + * @param params - The configuration of the requested chains * @param options - Request options - * @returns The tools that are available on the requested chains + * @returns A list of all available chains + * @throws {LiFiError} Throws a LiFiError if request fails. */ -export const getTools = async ( - params?: ToolsRequest, +export const getChains = async ( + params?: ChainsRequest, options?: RequestOptions -): Promise => { +): Promise => { if (params) { for (const key of Object.keys(params)) { - if (!params[key as keyof ToolsRequest]) { - delete params[key as keyof ToolsRequest] + if (!params[key as keyof ChainsRequest]) { + delete params[key as keyof ChainsRequest] } } } - return await request( - `${config.get().apiUrl}/tools?${new URLSearchParams( - params as Record - )}`, - { - signal: options?.signal, - } + const urlSearchParams = new URLSearchParams( + params as Record + ).toString() + const response = await withDedupe( + () => + request( + `${config.get().apiUrl}/chains?${urlSearchParams}`, + { + signal: options?.signal, + } + ), + { id: `${getChains.name}.${urlSearchParams}` } ) + return response.chains } /** @@ -369,6 +482,67 @@ export const getTokens = async ( return response } +/** + * Fetch information about a Token + * @param chain - Id or key of the chain that contains the token + * @param token - Address or symbol of the token on the requested chain + * @param options - Request options + * @throws {LiFiError} - Throws a LiFiError if request fails + * @returns Token information + */ +export const getToken = async ( + chain: ChainKey | ChainId, + token: string, + options?: RequestOptions +): Promise => { + if (!chain) { + throw new SDKError( + new ValidationError('Required parameter "chain" is missing.') + ) + } + if (!token) { + throw new SDKError( + new ValidationError('Required parameter "token" is missing.') + ) + } + return await request( + `${config.get().apiUrl}/token?${new URLSearchParams({ + chain, + token, + } as Record)}`, + { + signal: options?.signal, + } + ) +} + +/** + * Get the available tools to bridge and swap tokens. + * @param params - The configuration of the requested tools + * @param options - Request options + * @returns The tools that are available on the requested chains + */ +export const getTools = async ( + params?: ToolsRequest, + options?: RequestOptions +): Promise => { + if (params) { + for (const key of Object.keys(params)) { + if (!params[key as keyof ToolsRequest]) { + delete params[key as keyof ToolsRequest] + } + } + } + return await request( + `${config.get().apiUrl}/tools?${new URLSearchParams( + params as Record + )}`, + { + signal: options?.signal, + } + ) +} + /** * Get gas recommendation for a certain chain * @param params - Configuration of the requested gas recommendation. diff --git a/src/utils/checkPackageUpdates.ts b/src/utils/checkPackageUpdates.ts new file mode 100644 index 00000000..d943e1e9 --- /dev/null +++ b/src/utils/checkPackageUpdates.ts @@ -0,0 +1,22 @@ +import { name, version } from '../version.js' + +export const checkPackageUpdates = async ( + packageName?: string, + packageVersion?: string +) => { + try { + const pkgName = packageName ?? name + const response = await fetch(`https://registry.npmjs.org/${pkgName}/latest`) + const reponseBody = await response.json() + const latestVersion = reponseBody.version + const currentVersion = packageVersion ?? version + + if (latestVersion > currentVersion) { + console.warn( + `${pkgName}: new package version is available. Please update as soon as possible to enjoy the newest features. Current version: ${currentVersion}. Latest version: ${latestVersion}.` + ) + } + } catch (_error) { + // Cannot verify version, might be network error etc. We don't bother showing anything in that case + } +} diff --git a/src/utils/checkPackageUpdates.unit.spec.ts b/src/utils/checkPackageUpdates.unit.spec.ts new file mode 100644 index 00000000..b65102e0 --- /dev/null +++ b/src/utils/checkPackageUpdates.unit.spec.ts @@ -0,0 +1,71 @@ +import { + type Mock, + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from 'vitest' +import { checkPackageUpdates } from './checkPackageUpdates.js' + +const latestVersion = '2.5.6' + +describe('checkPackageUpdates', () => { + beforeEach(() => { + vi.spyOn(global, 'fetch').mockResolvedValue({ + json: () => Promise.resolve({ version: latestVersion }), + } as Response) + + vi.spyOn(console, 'warn').mockImplementation(() => {}) + }) + + afterEach(() => { + vi.resetAllMocks() + }) + + it('should be able to check the version number against npm', async () => { + const packageName = '@lifi/sdk' + const currentVersion = '0.0.0' + + await checkPackageUpdates(packageName, currentVersion) + + expect(global.fetch as Mock).toBeCalledWith( + `https://registry.npmjs.org/${packageName}/latest` + ) + + expect(console.warn).toBeCalledWith( + `${packageName}: new package version is available. Please update as soon as possible to enjoy the newest features. Current version: ${currentVersion}. Latest version: ${latestVersion}.` + ) + }) + + it('should not report if version matchs the latest on npm', async () => { + const packageName = '@lifi/sdk' + const currentVersion = '2.5.6' + + await checkPackageUpdates(packageName, currentVersion) + + expect(global.fetch as Mock).toBeCalledWith( + `https://registry.npmjs.org/${packageName}/latest` + ) + + expect(console.warn).not.toBeCalled() + }) + + it('should fail sliently if it encounters a problem', async () => { + vi.spyOn(global, 'fetch').mockRejectedValue({ + json: () => Promise.resolve({ version: latestVersion }), + } as Response) + + const packageName = '@lifi/sdk' + const currentVersion = '0.0.0' + + await checkPackageUpdates(packageName, currentVersion) + + expect(global.fetch as Mock).toBeCalledWith( + `https://registry.npmjs.org/${packageName}/latest` + ) + + expect(console.warn).not.toBeCalled() + }) +}) diff --git a/src/utils/convertQuoteToRoute.ts b/src/utils/convertQuoteToRoute.ts new file mode 100644 index 00000000..756478d7 --- /dev/null +++ b/src/utils/convertQuoteToRoute.ts @@ -0,0 +1,45 @@ +import type { LiFiStep, Route } from '@lifi/types' +import { SDKError } from '../errors/SDKError.js' +import { ValidationError } from '../errors/errors.js' + +/** + * Converts a quote to Route + * @param quote - Step returned from the quote endpoint. + * @param txHash + * @param chainId + * @returns - The route to be executed. + * @throws {BaseError} Throws a ValidationError if the step has missing values. + */ +export const convertQuoteToRoute = (quote: LiFiStep): Route => { + if (!quote.estimate.fromAmountUSD) { + throw new SDKError( + new ValidationError("Missing 'fromAmountUSD' in step estimate.") + ) + } + + if (!quote.estimate.toAmountUSD) { + throw new SDKError( + new ValidationError("Missing 'toAmountUSD' in step estimate.") + ) + } + + const route: Route = { + id: quote.id, + fromChainId: quote.action.fromToken.chainId, + fromToken: quote.action.fromToken, + fromAmount: quote.action.fromAmount, + fromAmountUSD: quote.estimate.fromAmountUSD, + fromAddress: quote.action.fromAddress, + toChainId: quote.action.toToken.chainId, + toToken: quote.action.toToken, + toAmount: quote.estimate.toAmount, + toAmountMin: quote.estimate.toAmountMin, + toAmountUSD: quote.estimate.toAmountUSD, + toAddress: quote.action.toAddress || quote.action.fromAddress, + gasCostUSD: quote.estimate.gasCosts?.[0].amountUSD, + steps: [quote], + insurance: { state: 'NOT_INSURABLE', feeAmountUsd: '0' }, + } + + return route +} diff --git a/src/utils/fetchTxErrorDetails.ts b/src/utils/fetchTxErrorDetails.ts new file mode 100644 index 00000000..9ca3b0c1 --- /dev/null +++ b/src/utils/fetchTxErrorDetails.ts @@ -0,0 +1,10 @@ +export const fetchTxErrorDetails = async (txHash: string, chainId: number) => { + try { + const response = await fetch( + `https://api.tenderly.co/api/v1/public-contract/${chainId}/tx/${txHash}` + ) + const reponseBody = await response.json() + + return reponseBody + } catch (_) {} +} diff --git a/src/utils/getTransactionMessage.ts b/src/utils/getTransactionMessage.ts index 3c8a039e..4ec46cd5 100644 --- a/src/utils/getTransactionMessage.ts +++ b/src/utils/getTransactionMessage.ts @@ -1,6 +1,7 @@ -import type { LiFiStep, Process } from '@lifi/types' +import type { LiFiStep } from '@lifi/types' import { formatUnits } from 'viem' import { config } from '../config.js' +import type { Process } from '../core/types.js' export const getTransactionNotSentMessage = async ( step?: LiFiStep, diff --git a/src/utils/invariant.ts b/src/utils/invariant.ts new file mode 100644 index 00000000..a66ed496 --- /dev/null +++ b/src/utils/invariant.ts @@ -0,0 +1,51 @@ +// https://github.com/alexreardon/tiny-invariant + +const isProduction: boolean = process.env.NODE_ENV === 'production' +const prefix: string = 'Invariant failed' + +/** + * `invariant` is used to [assert](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions) that the `condition` is [truthy](https://github.com/getify/You-Dont-Know-JS/blob/bdbe570600d4e1107d0b131787903ca1c9ec8140/up%20%26%20going/ch2.md#truthy--falsy). + * + * 💥 `invariant` will `throw` an `Error` if the `condition` is [falsey](https://github.com/getify/You-Dont-Know-JS/blob/bdbe570600d4e1107d0b131787903ca1c9ec8140/up%20%26%20going/ch2.md#truthy--falsy) + * + * 🤏 `message`s are not displayed in production environments to help keep bundles small + * + * @example + * + * ```ts + * const value: Person | null = { name: 'Alex' }; + * invariant(value, 'Expected value to be a person'); + * // type of `value`` has been narrowed to `Person` + * ``` + */ +export function invariant( + condition: any, + // Not providing an inline default argument for message as the result is smaller + /** + * Can provide a string, or a function that returns a string for cases where + * the message takes a fair amount of effort to compute + */ + message?: string | (() => string) +): asserts condition { + if (condition) { + return + } + // Condition not passed + + // In production we strip the message but still throw + if (isProduction) { + throw new Error(prefix) + } + + // When not in production we allow the message to pass through + // *This block will be removed in production builds* + + const provided: string | undefined = + typeof message === 'function' ? message() : message + + // Options: + // 1. message provided: `${prefix}: ${provided}` + // 2. message not provided: prefix + const value: string = provided ? `${prefix}: ${provided}` : prefix + throw new Error(value) +} diff --git a/src/utils/waitForResult.ts b/src/utils/waitForResult.ts index b28d8b4c..954e1529 100644 --- a/src/utils/waitForResult.ts +++ b/src/utils/waitForResult.ts @@ -4,18 +4,37 @@ import { sleep } from './sleep.js' * Repeatedly calls a given asynchronous function until it resolves with a value * @param fn The function that should be repeated * @param interval The timeout in milliseconds between retries, defaults to 5000 + * @param maxRetries Maximum number of retries before throwing an error, defaults to 3 + * @param shouldRetry Optional predicate to determine if an error should trigger a retry * @returns The result of the fn function + * @throws Error if maximum retries is reached, if function keeps returning undefined, or if shouldRetry returns false */ export const waitForResult = async ( fn: () => Promise, - interval = 5000 + interval = 5000, + maxRetries = 3, + shouldRetry: (count: number, error: unknown) => boolean = () => true ): Promise => { let result: T | undefined + let attempts = 0 + while (!result) { - result = await fn() - if (!result) { + try { + result = await fn() + if (!result) { + await sleep(interval) + } + } catch (error) { + if (!shouldRetry(attempts, error)) { + throw error + } + attempts++ + if (attempts === maxRetries) { + throw error + } await sleep(interval) } } + return result } diff --git a/src/utils/waitForResult.unit.spec.ts b/src/utils/waitForResult.unit.spec.ts index c1f33e9f..0501a5ca 100644 --- a/src/utils/waitForResult.unit.spec.ts +++ b/src/utils/waitForResult.unit.spec.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { waitForResult } from './waitForResult.js' describe('utils', () => { @@ -7,24 +7,69 @@ describe('utils', () => { beforeEach(() => { mockedFunction = vi.fn() + vi.useFakeTimers() }) - //.mockImplementation(() => Promise.reject(new Error('some error'))) - it('should throw an error if repeat function fails', async () => { - mockedFunction.mockRejectedValue(new Error('some error')) + afterEach(() => { + vi.useRealTimers() + }) + + it('should throw immediately if shouldRetry returns false', async () => { + mockedFunction.mockImplementation(() => Promise.reject('some error')) + const shouldRetry = vi.fn().mockReturnValue(false) + + const promise = waitForResult(mockedFunction, 1000, 3, shouldRetry) - await expect(waitForResult(mockedFunction)).rejects.toThrow('some error') + await expect(promise).rejects.toThrowError('some error') + expect(mockedFunction).toHaveBeenCalledTimes(1) + expect(shouldRetry).toHaveBeenCalledWith(0, 'some error') }) it('should try until repeat function succeeds', async () => { mockedFunction .mockResolvedValueOnce(undefined) .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce('success!') + + const promise = waitForResult(mockedFunction, 1000) + + // Fast-forward through retries + for (let i = 0; i < 2; i++) { + await vi.advanceTimersByTimeAsync(1000) + } + + const result = await promise + expect(result).toEqual('success!') + expect(mockedFunction).toHaveBeenCalledTimes(3) + }) + + it('should respect the interval between retries', async () => { + mockedFunction .mockResolvedValueOnce(undefined) .mockResolvedValueOnce('success!') - const result = await waitForResult(mockedFunction, 10) + const promise = waitForResult(mockedFunction, 2000) + + await vi.advanceTimersByTimeAsync(2000) + const result = await promise + expect(result).toEqual('success!') + expect(mockedFunction).toHaveBeenCalledTimes(2) + }) + + it('should throw an error if repeat function fails and maxRetries is reached', async () => { + mockedFunction.mockImplementation(() => Promise.reject('some error')) + const maxRetries = 2 + + const promise = waitForResult(mockedFunction, 1000, maxRetries) + const expectPromise = expect(promise).rejects.toThrowError('some error') + // Fast-forward through retries + for (let i = 0; i < maxRetries - 1; i++) { + await vi.advanceTimersByTimeAsync(1000) + } + + await expectPromise + expect(mockedFunction).toHaveBeenCalledTimes(maxRetries) }) }) }) diff --git a/src/version.ts b/src/version.ts index b0abaa12..e4ce29f2 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,2 @@ export const name = '@lifi/sdk' -export const version = '3.5.3' +export const version = '3.6.0-beta.5' diff --git a/tsconfig.node.json b/tsconfig.node.json index ccf42994..b75a6e1c 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,9 +1,8 @@ { // This configuration is used for local development and type checking of configuration and script files that are not part of the build. - "include": [ - "vitest.config.ts" - ], + "include": ["vitest.config.ts"], "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "strict": true, "composite": true, "module": "ESNext",