diff --git a/package.json b/package.json index 783b201..13b3d75 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { - "name": "canon-vault-ui", + "name": "canon-guard-ui", "private": true, "version": "0.0.0", "type": "module", "license": "MIT", "author": "Wonderland", "engines": { - "node": ">=18.17.0" + "node": "22.x" }, "scripts": { "dev": "vite --port 3000", @@ -40,7 +40,11 @@ "@mui/material": "6.1.3", "@rainbow-me/rainbowkit": "2.2.5", "@tanstack/react-query": "5.80.3", + "@walletconnect/sign-client": "^2.23.1", + "@walletconnect/types": "^2.23.1", + "@walletconnect/utils": "^2.23.1", "lodash.merge": "4.6.2", + "lucide-react": "^0.562.0", "react": "19.1.0", "react-dom": "19.1.0", "react-router-dom": "7.6.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9a7322..aa8f824 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,13 +25,25 @@ importers: version: 6.1.3(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@rainbow-me/rainbowkit': specifier: 2.2.5 - version: 2.2.5(@tanstack/react-query@5.80.3(react@19.1.0))(@types/react@19.1.6)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(viem@2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4))(wagmi@2.15.5(@tanstack/query-core@5.80.2)(@tanstack/react-query@5.80.3(react@19.1.0))(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4))(zod@3.22.4)) + version: 2.2.5(@tanstack/react-query@5.80.3(react@19.1.0))(@types/react@19.1.6)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(viem@2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4))(wagmi@2.15.5(@tanstack/query-core@5.80.2)(@tanstack/react-query@5.80.3(react@19.1.0))(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4))(zod@3.22.4)) '@tanstack/react-query': specifier: 5.80.3 version: 5.80.3(react@19.1.0) + '@walletconnect/sign-client': + specifier: ^2.23.1 + version: 2.23.1(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@walletconnect/types': + specifier: ^2.23.1 + version: 2.23.1 + '@walletconnect/utils': + specifier: ^2.23.1 + version: 2.23.1(typescript@5.8.3)(zod@3.22.4) lodash.merge: specifier: 4.6.2 version: 4.6.2 + lucide-react: + specifier: ^0.562.0 + version: 0.562.0(react@19.1.0) react: specifier: 19.1.0 version: 19.1.0 @@ -43,10 +55,10 @@ importers: version: 7.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) viem: specifier: 2.30.6 - version: 2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + version: 2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) wagmi: specifier: 2.15.5 - version: 2.15.5(@tanstack/query-core@5.80.2)(@tanstack/react-query@5.80.3(react@19.1.0))(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4))(zod@3.22.4) + version: 2.15.5(@tanstack/query-core@5.80.2)(@tanstack/react-query@5.80.3(react@19.1.0))(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4))(zod@3.22.4) devDependencies: '@defi-wonderland/crypto-husky-checks': specifier: 1.5.6 @@ -62,7 +74,7 @@ importers: version: 1.53.0 '@synthetixio/synpress': specifier: 4.1.0 - version: 4.1.0(@depay/solana-web3.js@1.98.2)(@depay/web3-blockchains@9.8.6)(@playwright/test@1.53.0)(bufferutil@4.0.9)(ethers@5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(playwright-core@1.53.0)(postcss@8.5.4)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + version: 4.1.0(@depay/solana-web3.js@1.98.3)(@depay/web3-blockchains@9.8.10)(@playwright/test@1.53.0)(bufferutil@4.1.0)(ethers@5.8.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(playwright-core@1.53.0)(postcss@8.5.6)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) '@types/lodash.merge': specifier: 4.6.9 version: 4.6.9 @@ -77,7 +89,7 @@ importers: version: 19.1.6(@types/react@19.1.6) '@vitejs/plugin-react': specifier: 4.5.1 - version: 4.5.1(vite@6.3.5(@types/node@24.0.1)(terser@5.39.0)(yaml@2.8.0)) + version: 4.5.1(vite@6.3.5(@types/node@24.0.1)(yaml@2.8.2)) eslint: specifier: 9.28.0 version: 9.28.0 @@ -110,7 +122,7 @@ importers: version: 9.1.7 jsdom: specifier: 26.1.0 - version: 26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + version: 26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) prettier: specifier: 3.5.3 version: 3.5.3 @@ -122,64 +134,52 @@ importers: version: 8.33.1(eslint@9.28.0)(typescript@5.8.3) vite: specifier: 6.3.5 - version: 6.3.5(@types/node@24.0.1)(terser@5.39.0)(yaml@2.8.0) + version: 6.3.5(@types/node@24.0.1)(yaml@2.8.2) vitest: specifier: 3.2.1 - version: 3.2.1(@types/debug@4.1.12)(@types/node@24.0.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(terser@5.39.0)(yaml@2.8.0) + version: 3.2.1(@types/debug@4.1.12)(@types/node@24.0.1)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(yaml@2.8.2) packages: '@adraffy/ens-normalize@1.10.0': resolution: {integrity: sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==} - '@adraffy/ens-normalize@1.11.0': - resolution: {integrity: sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==} - - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} + '@adraffy/ens-normalize@1.11.1': + resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} - '@babel/code-frame@7.26.2': - resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} - engines: {node: '>=6.9.0'} - '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.27.5': - resolution: {integrity: sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==} + '@babel/compat-data@7.28.5': + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} engines: {node: '>=6.9.0'} - '@babel/core@7.27.4': - resolution: {integrity: sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==} + '@babel/core@7.28.5': + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} engines: {node: '>=6.9.0'} - '@babel/generator@7.26.9': - resolution: {integrity: sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.27.5': - resolution: {integrity: sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==} + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} '@babel/helper-compilation-targets@7.27.2': resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.25.9': - resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} engines: {node: '>=6.9.0'} '@babel/helper-module-imports@7.27.1': resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.27.3': - resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -188,37 +188,24 @@ packages: resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.25.9': - resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} - engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.25.9': - resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.27.4': - resolution: {integrity: sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==} + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} engines: {node: '>=6.9.0'} - '@babel/parser@7.26.9': - resolution: {integrity: sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/parser@7.27.5': - resolution: {integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==} + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -234,36 +221,20 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.26.10': - resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==} - engines: {node: '>=6.9.0'} - - '@babel/runtime@7.27.4': - resolution: {integrity: sha512-t3yaEOuGu9NlIZ+hIeGbBjFtZT7j2cb2tg0fuaJKeGotchRjjLfrBA9Kwf8quhpP1EUuxModQg04q/mBwyg8uA==} - engines: {node: '>=6.9.0'} - - '@babel/template@7.26.9': - resolution: {integrity: sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==} + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.26.9': - resolution: {integrity: sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.27.4': - resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.26.9': - resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==} + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.27.3': - resolution: {integrity: sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==} + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} '@coinbase/wallet-sdk@3.9.3': @@ -272,8 +243,8 @@ packages: '@coinbase/wallet-sdk@4.3.0': resolution: {integrity: sha512-T3+SNmiCw4HzDm4we9wCHCxlP0pqCiwKe4sOwPH3YAK2KSKjxPRydKu6UQJrdONFVLG7ujXvbd/6ZqmvJb8rkw==} - '@csstools/color-helpers@5.0.2': - resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} '@csstools/css-calc@2.1.4': @@ -283,8 +254,8 @@ packages: '@csstools/css-parser-algorithms': ^3.0.5 '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-color-parser@3.0.10': - resolution: {integrity: sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==} + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} engines: {node: '>=18'} peerDependencies: '@csstools/css-parser-algorithms': ^3.0.5 @@ -307,11 +278,11 @@ packages: resolution: {integrity: sha512-YvrAVALo/rckBUjHMJnbTrTORDxajXVQGKwFXHOqAZkbDeMcrUCzQ7fPQC94bk2XuGzpsVCVZEXX8beI1cFhOA==} hasBin: true - '@depay/solana-web3.js@1.98.2': - resolution: {integrity: sha512-O7SvHsZ6HGXlzSmjhj7mj0B/VvQQn8mzm/xKQ0SUrEUJVxg9zKFBlwIvxCtgf+IOrWlBJi6VqXRu7UznWvfrCA==} + '@depay/solana-web3.js@1.98.3': + resolution: {integrity: sha512-wxr+2gpjKRZ1eVBLhQYJxImDsRukk0DvCsEElkTMyybP+7SamWRs48o3DYE6VLEgQJFZgOoUec3t5FM5s1J1ww==} - '@depay/web3-blockchains@9.8.6': - resolution: {integrity: sha512-Fp2sGHjvRuqsLivfbL2qdhB447Fk/A0eS/iZj5VlzVnrpfa1Jt+tAKPk1c5T5tOYagnWaGG5DTFMuFNKiqRPkQ==} + '@depay/web3-blockchains@9.8.10': + resolution: {integrity: sha512-YMFnkp4ISflVHRkK5fp7vLlYwDuIkXiDb9ajFDWNu0z++dridBk+uRfgtKxZ8I6EH+9Lg02ORzb3wwr4OAiiBw==} engines: {node: '>=18'} '@depay/web3-client@10.18.6': @@ -334,8 +305,8 @@ packages: resolution: {integrity: sha512-2C44+G2dch4cB6zw7+oGQ9VcFQuuVhc5xOzfVvY7iUEj2PRhiVMIB6SpNMK1V5TvpdqrAqCYFjclK18Mh9vwNQ==} hasBin: true - '@ecies/ciphers@0.2.3': - resolution: {integrity: sha512-tapn6XhOueMwht3E2UzY0ZZjYokdaw9XtL9kEyjhQ/Fb9vL9xTFbOaI+fV0AWvTpYu4BNloC6getKW6NtSg4mA==} + '@ecies/ciphers@0.2.5': + resolution: {integrity: sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A==} engines: {bun: '>=1', deno: '>=2', node: '>=16'} peerDependencies: '@noble/ciphers': ^1.0.0 @@ -349,8 +320,8 @@ packages: '@emotion/hash@0.9.2': resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} - '@emotion/is-prop-valid@1.3.1': - resolution: {integrity: sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==} + '@emotion/is-prop-valid@1.4.0': + resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==} '@emotion/memoize@0.9.0': resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} @@ -406,8 +377,8 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.25.0': - resolution: {integrity: sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -424,8 +395,8 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.25.0': - resolution: {integrity: sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==} + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -442,8 +413,8 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.25.0': - resolution: {integrity: sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==} + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -460,8 +431,8 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.25.0': - resolution: {integrity: sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==} + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -478,8 +449,8 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.25.0': - resolution: {integrity: sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==} + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -496,8 +467,8 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.25.0': - resolution: {integrity: sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==} + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -514,8 +485,8 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.25.0': - resolution: {integrity: sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==} + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -532,8 +503,8 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.0': - resolution: {integrity: sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==} + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -550,8 +521,8 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.25.0': - resolution: {integrity: sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==} + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -568,8 +539,8 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.25.0': - resolution: {integrity: sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==} + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -586,8 +557,8 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.25.0': - resolution: {integrity: sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==} + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -604,8 +575,8 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.25.0': - resolution: {integrity: sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==} + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -622,8 +593,8 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.25.0': - resolution: {integrity: sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==} + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -640,8 +611,8 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.25.0': - resolution: {integrity: sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==} + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -658,8 +629,8 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.25.0': - resolution: {integrity: sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==} + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -676,8 +647,8 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.25.0': - resolution: {integrity: sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==} + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -694,14 +665,14 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.25.0': - resolution: {integrity: sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==} + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.0': - resolution: {integrity: sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==} + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -718,14 +689,14 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.0': - resolution: {integrity: sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==} + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.0': - resolution: {integrity: sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==} + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -742,12 +713,18 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.0': - resolution: {integrity: sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==} + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.19.12': resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} engines: {node: '>=12'} @@ -760,8 +737,8 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.25.0': - resolution: {integrity: sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==} + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -778,8 +755,8 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.25.0': - resolution: {integrity: sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==} + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -796,8 +773,8 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.25.0': - resolution: {integrity: sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==} + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -814,48 +791,52 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.25.0': - resolution: {integrity: sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==} + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.7.0': - resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.12.1': - resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.20.0': - resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} + '@eslint/config-array@0.20.1': + resolution: {integrity: sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.2.2': - resolution: {integrity: sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==} + '@eslint/config-helpers@0.2.3': + resolution: {integrity: sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/core@0.14.0': resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.1': - resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@9.28.0': resolution: {integrity: sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/object-schema@2.1.6': - resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.3.1': - resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==} + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ethereumjs/common@3.2.0': @@ -968,18 +949,14 @@ packages: resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} - '@humanfs/node@0.16.6': - resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/retry@0.3.1': - resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} - engines: {node: '>=18.18'} - '@humanwhocodes/retry@0.4.3': resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} @@ -988,32 +965,27 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - '@jridgewell/gen-mapping@0.3.8': - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} - engines: {node: '>=6.0.0'} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - - '@jridgewell/source-map@0.3.6': - resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} - - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@lit-labs/ssr-dom-shim@1.3.0': - resolution: {integrity: sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==} + '@lit-labs/ssr-dom-shim@1.5.0': + resolution: {integrity: sha512-HLomZXMmrCFHSRKESF5vklAKsDY7/fsT/ZhqCu3V0UoW/Qbv8wxmO4W9bx4KnCCF2Zak4yuk+AGraK/bPmI4kA==} - '@lit/reactive-element@2.1.0': - resolution: {integrity: sha512-L2qyoZSQClcBmq0qajBVbhYEcG6iK0XfLn66ifLe/RfC0/ihpc+pl0Wdn8bJ8o+hj38cG0fGXRgSS20MuXn7qA==} + '@lit/reactive-element@2.1.2': + resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==} '@metamask/eth-json-rpc-provider@1.0.1': resolution: {integrity: sha512-whiUMPlAOrVGmX8aKYVPvlKyG4CpQXiNNyt74vE1xb5sPvmx5oA7B/kOi/JdBvhGQq97U1/AVdXEdk2zkP8qyA==} @@ -1084,8 +1056,12 @@ packages: resolution: {integrity: sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==} engines: {node: '>=16.0.0'} - '@mui/core-downloads-tracker@6.4.12': - resolution: {integrity: sha512-M7IkG4LqSJfkY+thlQQHNkcS5NdmMDwLq/2RKoW40XR0mv/2BYb6X8fRnyaxP4zGdPD2M4MQdbzKihSVormJ7Q==} + '@msgpack/msgpack@3.1.2': + resolution: {integrity: sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ==} + engines: {node: '>= 18'} + + '@mui/core-downloads-tracker@6.5.0': + resolution: {integrity: sha512-LGb8t8i6M2ZtS3Drn3GbTI1DVhDY6FJ9crEey2lZ0aN2EMZo8IZBZj9wRf4vqbZHaWjsYgtbOnJw5V8UWbmK2Q==} '@mui/icons-material@6.1.3': resolution: {integrity: sha512-QBQCCIMSAv6IkArTg4Hg8q2sJRhHOci8oPAlkHWFlt2ghBdy3EqyLbIELLE/bhpqhX+E/ZkPYGIUQCd5/L0owA==} @@ -1128,8 +1104,8 @@ packages: '@types/react': optional: true - '@mui/styled-engine@6.4.11': - resolution: {integrity: sha512-74AUmlHXaGNbyUqdK/+NwDJOZqgRQw6BcNvhoWYLq3LGbLTkE+khaJ7soz6cIabE4CPYqO2/QAIU1Z/HEjjpcw==} + '@mui/styled-engine@6.5.0': + resolution: {integrity: sha512-8woC2zAqF4qUDSPIBZ8v3sakj+WgweolpyM/FXf8jAx6FMls+IE4Y8VDZc+zS805J7PRz31vz73n2SovKGaYgw==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.4.1 @@ -1141,8 +1117,8 @@ packages: '@emotion/styled': optional: true - '@mui/system@6.4.12': - resolution: {integrity: sha512-fgEfm1qxpKCztndESeL1L0sLwA2c7josZ2w42D8OM3pbLee4bH2twEjoMo6qf7z2rNw1Uc9EU9haXeMoq0oTdQ==} + '@mui/system@6.5.0': + resolution: {integrity: sha512-XcbBYxDS+h/lgsoGe78ExXFZXtuIlSBpn/KsZq8PtZcIkUNJInkuDqcLd2rVBQrDC1u+rvVovdaWPf2FHKJf3w==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -1165,8 +1141,8 @@ packages: '@types/react': optional: true - '@mui/types@7.4.3': - resolution: {integrity: sha512-2UCEiK29vtiZTeLdS2d4GndBKacVyxGvReznGXGr+CzW/YhjIX+OHUdCIczZjzcRAgKBGmE9zCIgoV9FleuyRQ==} + '@mui/types@7.4.9': + resolution: {integrity: sha512-dNO8Z9T2cujkSIaCnWwprfeKmTWh97cnjkgmpFJ2sbfXLx8SMZijCYHOtP/y5nnUb/Rm2omxbDMmtUoSaUtKaw==} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: @@ -1209,6 +1185,10 @@ packages: resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} engines: {node: ^14.21.3 || >=16} + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + '@noble/hashes@1.3.2': resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} engines: {node: '>= 16'} @@ -1249,8 +1229,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@pkgr/core@0.2.7': - resolution: {integrity: sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==} + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} '@playwright/test@1.53.0': @@ -1303,103 +1283,128 @@ packages: '@rolldown/pluginutils@1.0.0-beta.9': resolution: {integrity: sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==} - '@rollup/rollup-android-arm-eabi@4.41.1': - resolution: {integrity: sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==} + '@rollup/rollup-android-arm-eabi@4.55.1': + resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.41.1': - resolution: {integrity: sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==} + '@rollup/rollup-android-arm64@4.55.1': + resolution: {integrity: sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.41.1': - resolution: {integrity: sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==} + '@rollup/rollup-darwin-arm64@4.55.1': + resolution: {integrity: sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.41.1': - resolution: {integrity: sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==} + '@rollup/rollup-darwin-x64@4.55.1': + resolution: {integrity: sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.41.1': - resolution: {integrity: sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==} + '@rollup/rollup-freebsd-arm64@4.55.1': + resolution: {integrity: sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.41.1': - resolution: {integrity: sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==} + '@rollup/rollup-freebsd-x64@4.55.1': + resolution: {integrity: sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.41.1': - resolution: {integrity: sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==} + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': + resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.41.1': - resolution: {integrity: sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==} + '@rollup/rollup-linux-arm-musleabihf@4.55.1': + resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.41.1': - resolution: {integrity: sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==} + '@rollup/rollup-linux-arm64-gnu@4.55.1': + resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.41.1': - resolution: {integrity: sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==} + '@rollup/rollup-linux-arm64-musl@4.55.1': + resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.41.1': - resolution: {integrity: sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==} + '@rollup/rollup-linux-loong64-gnu@4.55.1': + resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.55.1': + resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.41.1': - resolution: {integrity: sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==} + '@rollup/rollup-linux-ppc64-gnu@4.55.1': + resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.41.1': - resolution: {integrity: sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==} + '@rollup/rollup-linux-ppc64-musl@4.55.1': + resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.55.1': + resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.41.1': - resolution: {integrity: sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==} + '@rollup/rollup-linux-riscv64-musl@4.55.1': + resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.41.1': - resolution: {integrity: sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==} + '@rollup/rollup-linux-s390x-gnu@4.55.1': + resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.41.1': - resolution: {integrity: sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==} + '@rollup/rollup-linux-x64-gnu@4.55.1': + resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.41.1': - resolution: {integrity: sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==} + '@rollup/rollup-linux-x64-musl@4.55.1': + resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.41.1': - resolution: {integrity: sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==} + '@rollup/rollup-openbsd-x64@4.55.1': + resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.55.1': + resolution: {integrity: sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.55.1': + resolution: {integrity: sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.41.1': - resolution: {integrity: sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==} + '@rollup/rollup-win32-ia32-msvc@4.55.1': + resolution: {integrity: sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.41.1': - resolution: {integrity: sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==} + '@rollup/rollup-win32-x64-gnu@4.55.1': + resolution: {integrity: sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.55.1': + resolution: {integrity: sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==} cpu: [x64] os: [win32] @@ -1416,8 +1421,8 @@ packages: resolution: {integrity: sha512-6ORQfwtEJYpalCeVO21L4XXGSdbEMfyp2hEv6cP82afKXSwvse6d3sdelgaPWUxHIsFRkWvHDdzh8IyyKHZKxw==} engines: {node: '>=16'} - '@safe-global/safe-smart-account@https://codeload.github.com/safe-global/safe-smart-account/tar.gz/b75e250': - resolution: {tarball: https://codeload.github.com/safe-global/safe-smart-account/tar.gz/b75e250} + '@safe-global/safe-smart-account@git+https://git@github.com:safe-global/safe-smart-account.git#b75e2509ba4968f97c427f2657996a0711b7a4cf': + resolution: {commit: b75e2509ba4968f97c427f2657996a0711b7a4cf, repo: git@github.com:safe-global/safe-smart-account.git, type: git} version: 1.4.1-build.0 '@scure/base@1.1.9': @@ -1502,11 +1507,11 @@ packages: '@types/babel__template@7.4.4': resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - '@types/babel__traverse@7.20.7': - resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/chai@5.2.2': - resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -1514,8 +1519,8 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - '@types/estree@1.0.7': - resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1526,8 +1531,8 @@ packages: '@types/lodash.merge@4.6.9': resolution: {integrity: sha512-23sHDPmzd59kUgWyKGiOMO2Qb9YtqRO/x4IhkgNUiPQ1+5MUVqi6bCZeq9nBJ17msjIMbEIO5u+XW4Kz6aGUhQ==} - '@types/lodash@4.17.16': - resolution: {integrity: sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==} + '@types/lodash@4.17.21': + resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==} '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -1538,8 +1543,8 @@ packages: '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} - '@types/prop-types@15.7.14': - resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} '@types/react-dom@19.1.6': resolution: {integrity: sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==} @@ -1625,8 +1630,8 @@ packages: '@vanilla-extract/dynamic@2.1.2': resolution: {integrity: sha512-9BGMciD8rO1hdSPIAh1ntsG4LPD3IYKhywR7VOmmz9OO4Lx1hlwkSg3E6X07ujFx7YuBfx0GDQnApG9ESHvB2A==} - '@vanilla-extract/private@1.0.7': - resolution: {integrity: sha512-v9Yb0bZ5H5Kr8ciwPXyEToOFD7J/fKKH93BYP7NCSZg02VYsA/pNFrLeVDJM2OO/vsygduPKuiEI6ORGQ4IcBw==} + '@vanilla-extract/private@1.0.9': + resolution: {integrity: sha512-gT2jbfZuaaCLrAxwXbRgIhGhcXbRZCG3v4TTUnjw0EJ7ArdBRxkq4msNJkbuRkCgfIK5ATmprB5t9ljvLeFDEA==} '@vanilla-extract/sprinkles@1.6.3': resolution: {integrity: sha512-oCHlQeYOBIJIA2yWy2GnY5wE2A7hGHDyJplJo4lb+KEIBcJWRnDJDg8ywDwQS5VfWJrBBO3drzYZPFpWQjAMiQ==} @@ -1659,6 +1664,9 @@ packages: '@vitest/pretty-format@3.2.1': resolution: {integrity: sha512-xBh1X2GPlOGBupp6E1RcUQWIxw0w/hRLd3XyBS6H+dMdKTAqHDNsIR2AnJwPA3yYe9DFy3VUKTe3VRTrAiQ01g==} + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + '@vitest/runner@3.2.1': resolution: {integrity: sha512-kygXhNTu/wkMYbwYpS3z/9tBe0O8qpdBuC3dD/AW9sWa0LE/DAZEjnHtWA9sIad7lpD4nFW1yQ+zN7mEKNH3yA==} @@ -1701,11 +1709,16 @@ packages: resolution: {integrity: sha512-Tp4MHJYcdWD846PH//2r+Mu4wz1/ZU/fr9av1UWFiaYQ2t2TPLDiZxjLw54AAEpMqlEHemwCgiRiAmjR1NDdTQ==} engines: {node: '>=18'} + '@walletconnect/core@2.23.1': + resolution: {integrity: sha512-fW48PIw41Q/LJW+q0msFogD/OcelkrrDONQMcpGw4C4Y6w+IvFKGEg+7dxGLKWx1g8QuHk/p6C9VEIV/tDsm5A==} + engines: {node: '>=18.20.8'} + '@walletconnect/environment@1.0.1': resolution: {integrity: sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg==} '@walletconnect/ethereum-provider@2.21.1': resolution: {integrity: sha512-SSlIG6QEVxClgl1s0LMk4xr2wg4eT3Zn/Hb81IocyqNSGfXpjtawWxKxiC5/9Z95f1INyBD6MctJbL/R1oBwIw==} + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' '@walletconnect/events@1.0.1': resolution: {integrity: sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ==} @@ -1739,6 +1752,9 @@ packages: '@walletconnect/logger@2.1.2': resolution: {integrity: sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw==} + '@walletconnect/logger@3.0.1': + resolution: {integrity: sha512-O8lXGMZO1+e5NtHhBSjsAih/I9KC+1BxNhGNGD+SIWTqWd0zsbT5wJtNnJ+LnSXTRE7XZRxFUlvZgkER3vlhFA==} + '@walletconnect/relay-api@1.0.11': resolution: {integrity: sha512-tLPErkze/HmC9aCmdZOhtVmYZq1wKfWTJtygQHoWtgg722Jd4homo54Cs4ak2RUFUZIGO2RsOpIcWipaua5D5Q==} @@ -1750,9 +1766,14 @@ packages: '@walletconnect/sign-client@2.21.0': resolution: {integrity: sha512-z7h+PeLa5Au2R591d/8ZlziE0stJvdzP9jNFzFolf2RG/OiXulgFKum8PrIyXy+Rg2q95U9nRVUF9fWcn78yBA==} + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' '@walletconnect/sign-client@2.21.1': resolution: {integrity: sha512-QaXzmPsMnKGV6tc4UcdnQVNOz4zyXgarvdIQibJ4L3EmLat73r5ZVl4c0cCOcoaV7rgM9Wbphgu5E/7jNcd3Zg==} + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' + + '@walletconnect/sign-client@2.23.1': + resolution: {integrity: sha512-x0sG8ZuuaOi3G/gYWLppf7nmNItWlV8Yga9Bltb46/Ve6G20nCBis6gcTVVeJOpnmqQ85FISwExqOYPmJ0FQlw==} '@walletconnect/time@1.0.2': resolution: {integrity: sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==} @@ -1763,11 +1784,16 @@ packages: '@walletconnect/types@2.21.1': resolution: {integrity: sha512-UeefNadqP6IyfwWC1Yi7ux+ljbP2R66PLfDrDm8izmvlPmYlqRerJWJvYO4t0Vvr9wrG4Ko7E0c4M7FaPKT/sQ==} + '@walletconnect/types@2.23.1': + resolution: {integrity: sha512-sbWOM9oCuzSbz/187rKWnSB3sy7FCFcbTQYeIJMc9+HTMTG2TUPftPCn8NnkfvmXbIeyLw00Y0KNvXoCV/eIeQ==} + '@walletconnect/universal-provider@2.21.0': resolution: {integrity: sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg==} + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' '@walletconnect/universal-provider@2.21.1': resolution: {integrity: sha512-Wjx9G8gUHVMnYfxtasC9poGm8QMiPCpXpbbLFT+iPoQskDDly8BwueWnqKs4Mx2SdIAWAwuXeZ5ojk5qQOxJJg==} + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' '@walletconnect/utils@2.21.0': resolution: {integrity: sha512-zfHLiUoBrQ8rP57HTPXW7rQMnYxYI4gT9yTACxVW6LhIFROTF6/ytm5SKNoIvi4a5nX5dfXG4D9XwQUCu8Ilig==} @@ -1775,6 +1801,9 @@ packages: '@walletconnect/utils@2.21.1': resolution: {integrity: sha512-VPZvTcrNQCkbGOjFRbC24mm/pzbRMUq2DSQoiHlhh0X1U7ZhuIrzVtAoKsrzu6rqjz0EEtGxCr3K1TGRqDG4NA==} + '@walletconnect/utils@2.23.1': + resolution: {integrity: sha512-J12DadZHIL0KvsUoQuK0rag9jDUy8qu1zwz47xEHl03LrMcgrotQiXvdTQ3uHwAVA4yKLTQB/LEI2JiTIt7X8Q==} + '@walletconnect/window-getters@1.0.1': resolution: {integrity: sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q==} @@ -1803,21 +1832,32 @@ packages: zod: optional: true + abitype@1.2.3: + resolution: {integrity: sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} hasBin: true aes-js@3.0.0: resolution: {integrity: sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==} - agent-base@7.1.3: - resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} ajv@6.12.6: @@ -1827,16 +1867,16 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} any-promise@1.3.0: @@ -1857,10 +1897,6 @@ packages: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} - array-includes@3.1.8: - resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} - engines: {node: '>= 0.4'} - array-includes@3.1.9: resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} engines: {node: '>= 0.4'} @@ -1869,8 +1905,8 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} - array.prototype.findlastindex@1.2.5: - resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} engines: {node: '>= 0.4'} array.prototype.flat@1.3.3: @@ -1910,8 +1946,8 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axe-core@4.10.3: - resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} + axe-core@4.11.1: + resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} engines: {node: '>=4'} axios@1.6.7: @@ -1937,6 +1973,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.9.11: + resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} + hasBin: true + bech32@1.1.4: resolution: {integrity: sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==} @@ -1954,6 +1994,9 @@ packages: binary@0.3.0: resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==} + blakejs@1.2.1: + resolution: {integrity: sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==} + bluebird@3.4.7: resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} @@ -1963,14 +2006,14 @@ packages: bn.js@5.2.2: resolution: {integrity: sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==} - bowser@2.11.0: - resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} + bowser@2.13.1: + resolution: {integrity: sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==} - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -1979,8 +2022,8 @@ packages: brorand@1.1.0: resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} - browserslist@4.25.0: - resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==} + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -1990,9 +2033,6 @@ packages: bs58@6.0.0: resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - buffer-indexof-polyfill@1.0.2: resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==} engines: {node: '>=0.10'} @@ -2004,8 +2044,8 @@ packages: resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==} engines: {node: '>=0.2.0'} - bufferutil@4.0.9: - resolution: {integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==} + bufferutil@4.1.0: + resolution: {integrity: sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==} engines: {node: '>=6.14.2'} bundle-require@4.2.1: @@ -2038,12 +2078,12 @@ packages: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} - caniuse-lite@1.0.30001721: - resolution: {integrity: sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==} + caniuse-lite@1.0.30001762: + resolution: {integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==} - chai@5.2.0: - resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} - engines: {node: '>=12'} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} chainsaw@0.1.0: resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} @@ -2056,8 +2096,8 @@ packages: resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - check-error@2.1.1: - resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} chokidar@3.6.0: @@ -2098,9 +2138,6 @@ packages: resolution: {integrity: sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==} engines: {node: '>=18'} - commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -2117,8 +2154,8 @@ packages: cookie-es@1.2.2: resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} - cookie@1.0.2: - resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} core-util-is@1.0.3: @@ -2146,8 +2183,8 @@ packages: crossws@0.3.5: resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} - css-what@6.1.0: - resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} cssesc@3.0.0: @@ -2155,12 +2192,12 @@ packages: engines: {node: '>=4'} hasBin: true - cssstyle@4.3.1: - resolution: {integrity: sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==} + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} engines: {node: '>=18'} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -2196,26 +2233,8 @@ packages: supports-color: optional: true - debug@4.3.7: - resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - debug@4.4.1: - resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -2227,15 +2246,15 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} - decimal.js@10.5.0: - resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} decode-uri-component@0.2.2: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} - dedent@1.6.0: - resolution: {integrity: sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==} + dedent@1.7.1: + resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==} peerDependencies: babel-plugin-macros: ^3.1.0 peerDependenciesMeta: @@ -2299,8 +2318,8 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} - dotenv@16.5.0: - resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} dunder-proto@1.0.1: @@ -2316,12 +2335,12 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - eciesjs@0.4.15: - resolution: {integrity: sha512-r6kEJXDKecVOCj2nLMuXK/FCPeurW33+3JRpfXVbjLja3XUYFfD9I/JBreH6sUyzcm3G/YQboBjMla6poKeSdA==} + eciesjs@0.4.16: + resolution: {integrity: sha512-dS5cbA9rA2VR4Ybuvhg6jvdmp46ubLn3E+px8cG/35aEDNclrqoCjg6mt0HYZ/M+OoESS3jSkCrqk1kWAEhWAw==} engines: {bun: '>=1', deno: '>=2', node: '>=16'} - electron-to-chromium@1.5.165: - resolution: {integrity: sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==} + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} elliptic@6.6.1: resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} @@ -2335,29 +2354,25 @@ packages: encode-utf8@1.0.3: resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==} - end-of-stream@1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - engine.io-client@6.6.3: - resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==} + engine.io-client@6.6.4: + resolution: {integrity: sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==} engine.io-parser@5.2.3: resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} engines: {node: '>=10.0.0'} - entities@6.0.0: - resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} - error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - - es-abstract@1.23.9: - resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} - engines: {node: '>= 0.4'} + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - es-abstract@1.24.0: - resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} engines: {node: '>= 0.4'} es-define-property@1.0.1: @@ -2390,6 +2405,9 @@ packages: es-toolkit@1.33.0: resolution: {integrity: sha512-X13Q/ZSc+vsO1q600bvNK4bxgXMkHcf//RxCmYDaRY5DAcT+eoXjY5hoAPGMdRnWQjvyLEcyauG3b6hz76LNqg==} + es-toolkit@1.39.3: + resolution: {integrity: sha512-Qb/TCFCldgOy8lZ5uC7nLGdqJwSabkQiYQShmw4jyiPk1pZzaYWTwaYKYP7EgLccWYgZocMrtItrwh683voaww==} + esbuild@0.19.12: resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} engines: {node: '>=12'} @@ -2400,8 +2418,8 @@ packages: engines: {node: '>=12'} hasBin: true - esbuild@0.25.0: - resolution: {integrity: sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==} + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} hasBin: true @@ -2428,8 +2446,8 @@ packages: eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} - eslint-module-utils@2.12.0: - resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' @@ -2490,16 +2508,16 @@ packages: peerDependencies: eslint: '>=8.40' - eslint-scope@8.3.0: - resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.2.0: - resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint@9.28.0: @@ -2512,12 +2530,12 @@ packages: jiti: optional: true - espree@10.3.0: - resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} esrecurse@4.3.0: @@ -2576,8 +2594,8 @@ packages: resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==} engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} - expect-type@1.2.1: - resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} extension-port-stream@3.0.0: @@ -2607,11 +2625,12 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - fdir@6.4.5: - resolution: {integrity: sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -2648,8 +2667,8 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - follow-redirects@1.15.9: - resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} peerDependencies: debug: '*' @@ -2665,8 +2684,8 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data@4.0.3: - resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} fs-extra@11.2.0: @@ -2701,6 +2720,10 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -2750,10 +2773,6 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported - globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -2784,8 +2803,8 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - h3@1.15.3: - resolution: {integrity: sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ==} + h3@1.15.4: + resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} @@ -2950,8 +2969,8 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-generator-function@1.1.0: - resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} engines: {node: '>= 0.4'} is-glob@4.0.3: @@ -3063,8 +3082,8 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true jsdom@26.1.0: @@ -3109,8 +3128,8 @@ packages: engines: {node: '>=6'} hasBin: true - jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} @@ -3153,11 +3172,11 @@ packages: listenercount@1.0.1: resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==} - lit-element@4.2.0: - resolution: {integrity: sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q==} + lit-element@4.2.2: + resolution: {integrity: sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==} - lit-html@3.3.0: - resolution: {integrity: sha512-RHoswrFAxY2d8Cf2mm4OZ1DgzCoBKUKSPvA1fhtSELxUERq2aQQ2h05pO9j81gS1o7RIRJ+CePLogfyahwmynw==} + lit-html@3.3.2: + resolution: {integrity: sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==} lit@3.3.0: resolution: {integrity: sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==} @@ -3184,8 +3203,8 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loupe@3.1.3: - resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -3193,8 +3212,13 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - magic-string@0.30.17: - resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + lucide-react@0.562.0: + resolution: {integrity: sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} @@ -3288,8 +3312,8 @@ packages: node-addon-api@2.0.2: resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==} - node-fetch-native@1.6.6: - resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==} + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} @@ -3304,11 +3328,11 @@ packages: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true - node-mock-http@1.0.0: - resolution: {integrity: sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==} + node-mock-http@1.0.4: + resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} - node-releases@2.0.19: - resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} @@ -3322,8 +3346,8 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - nwsapi@2.2.20: - resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} obj-multiplex@1.0.0: resolution: {integrity: sha512-0GNJAOsHoBHeNTvl5Vt6IWnpUEcc3uSRxzBri7EDyIcMgYvnY2JL2qdeV5zTMjWQX5OHcD5amcW2HFfDh0gjIA==} @@ -3360,12 +3384,16 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} - ofetch@1.4.1: - resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} on-exit-leak-free@0.2.0: resolution: {integrity: sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -3401,6 +3429,14 @@ packages: typescript: optional: true + ox@0.9.3: + resolution: {integrity: sha512-KzyJP+fPV4uhuuqrTZyok4DC7vFzi7HLUFiUNEmpbyh59htKWkOC98IONC1zgXJPbHAhQgqs6B0Z6StCGhmQvg==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -3465,8 +3501,8 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pathval@2.0.0: - resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} picocolors@1.1.1: @@ -3476,8 +3512,8 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} pify@3.0.0: @@ -3491,9 +3527,19 @@ packages: pino-abstract-transport@0.5.0: resolution: {integrity: sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==} + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + pino-std-serializers@4.0.0: resolution: {integrity: sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==} + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + + pino@10.0.0: + resolution: {integrity: sha512-eI9pKwWEix40kfvSzqEP6ldqOoBIN7dwD/o91TY5z8vQI12sAffpR/pOqAD1IVVwIVHDpHjkq0joBPdJD0rafA==} + hasBin: true + pino@7.11.0: resolution: {integrity: sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==} hasBin: true @@ -3536,19 +3582,19 @@ packages: ts-node: optional: true - postcss@8.5.4: - resolution: {integrity: sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==} + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - preact@10.26.8: - resolution: {integrity: sha512-1nMfdFjucm5hKvq0IClqZwK4FJkGXhRrQstOQ3P4vp8HxKrJEMFcY6RdBRVTdfQS/UlnX6gfbPuTvaqx/bDoeQ==} + preact@10.28.2: + resolution: {integrity: sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==} prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-linter-helpers@1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} prettier@3.5.3: @@ -3562,6 +3608,9 @@ packages: process-warning@1.0.0: resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==} + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -3575,8 +3624,8 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - pump@3.0.2: - resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} @@ -3616,8 +3665,8 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - react-is@19.1.0: - resolution: {integrity: sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==} + react-is@19.2.3: + resolution: {integrity: sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==} react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} @@ -3699,13 +3748,14 @@ packages: resolution: {integrity: sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==} engines: {node: '>= 12.13.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} - regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -3728,8 +3778,8 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - resolve@1.22.10: - resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} hasBin: true @@ -3742,8 +3792,8 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rollup@4.41.1: - resolution: {integrity: sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==} + rollup@4.55.1: + resolution: {integrity: sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -3792,16 +3842,16 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.2: - resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - set-cookie-parser@2.7.1: - resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} @@ -3818,8 +3868,9 @@ packages: setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} - sha.js@2.4.11: - resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + sha.js@2.4.12: + resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} + engines: {node: '>= 0.10'} hasBin: true shebang-command@2.0.0: @@ -3860,12 +3911,15 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - socket.io-client@4.8.1: - resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==} + slow-redact@0.3.2: + resolution: {integrity: sha512-MseHyi2+E/hBRqdOi5COy6wZ7j7DxXRz9NkseavNYSvvWC06D8a5cidVZX3tcG5eCW3NIyVU4zT63hw0Q486jw==} + + socket.io-client@4.8.3: + resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==} engines: {node: '>=10.0.0'} - socket.io-parser@4.2.4: - resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + socket.io-parser@4.2.5: + resolution: {integrity: sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==} engines: {node: '>=10.0.0'} solady@https://codeload.github.com/vectorized/solady/tar.gz/701406e: @@ -3875,24 +3929,21 @@ packages: sonic-boom@2.8.0: resolution: {integrity: sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==} + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - source-map@0.5.7: resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} engines: {node: '>=0.10.0'} - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions split-on-first@1.1.0: resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} @@ -3905,8 +3956,8 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@3.9.0: - resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} @@ -3953,8 +4004,8 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} strip-bom@3.0.0: @@ -3976,8 +4027,8 @@ packages: stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} - sucrase@3.35.0: - resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} hasBin: true @@ -3996,15 +4047,10 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - synckit@0.11.8: - resolution: {integrity: sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==} + synckit@0.11.11: + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} - terser@5.39.0: - resolution: {integrity: sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==} - engines: {node: '>=10'} - hasBin: true - thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -4015,6 +4061,9 @@ packages: thread-stream@0.15.2: resolution: {integrity: sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==} + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -4024,23 +4073,23 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyglobby@0.2.14: - resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} tinygradient@1.1.5: resolution: {integrity: sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==} - tinypool@1.1.0: - resolution: {integrity: sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} - tinyspy@4.0.3: - resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} tldts-core@6.1.86: @@ -4050,6 +4099,10 @@ packages: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true + to-buffer@1.2.2: + resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} + engines: {node: '>= 0.4'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -4075,8 +4128,8 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - ts-api-utils@2.1.0: - resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -4144,16 +4197,19 @@ packages: engines: {node: '>=14.17'} hasBin: true - ua-parser-js@1.0.40: - resolution: {integrity: sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==} + ua-parser-js@1.0.41: + resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} hasBin: true - ufo@1.6.1: - resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + ufo@1.6.2: + resolution: {integrity: sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q==} uint8arrays@3.1.0: resolution: {integrity: sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog==} + uint8arrays@3.1.1: + resolution: {integrity: sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -4168,8 +4224,8 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - unstorage@1.16.0: - resolution: {integrity: sha512-WQ37/H5A7LcRPWfYOrDa1Ys02xAbpPJq6q5GkO88FBXVSQzHd7+BjEwfRqyaSWCv9MbsJy058GWjjPjcJ16GGA==} + unstorage@1.17.3: + resolution: {integrity: sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q==} peerDependencies: '@azure/app-configuration': ^1.8.0 '@azure/cosmos': ^4.2.0 @@ -4179,10 +4235,11 @@ packages: '@azure/storage-blob': ^12.26.0 '@capacitor/preferences': ^6.0.3 || ^7.0.0 '@deno/kv': '>=0.9.0' - '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 '@planetscale/database': ^1.19.0 '@upstash/redis': ^1.34.3 '@vercel/blob': '>=0.27.1' + '@vercel/functions': ^2.2.12 || ^3.0.0 '@vercel/kv': ^1.0.1 aws4fetch: ^1.0.20 db0: '>=0.2.1' @@ -4214,6 +4271,8 @@ packages: optional: true '@vercel/blob': optional: true + '@vercel/functions': + optional: true '@vercel/kv': optional: true aws4fetch: @@ -4233,8 +4292,8 @@ packages: unzipper@0.10.14: resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==} - update-browserslist-db@1.1.3: - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -4430,6 +4489,7 @@ packages: whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} @@ -4460,10 +4520,6 @@ packages: which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} - which-typed-array@1.1.18: - resolution: {integrity: sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==} - engines: {node: '>= 0.4'} - which-typed-array@1.1.19: resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} engines: {node: '>= 0.4'} @@ -4526,8 +4582,8 @@ packages: utf-8-validate: optional: true - ws@8.17.1: - resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -4538,8 +4594,8 @@ packages: utf-8-validate: optional: true - ws@8.18.0: - resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + ws@8.18.2: + resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -4550,8 +4606,20 @@ packages: utf-8-validate: optional: true - ws@8.18.2: - resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -4590,8 +4658,8 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yaml@2.8.0: - resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} hasBin: true @@ -4632,188 +4700,129 @@ snapshots: '@adraffy/ens-normalize@1.10.0': {} - '@adraffy/ens-normalize@1.11.0': {} - - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@adraffy/ens-normalize@1.11.1': {} '@asamuzakjp/css-color@3.2.0': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 - '@babel/code-frame@7.26.2': - dependencies: - '@babel/helper-validator-identifier': 7.25.9 - js-tokens: 4.0.0 - picocolors: 1.1.1 - '@babel/code-frame@7.27.1': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.27.5': {} + '@babel/compat-data@7.28.5': {} - '@babel/core@7.27.4': + '@babel/core@7.28.5': dependencies: - '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.27.1 - '@babel/generator': 7.27.5 + '@babel/generator': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) - '@babel/helpers': 7.27.4 - '@babel/parser': 7.27.5 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.3 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.1 + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/generator@7.26.9': - dependencies: - '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 3.1.0 - - '@babel/generator@7.27.5': + '@babel/generator@7.28.5': dependencies: - '@babel/parser': 7.27.5 - '@babel/types': 7.27.3 - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 '@babel/helper-compilation-targets@7.27.2': dependencies: - '@babel/compat-data': 7.27.5 + '@babel/compat-data': 7.28.5 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.25.0 + browserslist: 4.28.1 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-module-imports@7.25.9': - dependencies: - '@babel/traverse': 7.26.9 - '@babel/types': 7.26.9 - transitivePeerDependencies: - - supports-color + '@babel/helper-globals@7.28.0': {} '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.3 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.4)': + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.28.5 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.27.4 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helper-plugin-utils@7.27.1': {} - '@babel/helper-string-parser@7.25.9': {} - '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-validator-identifier@7.25.9': {} - - '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} '@babel/helper-validator-option@7.27.1': {} - '@babel/helpers@7.27.4': + '@babel/helpers@7.28.4': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.27.3 - - '@babel/parser@7.26.9': - dependencies: - '@babel/types': 7.26.9 + '@babel/types': 7.28.5 - '@babel/parser@7.27.5': + '@babel/parser@7.28.5': dependencies: - '@babel/types': 7.27.3 + '@babel/types': 7.28.5 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.4)': + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.4)': + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.27.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/runtime@7.26.10': - dependencies: - regenerator-runtime: 0.14.1 - - '@babel/runtime@7.27.4': {} - - '@babel/template@7.26.9': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 + '@babel/runtime@7.28.4': {} '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.27.5 - '@babel/types': 7.27.3 - - '@babel/traverse@7.26.9': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.26.9 - '@babel/parser': 7.26.9 - '@babel/template': 7.26.9 - '@babel/types': 7.26.9 - debug: 4.4.0 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 - '@babel/traverse@7.27.4': + '@babel/traverse@7.28.5': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.27.5 - '@babel/parser': 7.27.5 + '@babel/generator': 7.28.5 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/types': 7.27.3 - debug: 4.4.1 - globals: 11.12.0 + '@babel/types': 7.28.5 + debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.26.9': - dependencies: - '@babel/helper-string-parser': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 - - '@babel/types@7.27.3': + '@babel/types@7.28.5': dependencies: '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@coinbase/wallet-sdk@3.9.3': dependencies: @@ -4824,8 +4833,8 @@ snapshots: eth-json-rpc-filters: 6.0.1 eventemitter3: 5.0.1 keccak: 3.0.4 - preact: 10.26.8 - sha.js: 2.4.11 + preact: 10.28.2 + sha.js: 2.4.12 transitivePeerDependencies: - supports-color @@ -4834,18 +4843,18 @@ snapshots: '@noble/hashes': 1.8.0 clsx: 1.2.1 eventemitter3: 5.0.1 - preact: 10.26.8 + preact: 10.28.2 - '@csstools/color-helpers@5.0.2': {} + '@csstools/color-helpers@5.1.0': {} '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/css-color-parser@3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: - '@csstools/color-helpers': 5.0.2 + '@csstools/color-helpers': 5.1.0 '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 @@ -4858,36 +4867,36 @@ snapshots: '@defi-wonderland/canon-guard-interfaces@0.0.1': dependencies: - '@safe-global/safe-contracts': '@safe-global/safe-smart-account@https://codeload.github.com/safe-global/safe-smart-account/tar.gz/b75e250' + '@safe-global/safe-contracts': '@safe-global/safe-smart-account@git+https://git@github.com:safe-global/safe-smart-account.git#b75e2509ba4968f97c427f2657996a0711b7a4cf' solady: https://codeload.github.com/vectorized/solady/tar.gz/701406e '@defi-wonderland/crypto-husky-checks@1.5.6': {} - '@depay/solana-web3.js@1.98.2': + '@depay/solana-web3.js@1.98.3': dependencies: bs58: 5.0.0 - '@depay/web3-blockchains@9.8.6': {} + '@depay/web3-blockchains@9.8.10': {} - '@depay/web3-client@10.18.6(@depay/solana-web3.js@1.98.2)(@depay/web3-blockchains@9.8.6)(ethers@5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@depay/web3-client@10.18.6(@depay/solana-web3.js@1.98.3)(@depay/web3-blockchains@9.8.10)(ethers@5.8.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))': dependencies: - '@depay/solana-web3.js': 1.98.2 - '@depay/web3-blockchains': 9.8.6 - ethers: 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@depay/solana-web3.js': 1.98.3 + '@depay/web3-blockchains': 9.8.10 + ethers: 5.8.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) - '@depay/web3-mock-evm@14.19.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@depay/web3-mock-evm@14.19.1(bufferutil@4.1.0)(utf-8-validate@5.0.10)': dependencies: - '@depay/web3-blockchains': 9.8.6 - ethers: 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@depay/web3-blockchains': 9.8.10 + ethers: 5.8.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate - '@depay/web3-mock@14.19.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@depay/web3-mock@14.19.1(bufferutil@4.1.0)(utf-8-validate@5.0.10)': dependencies: - '@depay/solana-web3.js': 1.98.2 - '@depay/web3-blockchains': 9.8.6 - ethers: 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@depay/solana-web3.js': 1.98.3 + '@depay/web3-blockchains': 9.8.10 + ethers: 5.8.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -4895,23 +4904,23 @@ snapshots: '@dotenvx/dotenvx@1.44.2': dependencies: commander: 11.1.0 - dotenv: 16.5.0 - eciesjs: 0.4.15 + dotenv: 16.6.1 + eciesjs: 0.4.16 execa: 5.1.1 - fdir: 6.4.5(picomatch@4.0.2) + fdir: 6.5.0(picomatch@4.0.3) ignore: 5.3.2 object-treeify: 1.1.33 - picomatch: 4.0.2 + picomatch: 4.0.3 which: 4.0.0 - '@ecies/ciphers@0.2.3(@noble/ciphers@1.3.0)': + '@ecies/ciphers@0.2.5(@noble/ciphers@1.3.0)': dependencies: '@noble/ciphers': 1.3.0 '@emotion/babel-plugin@11.13.5': dependencies: - '@babel/helper-module-imports': 7.25.9 - '@babel/runtime': 7.26.10 + '@babel/helper-module-imports': 7.27.1 + '@babel/runtime': 7.28.4 '@emotion/hash': 0.9.2 '@emotion/memoize': 0.9.0 '@emotion/serialize': 1.3.3 @@ -4934,7 +4943,7 @@ snapshots: '@emotion/hash@0.9.2': {} - '@emotion/is-prop-valid@1.3.1': + '@emotion/is-prop-valid@1.4.0': dependencies: '@emotion/memoize': 0.9.0 @@ -4942,7 +4951,7 @@ snapshots: '@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0)': dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 @@ -4962,15 +4971,15 @@ snapshots: '@emotion/memoize': 0.9.0 '@emotion/unitless': 0.10.0 '@emotion/utils': 1.4.2 - csstype: 3.1.3 + csstype: 3.2.3 '@emotion/sheet@1.4.0': {} '@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react@19.1.0)': dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 - '@emotion/is-prop-valid': 1.3.1 + '@emotion/is-prop-valid': 1.4.0 '@emotion/react': 11.14.0(@types/react@19.1.6)(react@19.1.0) '@emotion/serialize': 1.3.3 '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.0) @@ -4997,7 +5006,7 @@ snapshots: '@esbuild/aix-ppc64@0.20.0': optional: true - '@esbuild/aix-ppc64@0.25.0': + '@esbuild/aix-ppc64@0.25.12': optional: true '@esbuild/android-arm64@0.19.12': @@ -5006,7 +5015,7 @@ snapshots: '@esbuild/android-arm64@0.20.0': optional: true - '@esbuild/android-arm64@0.25.0': + '@esbuild/android-arm64@0.25.12': optional: true '@esbuild/android-arm@0.19.12': @@ -5015,7 +5024,7 @@ snapshots: '@esbuild/android-arm@0.20.0': optional: true - '@esbuild/android-arm@0.25.0': + '@esbuild/android-arm@0.25.12': optional: true '@esbuild/android-x64@0.19.12': @@ -5024,7 +5033,7 @@ snapshots: '@esbuild/android-x64@0.20.0': optional: true - '@esbuild/android-x64@0.25.0': + '@esbuild/android-x64@0.25.12': optional: true '@esbuild/darwin-arm64@0.19.12': @@ -5033,7 +5042,7 @@ snapshots: '@esbuild/darwin-arm64@0.20.0': optional: true - '@esbuild/darwin-arm64@0.25.0': + '@esbuild/darwin-arm64@0.25.12': optional: true '@esbuild/darwin-x64@0.19.12': @@ -5042,7 +5051,7 @@ snapshots: '@esbuild/darwin-x64@0.20.0': optional: true - '@esbuild/darwin-x64@0.25.0': + '@esbuild/darwin-x64@0.25.12': optional: true '@esbuild/freebsd-arm64@0.19.12': @@ -5051,7 +5060,7 @@ snapshots: '@esbuild/freebsd-arm64@0.20.0': optional: true - '@esbuild/freebsd-arm64@0.25.0': + '@esbuild/freebsd-arm64@0.25.12': optional: true '@esbuild/freebsd-x64@0.19.12': @@ -5060,7 +5069,7 @@ snapshots: '@esbuild/freebsd-x64@0.20.0': optional: true - '@esbuild/freebsd-x64@0.25.0': + '@esbuild/freebsd-x64@0.25.12': optional: true '@esbuild/linux-arm64@0.19.12': @@ -5069,7 +5078,7 @@ snapshots: '@esbuild/linux-arm64@0.20.0': optional: true - '@esbuild/linux-arm64@0.25.0': + '@esbuild/linux-arm64@0.25.12': optional: true '@esbuild/linux-arm@0.19.12': @@ -5078,7 +5087,7 @@ snapshots: '@esbuild/linux-arm@0.20.0': optional: true - '@esbuild/linux-arm@0.25.0': + '@esbuild/linux-arm@0.25.12': optional: true '@esbuild/linux-ia32@0.19.12': @@ -5087,7 +5096,7 @@ snapshots: '@esbuild/linux-ia32@0.20.0': optional: true - '@esbuild/linux-ia32@0.25.0': + '@esbuild/linux-ia32@0.25.12': optional: true '@esbuild/linux-loong64@0.19.12': @@ -5096,7 +5105,7 @@ snapshots: '@esbuild/linux-loong64@0.20.0': optional: true - '@esbuild/linux-loong64@0.25.0': + '@esbuild/linux-loong64@0.25.12': optional: true '@esbuild/linux-mips64el@0.19.12': @@ -5105,7 +5114,7 @@ snapshots: '@esbuild/linux-mips64el@0.20.0': optional: true - '@esbuild/linux-mips64el@0.25.0': + '@esbuild/linux-mips64el@0.25.12': optional: true '@esbuild/linux-ppc64@0.19.12': @@ -5114,7 +5123,7 @@ snapshots: '@esbuild/linux-ppc64@0.20.0': optional: true - '@esbuild/linux-ppc64@0.25.0': + '@esbuild/linux-ppc64@0.25.12': optional: true '@esbuild/linux-riscv64@0.19.12': @@ -5123,7 +5132,7 @@ snapshots: '@esbuild/linux-riscv64@0.20.0': optional: true - '@esbuild/linux-riscv64@0.25.0': + '@esbuild/linux-riscv64@0.25.12': optional: true '@esbuild/linux-s390x@0.19.12': @@ -5132,7 +5141,7 @@ snapshots: '@esbuild/linux-s390x@0.20.0': optional: true - '@esbuild/linux-s390x@0.25.0': + '@esbuild/linux-s390x@0.25.12': optional: true '@esbuild/linux-x64@0.19.12': @@ -5141,10 +5150,10 @@ snapshots: '@esbuild/linux-x64@0.20.0': optional: true - '@esbuild/linux-x64@0.25.0': + '@esbuild/linux-x64@0.25.12': optional: true - '@esbuild/netbsd-arm64@0.25.0': + '@esbuild/netbsd-arm64@0.25.12': optional: true '@esbuild/netbsd-x64@0.19.12': @@ -5153,10 +5162,10 @@ snapshots: '@esbuild/netbsd-x64@0.20.0': optional: true - '@esbuild/netbsd-x64@0.25.0': + '@esbuild/netbsd-x64@0.25.12': optional: true - '@esbuild/openbsd-arm64@0.25.0': + '@esbuild/openbsd-arm64@0.25.12': optional: true '@esbuild/openbsd-x64@0.19.12': @@ -5165,7 +5174,10 @@ snapshots: '@esbuild/openbsd-x64@0.20.0': optional: true - '@esbuild/openbsd-x64@0.25.0': + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': optional: true '@esbuild/sunos-x64@0.19.12': @@ -5174,7 +5186,7 @@ snapshots: '@esbuild/sunos-x64@0.20.0': optional: true - '@esbuild/sunos-x64@0.25.0': + '@esbuild/sunos-x64@0.25.12': optional: true '@esbuild/win32-arm64@0.19.12': @@ -5183,7 +5195,7 @@ snapshots: '@esbuild/win32-arm64@0.20.0': optional: true - '@esbuild/win32-arm64@0.25.0': + '@esbuild/win32-arm64@0.25.12': optional: true '@esbuild/win32-ia32@0.19.12': @@ -5192,7 +5204,7 @@ snapshots: '@esbuild/win32-ia32@0.20.0': optional: true - '@esbuild/win32-ia32@0.25.0': + '@esbuild/win32-ia32@0.25.12': optional: true '@esbuild/win32-x64@0.19.12': @@ -5201,39 +5213,43 @@ snapshots: '@esbuild/win32-x64@0.20.0': optional: true - '@esbuild/win32-x64@0.25.0': + '@esbuild/win32-x64@0.25.12': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@9.28.0)': + '@eslint-community/eslint-utils@4.9.1(eslint@9.28.0)': dependencies: eslint: 9.28.0 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.12.1': {} + '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.20.0': + '@eslint/config-array@0.20.1': dependencies: - '@eslint/object-schema': 2.1.6 - debug: 4.4.1 + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 minimatch: 3.1.2 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.2.2': {} + '@eslint/config-helpers@0.2.3': {} '@eslint/core@0.14.0': dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.1': + '@eslint/core@0.15.2': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.3': dependencies: ajv: 6.12.6 - debug: 4.4.1 - espree: 10.3.0 + debug: 4.4.3 + espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 minimatch: 3.1.2 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -5241,11 +5257,11 @@ snapshots: '@eslint/js@9.28.0': {} - '@eslint/object-schema@2.1.6': {} + '@eslint/object-schema@2.1.7': {} - '@eslint/plugin-kit@0.3.1': + '@eslint/plugin-kit@0.3.5': dependencies: - '@eslint/core': 0.14.0 + '@eslint/core': 0.15.2 levn: 0.4.1 '@ethereumjs/common@3.2.0': @@ -5405,7 +5421,7 @@ snapshots: dependencies: '@ethersproject/logger': 5.8.0 - '@ethersproject/providers@5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@ethersproject/providers@5.8.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)': dependencies: '@ethersproject/abstract-provider': 5.8.0 '@ethersproject/abstract-signer': 5.8.0 @@ -5426,7 +5442,7 @@ snapshots: '@ethersproject/transactions': 5.8.0 '@ethersproject/web': 5.8.0 bech32: 1.1.4 - ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -5525,54 +5541,48 @@ snapshots: '@humanfs/core@0.19.1': {} - '@humanfs/node@0.16.6': + '@humanfs/node@0.16.7': dependencies: '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.3.1 + '@humanwhocodes/retry': 0.4.3 '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/retry@0.3.1': {} - '@humanwhocodes/retry@0.4.3': {} '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 strip-ansi-cjs: strip-ansi@6.0.1 wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@jridgewell/gen-mapping@0.3.8': + '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/set-array@1.2.1': {} + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/source-map@0.3.6': + '@jridgewell/remapping@2.3.5': dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 - optional: true + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.25': + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 - '@lit-labs/ssr-dom-shim@1.3.0': {} + '@lit-labs/ssr-dom-shim@1.5.0': {} - '@lit/reactive-element@2.1.0': + '@lit/reactive-element@2.1.2': dependencies: - '@lit-labs/ssr-dom-shim': 1.3.0 + '@lit-labs/ssr-dom-shim': 1.5.0 '@metamask/eth-json-rpc-provider@1.0.1': dependencies: @@ -5614,7 +5624,7 @@ snapshots: '@metamask/onboarding@1.0.1': dependencies: - bowser: 2.11.0 + bowser: 2.13.1 '@metamask/providers@16.1.0': dependencies: @@ -5644,16 +5654,16 @@ snapshots: '@metamask/safe-event-emitter@3.1.2': {} - '@metamask/sdk-communication-layer@0.32.0(cross-fetch@4.1.0)(eciesjs@0.4.15)(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.16)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))': dependencies: - bufferutil: 4.0.9 + bufferutil: 4.1.0 cross-fetch: 4.1.0 date-fns: 2.30.0 - debug: 4.4.1 - eciesjs: 0.4.15 + debug: 4.4.3 + eciesjs: 0.4.16 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) + socket.io-client: 4.8.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) utf-8-validate: 5.0.10 uuid: 8.3.2 transitivePeerDependencies: @@ -5663,24 +5673,24 @@ snapshots: dependencies: '@paulmillr/qr': 0.2.1 - '@metamask/sdk@0.32.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@metamask/sdk@0.32.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)': dependencies: - '@babel/runtime': 7.27.4 + '@babel/runtime': 7.28.4 '@metamask/onboarding': 1.0.1 '@metamask/providers': 16.1.0 - '@metamask/sdk-communication-layer': 0.32.0(cross-fetch@4.1.0)(eciesjs@0.4.15)(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.16)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) '@metamask/sdk-install-modal-web': 0.32.0 '@paulmillr/qr': 0.2.1 - bowser: 2.11.0 + bowser: 2.13.1 cross-fetch: 4.1.0 - debug: 4.4.1 - eciesjs: 0.4.15 + debug: 4.4.3 + eciesjs: 0.4.16 eth-rpc-errors: 4.0.3 eventemitter2: 6.4.9 obj-multiplex: 1.0.0 - pump: 3.0.2 + pump: 3.0.3 readable-stream: 3.6.2 - socket.io-client: 4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + socket.io-client: 4.8.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) tslib: 2.8.1 util: 0.12.5 uuid: 8.3.2 @@ -5696,8 +5706,8 @@ snapshots: dependencies: '@ethereumjs/tx': 4.2.0 '@types/debug': 4.1.12 - debug: 4.4.1 - semver: 7.7.2 + debug: 4.4.3 + semver: 7.7.3 superstruct: 1.0.4 transitivePeerDependencies: - supports-color @@ -5709,9 +5719,9 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@types/debug': 4.1.12 - debug: 4.4.1 + debug: 4.4.3 pony-cause: 2.1.11 - semver: 7.7.2 + semver: 7.7.3 uuid: 9.0.1 transitivePeerDependencies: - supports-color @@ -5723,18 +5733,20 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@types/debug': 4.1.12 - debug: 4.4.1 + debug: 4.4.3 pony-cause: 2.1.11 - semver: 7.7.2 + semver: 7.7.3 uuid: 9.0.1 transitivePeerDependencies: - supports-color - '@mui/core-downloads-tracker@6.4.12': {} + '@msgpack/msgpack@3.1.2': {} + + '@mui/core-downloads-tracker@6.5.0': {} '@mui/icons-material@6.1.3(@mui/material@6.1.3(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.6)(react@19.1.0)': dependencies: - '@babel/runtime': 7.27.4 + '@babel/runtime': 7.28.4 '@mui/material': 6.1.3(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: 19.1.0 optionalDependencies: @@ -5742,15 +5754,15 @@ snapshots: '@mui/material@6.1.3(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@babel/runtime': 7.27.4 - '@mui/core-downloads-tracker': 6.4.12 - '@mui/system': 6.4.12(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react@19.1.0) - '@mui/types': 7.4.3(@types/react@19.1.6) + '@babel/runtime': 7.28.4 + '@mui/core-downloads-tracker': 6.5.0 + '@mui/system': 6.5.0(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react@19.1.0) + '@mui/types': 7.4.9(@types/react@19.1.6) '@mui/utils': 6.4.9(@types/react@19.1.6)(react@19.1.0) '@popperjs/core': 2.11.8 '@types/react-transition-group': 4.4.12(@types/react@19.1.6) clsx: 2.1.1 - csstype: 3.1.3 + csstype: 3.2.3 prop-types: 15.8.1 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) @@ -5763,35 +5775,35 @@ snapshots: '@mui/private-theming@6.4.9(@types/react@19.1.6)(react@19.1.0)': dependencies: - '@babel/runtime': 7.27.4 + '@babel/runtime': 7.28.4 '@mui/utils': 6.4.9(@types/react@19.1.6)(react@19.1.0) prop-types: 15.8.1 react: 19.1.0 optionalDependencies: '@types/react': 19.1.6 - '@mui/styled-engine@6.4.11(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0)': + '@mui/styled-engine@6.5.0(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0)': dependencies: - '@babel/runtime': 7.27.4 + '@babel/runtime': 7.28.4 '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 '@emotion/sheet': 1.4.0 - csstype: 3.1.3 + csstype: 3.2.3 prop-types: 15.8.1 react: 19.1.0 optionalDependencies: '@emotion/react': 11.14.0(@types/react@19.1.6)(react@19.1.0) '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react@19.1.0) - '@mui/system@6.4.12(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react@19.1.0)': + '@mui/system@6.5.0(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react@19.1.0)': dependencies: - '@babel/runtime': 7.27.4 + '@babel/runtime': 7.28.4 '@mui/private-theming': 6.4.9(@types/react@19.1.6)(react@19.1.0) - '@mui/styled-engine': 6.4.11(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0) + '@mui/styled-engine': 6.5.0(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.6)(react@19.1.0))(@types/react@19.1.6)(react@19.1.0))(react@19.1.0) '@mui/types': 7.2.24(@types/react@19.1.6) '@mui/utils': 6.4.9(@types/react@19.1.6)(react@19.1.0) clsx: 2.1.1 - csstype: 3.1.3 + csstype: 3.2.3 prop-types: 15.8.1 react: 19.1.0 optionalDependencies: @@ -5803,21 +5815,21 @@ snapshots: optionalDependencies: '@types/react': 19.1.6 - '@mui/types@7.4.3(@types/react@19.1.6)': + '@mui/types@7.4.9(@types/react@19.1.6)': dependencies: - '@babel/runtime': 7.27.4 + '@babel/runtime': 7.28.4 optionalDependencies: '@types/react': 19.1.6 '@mui/utils@6.4.9(@types/react@19.1.6)(react@19.1.0)': dependencies: - '@babel/runtime': 7.27.4 + '@babel/runtime': 7.28.4 '@mui/types': 7.2.24(@types/react@19.1.6) - '@types/prop-types': 15.7.14 + '@types/prop-types': 15.7.15 clsx: 2.1.1 prop-types: 15.8.1 react: 19.1.0 - react-is: 19.1.0 + react-is: 19.2.3 optionalDependencies: '@types/react': 19.1.6 @@ -5845,6 +5857,10 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + '@noble/hashes@1.3.2': {} '@noble/hashes@1.4.0': {} @@ -5865,14 +5881,14 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 + fastq: 1.20.1 '@paulmillr/qr@0.2.1': {} '@pkgjs/parseargs@0.11.0': optional: true - '@pkgr/core@0.2.7': {} + '@pkgr/core@0.2.9': {} '@playwright/test@1.53.0': dependencies: @@ -5880,7 +5896,7 @@ snapshots: '@popperjs/core@2.11.8': {} - '@rainbow-me/rainbowkit@2.2.5(@tanstack/react-query@5.80.3(react@19.1.0))(@types/react@19.1.6)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(viem@2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4))(wagmi@2.15.5(@tanstack/query-core@5.80.2)(@tanstack/react-query@5.80.3(react@19.1.0))(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4))(zod@3.22.4))': + '@rainbow-me/rainbowkit@2.2.5(@tanstack/react-query@5.80.3(react@19.1.0))(@types/react@19.1.6)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(viem@2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4))(wagmi@2.15.5(@tanstack/query-core@5.80.2)(@tanstack/react-query@5.80.3(react@19.1.0))(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4))(zod@3.22.4))': dependencies: '@tanstack/react-query': 5.80.3(react@19.1.0) '@vanilla-extract/css': 1.15.5(babel-plugin-macros@3.1.0) @@ -5891,31 +5907,31 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) react-remove-scroll: 2.6.2(@types/react@19.1.6)(react@19.1.0) - ua-parser-js: 1.0.40 - viem: 2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) - wagmi: 2.15.5(@tanstack/query-core@5.80.2)(@tanstack/react-query@5.80.3(react@19.1.0))(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4))(zod@3.22.4) + ua-parser-js: 1.0.41 + viem: 2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + wagmi: 2.15.5(@tanstack/query-core@5.80.2)(@tanstack/react-query@5.80.3(react@19.1.0))(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4))(zod@3.22.4) transitivePeerDependencies: - '@types/react' - babel-plugin-macros - '@reown/appkit-common@1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + '@reown/appkit-common@1.7.8(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: big.js: 6.2.2 dayjs: 1.11.13 - viem: 2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + viem: 2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) transitivePeerDependencies: - bufferutil - typescript - utf-8-validate - zod - '@reown/appkit-controllers@1.7.8(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + '@reown/appkit-controllers@1.7.8(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) - '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) valtio: 1.13.2(@types/react@19.1.6)(react@19.1.0) - viem: 2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + viem: 2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -5931,6 +5947,7 @@ snapshots: - '@types/react' - '@upstash/redis' - '@vercel/blob' + - '@vercel/functions' - '@vercel/kv' - aws4fetch - bufferutil @@ -5943,12 +5960,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.7.8(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + '@reown/appkit-pay@1.7.8(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) - '@reown/appkit-controllers': 1.7.8(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) - '@reown/appkit-ui': 1.7.8(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) - '@reown/appkit-utils': 1.7.8(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.6)(react@19.1.0))(zod@3.22.4) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-controllers': 1.7.8(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-ui': 1.7.8(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-utils': 1.7.8(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.6)(react@19.1.0))(zod@3.22.4) lit: 3.3.0 valtio: 1.13.2(@types/react@19.1.6)(react@19.1.0) transitivePeerDependencies: @@ -5966,6 +5983,7 @@ snapshots: - '@types/react' - '@upstash/redis' - '@vercel/blob' + - '@vercel/functions' - '@vercel/kv' - aws4fetch - bufferutil @@ -5982,13 +6000,13 @@ snapshots: dependencies: buffer: 6.0.3 - '@reown/appkit-scaffold-ui@1.7.8(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.6)(react@19.1.0))(zod@3.22.4)': + '@reown/appkit-scaffold-ui@1.7.8(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.6)(react@19.1.0))(zod@3.22.4)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) - '@reown/appkit-controllers': 1.7.8(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) - '@reown/appkit-ui': 1.7.8(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) - '@reown/appkit-utils': 1.7.8(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.6)(react@19.1.0))(zod@3.22.4) - '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-controllers': 1.7.8(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-ui': 1.7.8(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-utils': 1.7.8(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.6)(react@19.1.0))(zod@3.22.4) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10) lit: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -6005,6 +6023,7 @@ snapshots: - '@types/react' - '@upstash/redis' - '@vercel/blob' + - '@vercel/functions' - '@vercel/kv' - aws4fetch - bufferutil @@ -6018,11 +6037,11 @@ snapshots: - valtio - zod - '@reown/appkit-ui@1.7.8(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + '@reown/appkit-ui@1.7.8(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) - '@reown/appkit-controllers': 1.7.8(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) - '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-controllers': 1.7.8(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10) lit: 3.3.0 qrcode: 1.5.3 transitivePeerDependencies: @@ -6040,6 +6059,7 @@ snapshots: - '@types/react' - '@upstash/redis' - '@vercel/blob' + - '@vercel/functions' - '@vercel/kv' - aws4fetch - bufferutil @@ -6052,16 +6072,16 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.7.8(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.6)(react@19.1.0))(zod@3.22.4)': + '@reown/appkit-utils@1.7.8(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.6)(react@19.1.0))(zod@3.22.4)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) - '@reown/appkit-controllers': 1.7.8(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-controllers': 1.7.8(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) '@reown/appkit-polyfills': 1.7.8 - '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10) '@walletconnect/logger': 2.1.2 - '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) valtio: 1.13.2(@types/react@19.1.6)(react@19.1.0) - viem: 2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + viem: 2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -6077,6 +6097,7 @@ snapshots: - '@types/react' - '@upstash/redis' - '@vercel/blob' + - '@vercel/functions' - '@vercel/kv' - aws4fetch - bufferutil @@ -6089,9 +6110,9 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-wallet@1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)': + '@reown/appkit-wallet@1.7.8(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) '@reown/appkit-polyfills': 1.7.8 '@walletconnect/logger': 2.1.2 zod: 3.22.4 @@ -6100,21 +6121,21 @@ snapshots: - typescript - utf-8-validate - '@reown/appkit@1.7.8(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + '@reown/appkit@1.7.8(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) - '@reown/appkit-controllers': 1.7.8(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) - '@reown/appkit-pay': 1.7.8(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-controllers': 1.7.8(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-pay': 1.7.8(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) '@reown/appkit-polyfills': 1.7.8 - '@reown/appkit-scaffold-ui': 1.7.8(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.6)(react@19.1.0))(zod@3.22.4) - '@reown/appkit-ui': 1.7.8(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) - '@reown/appkit-utils': 1.7.8(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.6)(react@19.1.0))(zod@3.22.4) - '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@reown/appkit-scaffold-ui': 1.7.8(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.6)(react@19.1.0))(zod@3.22.4) + '@reown/appkit-ui': 1.7.8(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-utils': 1.7.8(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.6)(react@19.1.0))(zod@3.22.4) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10) '@walletconnect/types': 2.21.0 - '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) bs58: 6.0.0 valtio: 1.13.2(@types/react@19.1.6)(react@19.1.0) - viem: 2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + viem: 2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -6130,6 +6151,7 @@ snapshots: - '@types/react' - '@upstash/redis' - '@vercel/blob' + - '@vercel/functions' - '@vercel/kv' - aws4fetch - bufferutil @@ -6144,71 +6166,86 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.9': {} - '@rollup/rollup-android-arm-eabi@4.41.1': + '@rollup/rollup-android-arm-eabi@4.55.1': + optional: true + + '@rollup/rollup-android-arm64@4.55.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.55.1': optional: true - '@rollup/rollup-android-arm64@4.41.1': + '@rollup/rollup-darwin-x64@4.55.1': optional: true - '@rollup/rollup-darwin-arm64@4.41.1': + '@rollup/rollup-freebsd-arm64@4.55.1': optional: true - '@rollup/rollup-darwin-x64@4.41.1': + '@rollup/rollup-freebsd-x64@4.55.1': optional: true - '@rollup/rollup-freebsd-arm64@4.41.1': + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': optional: true - '@rollup/rollup-freebsd-x64@4.41.1': + '@rollup/rollup-linux-arm-musleabihf@4.55.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.41.1': + '@rollup/rollup-linux-arm64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.41.1': + '@rollup/rollup-linux-arm64-musl@4.55.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.41.1': + '@rollup/rollup-linux-loong64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.41.1': + '@rollup/rollup-linux-loong64-musl@4.55.1': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.41.1': + '@rollup/rollup-linux-ppc64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.41.1': + '@rollup/rollup-linux-ppc64-musl@4.55.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.41.1': + '@rollup/rollup-linux-riscv64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-riscv64-musl@4.41.1': + '@rollup/rollup-linux-riscv64-musl@4.55.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.41.1': + '@rollup/rollup-linux-s390x-gnu@4.55.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.41.1': + '@rollup/rollup-linux-x64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-x64-musl@4.41.1': + '@rollup/rollup-linux-x64-musl@4.55.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.41.1': + '@rollup/rollup-openbsd-x64@4.55.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.41.1': + '@rollup/rollup-openharmony-arm64@4.55.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.41.1': + '@rollup/rollup-win32-arm64-msvc@4.55.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.55.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.55.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.55.1': optional: true '@rtsao/scc@1.1.0': {} - '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) events: 3.3.0 transitivePeerDependencies: - bufferutil @@ -6216,10 +6253,10 @@ snapshots: - utf-8-validate - zod - '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: '@safe-global/safe-gateway-typescript-sdk': 3.23.1 - viem: 2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + viem: 2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) transitivePeerDependencies: - bufferutil - typescript @@ -6228,7 +6265,7 @@ snapshots: '@safe-global/safe-gateway-typescript-sdk@3.23.1': {} - '@safe-global/safe-smart-account@https://codeload.github.com/safe-global/safe-smart-account/tar.gz/b75e250': {} + '@safe-global/safe-smart-account@git+https://git@github.com:safe-global/safe-smart-account.git#b75e2509ba4968f97c427f2657996a0711b7a4cf': {} '@scure/base@1.1.9': {} @@ -6254,7 +6291,7 @@ snapshots: '@scure/bip32@1.7.0': dependencies: - '@noble/curves': 1.9.1 + '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 @@ -6280,14 +6317,14 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} - '@synthetixio/ethereum-wallet-mock@0.0.12(@depay/solana-web3.js@1.98.2)(@depay/web3-blockchains@9.8.6)(@playwright/test@1.53.0)(bufferutil@4.0.9)(ethers@5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + '@synthetixio/ethereum-wallet-mock@0.0.12(@depay/solana-web3.js@1.98.3)(@depay/web3-blockchains@9.8.10)(@playwright/test@1.53.0)(bufferutil@4.1.0)(ethers@5.8.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: - '@depay/web3-client': 10.18.6(@depay/solana-web3.js@1.98.2)(@depay/web3-blockchains@9.8.6)(ethers@5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@depay/web3-mock': 14.19.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@depay/web3-mock-evm': 14.19.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@depay/web3-client': 10.18.6(@depay/solana-web3.js@1.98.3)(@depay/web3-blockchains@9.8.10)(ethers@5.8.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@depay/web3-mock': 14.19.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@depay/web3-mock-evm': 14.19.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) '@playwright/test': 1.53.0 '@synthetixio/synpress-core': 0.0.12(@playwright/test@1.53.0) - viem: 2.9.9(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + viem: 2.9.9(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) transitivePeerDependencies: - '@depay/solana-web3.js' - '@depay/web3-blockchains' @@ -6297,7 +6334,7 @@ snapshots: - utf-8-validate - zod - '@synthetixio/synpress-cache@0.0.12(playwright-core@1.53.0)(postcss@8.5.4)(typescript@5.8.3)': + '@synthetixio/synpress-cache@0.0.12(playwright-core@1.53.0)(postcss@8.5.6)(typescript@5.8.3)': dependencies: axios: 1.6.7 chalk: 5.3.0 @@ -6308,7 +6345,7 @@ snapshots: gradient-string: 2.0.2 playwright-core: 1.53.0 progress: 2.0.3 - tsup: 8.0.2(postcss@8.5.4)(typescript@5.8.3) + tsup: 8.0.2(postcss@8.5.6)(typescript@5.8.3) unzip-crx-3: 0.2.0 unzipper: 0.10.14 zod: 3.22.4 @@ -6325,12 +6362,12 @@ snapshots: dependencies: '@playwright/test': 1.53.0 - '@synthetixio/synpress-metamask@0.0.12(@playwright/test@1.53.0)(bufferutil@4.0.9)(playwright-core@1.53.0)(postcss@8.5.4)(typescript@5.8.3)(utf-8-validate@5.0.10)': + '@synthetixio/synpress-metamask@0.0.12(@playwright/test@1.53.0)(bufferutil@4.1.0)(playwright-core@1.53.0)(postcss@8.5.6)(typescript@5.8.3)(utf-8-validate@5.0.10)': dependencies: '@playwright/test': 1.53.0 - '@synthetixio/synpress-cache': 0.0.12(playwright-core@1.53.0)(postcss@8.5.4)(typescript@5.8.3) + '@synthetixio/synpress-cache': 0.0.12(playwright-core@1.53.0)(postcss@8.5.6)(typescript@5.8.3) '@synthetixio/synpress-core': 0.0.12(@playwright/test@1.53.0) - '@viem/anvil': 0.0.7(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@viem/anvil': 0.0.7(bufferutil@4.1.0)(utf-8-validate@5.0.10) fs-extra: 11.2.0 zod: 3.22.4 transitivePeerDependencies: @@ -6345,12 +6382,12 @@ snapshots: - typescript - utf-8-validate - '@synthetixio/synpress-phantom@0.0.12(@playwright/test@1.53.0)(bufferutil@4.0.9)(playwright-core@1.53.0)(postcss@8.5.4)(typescript@5.8.3)(utf-8-validate@5.0.10)': + '@synthetixio/synpress-phantom@0.0.12(@playwright/test@1.53.0)(bufferutil@4.1.0)(playwright-core@1.53.0)(postcss@8.5.6)(typescript@5.8.3)(utf-8-validate@5.0.10)': dependencies: '@playwright/test': 1.53.0 - '@synthetixio/synpress-cache': 0.0.12(playwright-core@1.53.0)(postcss@8.5.4)(typescript@5.8.3) + '@synthetixio/synpress-cache': 0.0.12(playwright-core@1.53.0)(postcss@8.5.6)(typescript@5.8.3) '@synthetixio/synpress-core': 0.0.12(@playwright/test@1.53.0) - '@viem/anvil': 0.0.7(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@viem/anvil': 0.0.7(bufferutil@4.1.0)(utf-8-validate@5.0.10) fs-extra: 11.2.0 zod: 3.22.4 transitivePeerDependencies: @@ -6365,14 +6402,14 @@ snapshots: - typescript - utf-8-validate - '@synthetixio/synpress@4.1.0(@depay/solana-web3.js@1.98.2)(@depay/web3-blockchains@9.8.6)(@playwright/test@1.53.0)(bufferutil@4.0.9)(ethers@5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(playwright-core@1.53.0)(postcss@8.5.4)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + '@synthetixio/synpress@4.1.0(@depay/solana-web3.js@1.98.3)(@depay/web3-blockchains@9.8.10)(@playwright/test@1.53.0)(bufferutil@4.1.0)(ethers@5.8.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(playwright-core@1.53.0)(postcss@8.5.6)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: '@playwright/test': 1.53.0 - '@synthetixio/ethereum-wallet-mock': 0.0.12(@depay/solana-web3.js@1.98.2)(@depay/web3-blockchains@9.8.6)(@playwright/test@1.53.0)(bufferutil@4.0.9)(ethers@5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) - '@synthetixio/synpress-cache': 0.0.12(playwright-core@1.53.0)(postcss@8.5.4)(typescript@5.8.3) + '@synthetixio/ethereum-wallet-mock': 0.0.12(@depay/solana-web3.js@1.98.3)(@depay/web3-blockchains@9.8.10)(@playwright/test@1.53.0)(bufferutil@4.1.0)(ethers@5.8.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@synthetixio/synpress-cache': 0.0.12(playwright-core@1.53.0)(postcss@8.5.6)(typescript@5.8.3) '@synthetixio/synpress-core': 0.0.12(@playwright/test@1.53.0) - '@synthetixio/synpress-metamask': 0.0.12(@playwright/test@1.53.0)(bufferutil@4.0.9)(playwright-core@1.53.0)(postcss@8.5.4)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@synthetixio/synpress-phantom': 0.0.12(@playwright/test@1.53.0)(bufferutil@4.0.9)(playwright-core@1.53.0)(postcss@8.5.4)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@synthetixio/synpress-metamask': 0.0.12(@playwright/test@1.53.0)(bufferutil@4.1.0)(playwright-core@1.53.0)(postcss@8.5.6)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@synthetixio/synpress-phantom': 0.0.12(@playwright/test@1.53.0)(bufferutil@4.1.0)(playwright-core@1.53.0)(postcss@8.5.6)(typescript@5.8.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - '@depay/solana-web3.js' - '@depay/web3-blockchains' @@ -6398,28 +6435,29 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.27.5 - '@babel/types': 7.27.3 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.7 + '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.27.3 + '@babel/types': 7.28.5 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.27.5 - '@babel/types': 7.27.3 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 - '@types/babel__traverse@7.20.7': + '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.27.3 + '@babel/types': 7.28.5 - '@types/chai@5.2.2': + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 '@types/debug@4.1.12': dependencies: @@ -6427,7 +6465,7 @@ snapshots: '@types/deep-eql@4.0.2': {} - '@types/estree@1.0.7': {} + '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} @@ -6435,9 +6473,9 @@ snapshots: '@types/lodash.merge@4.6.9': dependencies: - '@types/lodash': 4.17.16 + '@types/lodash': 4.17.21 - '@types/lodash@4.17.16': {} + '@types/lodash@4.17.21': {} '@types/ms@2.1.0': {} @@ -6447,7 +6485,7 @@ snapshots: '@types/parse-json@4.0.2': {} - '@types/prop-types@15.7.14': {} + '@types/prop-types@15.7.15': {} '@types/react-dom@19.1.6(@types/react@19.1.6)': dependencies: @@ -6459,7 +6497,7 @@ snapshots: '@types/react@19.1.6': dependencies: - csstype: 3.1.3 + csstype: 3.2.3 '@types/tinycolor2@1.4.6': {} @@ -6467,7 +6505,7 @@ snapshots: '@typescript-eslint/eslint-plugin@8.33.1(@typescript-eslint/parser@8.33.1(eslint@9.28.0)(typescript@5.8.3))(eslint@9.28.0)(typescript@5.8.3)': dependencies: - '@eslint-community/regexpp': 4.12.1 + '@eslint-community/regexpp': 4.12.2 '@typescript-eslint/parser': 8.33.1(eslint@9.28.0)(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.33.1 '@typescript-eslint/type-utils': 8.33.1(eslint@9.28.0)(typescript@5.8.3) @@ -6477,7 +6515,7 @@ snapshots: graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.8.3) + ts-api-utils: 2.4.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -6488,7 +6526,7 @@ snapshots: '@typescript-eslint/types': 8.33.1 '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.33.1 - debug: 4.4.1 + debug: 4.4.3 eslint: 9.28.0 typescript: 5.8.3 transitivePeerDependencies: @@ -6498,7 +6536,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.33.1(typescript@5.8.3) '@typescript-eslint/types': 8.33.1 - debug: 4.4.1 + debug: 4.4.3 typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -6516,9 +6554,9 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) '@typescript-eslint/utils': 8.33.1(eslint@9.28.0)(typescript@5.8.3) - debug: 4.4.1 + debug: 4.4.3 eslint: 9.28.0 - ts-api-utils: 2.1.0(typescript@5.8.3) + ts-api-utils: 2.4.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -6531,19 +6569,19 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.33.1(typescript@5.8.3) '@typescript-eslint/types': 8.33.1 '@typescript-eslint/visitor-keys': 8.33.1 - debug: 4.4.1 + debug: 4.4.3 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.7.2 - ts-api-utils: 2.1.0(typescript@5.8.3) + semver: 7.7.3 + ts-api-utils: 2.4.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color '@typescript-eslint/utils@8.33.1(eslint@9.28.0)(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.28.0) '@typescript-eslint/scope-manager': 8.33.1 '@typescript-eslint/types': 8.33.1 '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) @@ -6555,16 +6593,16 @@ snapshots: '@typescript-eslint/visitor-keys@8.33.1': dependencies: '@typescript-eslint/types': 8.33.1 - eslint-visitor-keys: 4.2.0 + eslint-visitor-keys: 4.2.1 '@vanilla-extract/css@1.15.5(babel-plugin-macros@3.1.0)': dependencies: '@emotion/hash': 0.9.2 - '@vanilla-extract/private': 1.0.7 - css-what: 6.1.0 + '@vanilla-extract/private': 1.0.9 + css-what: 6.2.2 cssesc: 3.0.0 - csstype: 3.1.3 - dedent: 1.6.0(babel-plugin-macros@3.1.0) + csstype: 3.2.3 + dedent: 1.7.1(babel-plugin-macros@3.1.0) deep-object-diff: 1.1.9 deepmerge: 4.3.1 lru-cache: 10.4.3 @@ -6576,57 +6614,61 @@ snapshots: '@vanilla-extract/dynamic@2.1.2': dependencies: - '@vanilla-extract/private': 1.0.7 + '@vanilla-extract/private': 1.0.9 - '@vanilla-extract/private@1.0.7': {} + '@vanilla-extract/private@1.0.9': {} '@vanilla-extract/sprinkles@1.6.3(@vanilla-extract/css@1.15.5(babel-plugin-macros@3.1.0))': dependencies: '@vanilla-extract/css': 1.15.5(babel-plugin-macros@3.1.0) - '@viem/anvil@0.0.7(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@viem/anvil@0.0.7(bufferutil@4.1.0)(utf-8-validate@5.0.10)': dependencies: execa: 7.2.0 get-port: 6.1.2 http-proxy: 1.18.1 - ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - debug - utf-8-validate - '@vitejs/plugin-react@4.5.1(vite@6.3.5(@types/node@24.0.1)(terser@5.39.0)(yaml@2.8.0))': + '@vitejs/plugin-react@4.5.1(vite@6.3.5(@types/node@24.0.1)(yaml@2.8.2))': dependencies: - '@babel/core': 7.27.4 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.4) + '@babel/core': 7.28.5 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) '@rolldown/pluginutils': 1.0.0-beta.9 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@24.0.1)(terser@5.39.0)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.0.1)(yaml@2.8.2) transitivePeerDependencies: - supports-color '@vitest/expect@3.2.1': dependencies: - '@types/chai': 5.2.2 + '@types/chai': 5.2.3 '@vitest/spy': 3.2.1 '@vitest/utils': 3.2.1 - chai: 5.2.0 + chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.1(vite@6.3.5(@types/node@24.0.1)(terser@5.39.0)(yaml@2.8.0))': + '@vitest/mocker@3.2.1(vite@6.3.5(@types/node@24.0.1)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.1 estree-walker: 3.0.3 - magic-string: 0.30.17 + magic-string: 0.30.21 optionalDependencies: - vite: 6.3.5(@types/node@24.0.1)(terser@5.39.0)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.0.1)(yaml@2.8.2) '@vitest/pretty-format@3.2.1': dependencies: tinyrainbow: 2.0.0 + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + '@vitest/runner@3.2.1': dependencies: '@vitest/utils': 3.2.1 @@ -6635,29 +6677,29 @@ snapshots: '@vitest/snapshot@3.2.1': dependencies: '@vitest/pretty-format': 3.2.1 - magic-string: 0.30.17 + magic-string: 0.30.21 pathe: 2.0.3 '@vitest/spy@3.2.1': dependencies: - tinyspy: 4.0.3 + tinyspy: 4.0.4 '@vitest/utils@3.2.1': dependencies: '@vitest/pretty-format': 3.2.1 - loupe: 3.1.3 + loupe: 3.2.1 tinyrainbow: 2.0.0 - '@wagmi/connectors@5.8.4(@types/react@19.1.6)(@wagmi/core@2.17.2(@tanstack/query-core@5.80.2)(@types/react@19.1.6)(react@19.1.0)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)))(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4))(zod@3.22.4)': + '@wagmi/connectors@5.8.4(@types/react@19.1.6)(@wagmi/core@2.17.2(@tanstack/query-core@5.80.2)(@types/react@19.1.6)(react@19.1.0)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)))(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4))(zod@3.22.4)': dependencies: '@coinbase/wallet-sdk': 4.3.0 - '@metamask/sdk': 0.32.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) - '@wagmi/core': 2.17.2(@tanstack/query-core@5.80.2)(@types/react@19.1.6)(react@19.1.0)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)) - '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@metamask/sdk': 0.32.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@wagmi/core': 2.17.2(@tanstack/query-core@5.80.2)(@types/react@19.1.6)(react@19.1.0)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)) + '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - viem: 2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + viem: 2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: @@ -6675,6 +6717,7 @@ snapshots: - '@types/react' - '@upstash/redis' - '@vercel/blob' + - '@vercel/functions' - '@vercel/kv' - aws4fetch - bufferutil @@ -6687,11 +6730,11 @@ snapshots: - utf-8-validate - zod - '@wagmi/core@2.17.2(@tanstack/query-core@5.80.2)(@types/react@19.1.6)(react@19.1.0)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4))': + '@wagmi/core@2.17.2(@tanstack/query-core@5.80.2)(@types/react@19.1.6)(react@19.1.0)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.8.3) - viem: 2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + viem: 2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) zustand: 5.0.0(@types/react@19.1.6)(react@19.1.0)(use-sync-external-store@1.4.0(react@19.1.0)) optionalDependencies: '@tanstack/query-core': 5.80.2 @@ -6702,13 +6745,13 @@ snapshots: - react - use-sync-external-store - '@walletconnect/core@2.21.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + '@walletconnect/core@2.21.0(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10) '@walletconnect/keyvaluestorage': 1.1.1 '@walletconnect/logger': 2.1.2 '@walletconnect/relay-api': 1.0.11 @@ -6716,7 +6759,7 @@ snapshots: '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.0 - '@walletconnect/utils': 2.21.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@walletconnect/utils': 2.21.0(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.33.0 events: 3.3.0 @@ -6735,6 +6778,7 @@ snapshots: - '@react-native-async-storage/async-storage' - '@upstash/redis' - '@vercel/blob' + - '@vercel/functions' - '@vercel/kv' - aws4fetch - bufferutil @@ -6745,13 +6789,13 @@ snapshots: - utf-8-validate - zod - '@walletconnect/core@2.21.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + '@walletconnect/core@2.21.1(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10) '@walletconnect/keyvaluestorage': 1.1.1 '@walletconnect/logger': 2.1.2 '@walletconnect/relay-api': 1.0.11 @@ -6759,7 +6803,7 @@ snapshots: '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.1 - '@walletconnect/utils': 2.21.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@walletconnect/utils': 2.21.1(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.33.0 events: 3.3.0 @@ -6778,6 +6822,51 @@ snapshots: - '@react-native-async-storage/async-storage' - '@upstash/redis' - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/core@2.23.1(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + dependencies: + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 3.0.1 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.23.1 + '@walletconnect/utils': 2.23.1(typescript@5.8.3)(zod@3.22.4) + '@walletconnect/window-getters': 1.0.1 + es-toolkit: 1.39.3 + events: 3.3.0 + uint8arrays: 3.1.1 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' - '@vercel/kv' - aws4fetch - bufferutil @@ -6792,18 +6881,18 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/ethereum-provider@2.21.1(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + '@walletconnect/ethereum-provider@2.21.1(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: - '@reown/appkit': 1.7.8(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit': 1.7.8(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) '@walletconnect/jsonrpc-http-connection': 1.0.8 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1 - '@walletconnect/sign-client': 2.21.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@walletconnect/sign-client': 2.21.1(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) '@walletconnect/types': 2.21.1 - '@walletconnect/universal-provider': 2.21.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) - '@walletconnect/utils': 2.21.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@walletconnect/universal-provider': 2.21.1(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@walletconnect/utils': 2.21.1(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -6820,6 +6909,7 @@ snapshots: - '@types/react' - '@upstash/redis' - '@vercel/blob' + - '@vercel/functions' - '@vercel/kv' - aws4fetch - bufferutil @@ -6869,12 +6959,12 @@ snapshots: '@walletconnect/jsonrpc-types': 1.0.4 tslib: 1.14.1 - '@walletconnect/jsonrpc-ws-connection@1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@walletconnect/jsonrpc-ws-connection@1.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10)': dependencies: '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/safe-json': 1.0.2 events: 3.3.0 - ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -6883,7 +6973,7 @@ snapshots: dependencies: '@walletconnect/safe-json': 1.0.2 idb-keyval: 6.2.2 - unstorage: 1.16.0(idb-keyval@6.2.2) + unstorage: 1.17.3(idb-keyval@6.2.2) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -6897,6 +6987,7 @@ snapshots: - '@planetscale/database' - '@upstash/redis' - '@vercel/blob' + - '@vercel/functions' - '@vercel/kv' - aws4fetch - db0 @@ -6908,6 +6999,11 @@ snapshots: '@walletconnect/safe-json': 1.0.2 pino: 7.11.0 + '@walletconnect/logger@3.0.1': + dependencies: + '@walletconnect/safe-json': 1.0.2 + pino: 10.0.0 + '@walletconnect/relay-api@1.0.11': dependencies: '@walletconnect/jsonrpc-types': 1.0.4 @@ -6918,22 +7014,22 @@ snapshots: '@noble/hashes': 1.7.0 '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 - uint8arrays: 3.1.0 + uint8arrays: 3.1.1 '@walletconnect/safe-json@1.0.2': dependencies: tslib: 1.14.1 - '@walletconnect/sign-client@2.21.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + '@walletconnect/sign-client@2.21.0(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: - '@walletconnect/core': 2.21.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@walletconnect/core': 2.21.0(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.0 - '@walletconnect/utils': 2.21.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@walletconnect/utils': 2.21.0(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -6949,6 +7045,7 @@ snapshots: - '@react-native-async-storage/async-storage' - '@upstash/redis' - '@vercel/blob' + - '@vercel/functions' - '@vercel/kv' - aws4fetch - bufferutil @@ -6959,16 +7056,16 @@ snapshots: - utf-8-validate - zod - '@walletconnect/sign-client@2.21.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + '@walletconnect/sign-client@2.21.1(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: - '@walletconnect/core': 2.21.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@walletconnect/core': 2.21.1(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.1 - '@walletconnect/utils': 2.21.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@walletconnect/utils': 2.21.1(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -6984,6 +7081,43 @@ snapshots: - '@react-native-async-storage/async-storage' - '@upstash/redis' - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/sign-client@2.23.1(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + dependencies: + '@walletconnect/core': 2.23.1(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/logger': 3.0.1 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.23.1 + '@walletconnect/utils': 2.23.1(typescript@5.8.3)(zod@3.22.4) + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' - '@vercel/kv' - aws4fetch - bufferutil @@ -7020,6 +7154,7 @@ snapshots: - '@react-native-async-storage/async-storage' - '@upstash/redis' - '@vercel/blob' + - '@vercel/functions' - '@vercel/kv' - aws4fetch - db0 @@ -7048,13 +7183,43 @@ snapshots: - '@react-native-async-storage/async-storage' - '@upstash/redis' - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - ioredis + - uploadthing + + '@walletconnect/types@2.23.1': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 3.0.1 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' - '@vercel/kv' - aws4fetch - db0 - ioredis - uploadthing - '@walletconnect/universal-provider@2.21.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + '@walletconnect/universal-provider@2.21.0(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/jsonrpc-http-connection': 1.0.8 @@ -7063,9 +7228,9 @@ snapshots: '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1 '@walletconnect/logger': 2.1.2 - '@walletconnect/sign-client': 2.21.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@walletconnect/sign-client': 2.21.0(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) '@walletconnect/types': 2.21.0 - '@walletconnect/utils': 2.21.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@walletconnect/utils': 2.21.0(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) es-toolkit: 1.33.0 events: 3.3.0 transitivePeerDependencies: @@ -7082,6 +7247,7 @@ snapshots: - '@react-native-async-storage/async-storage' - '@upstash/redis' - '@vercel/blob' + - '@vercel/functions' - '@vercel/kv' - aws4fetch - bufferutil @@ -7093,7 +7259,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/universal-provider@2.21.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + '@walletconnect/universal-provider@2.21.1(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/jsonrpc-http-connection': 1.0.8 @@ -7102,9 +7268,9 @@ snapshots: '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1 '@walletconnect/logger': 2.1.2 - '@walletconnect/sign-client': 2.21.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@walletconnect/sign-client': 2.21.1(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) '@walletconnect/types': 2.21.1 - '@walletconnect/utils': 2.21.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@walletconnect/utils': 2.21.1(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) es-toolkit: 1.33.0 events: 3.3.0 transitivePeerDependencies: @@ -7121,6 +7287,7 @@ snapshots: - '@react-native-async-storage/async-storage' - '@upstash/redis' - '@vercel/blob' + - '@vercel/functions' - '@vercel/kv' - aws4fetch - bufferutil @@ -7132,7 +7299,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/utils@2.21.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + '@walletconnect/utils@2.21.0(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: '@noble/ciphers': 1.2.1 '@noble/curves': 1.8.1 @@ -7150,7 +7317,7 @@ snapshots: detect-browser: 5.3.0 query-string: 7.1.3 uint8arrays: 3.1.0 - viem: 2.23.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + viem: 2.23.2(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -7165,6 +7332,7 @@ snapshots: - '@react-native-async-storage/async-storage' - '@upstash/redis' - '@vercel/blob' + - '@vercel/functions' - '@vercel/kv' - aws4fetch - bufferutil @@ -7175,7 +7343,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/utils@2.21.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + '@walletconnect/utils@2.21.1(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: '@noble/ciphers': 1.2.1 '@noble/curves': 1.8.1 @@ -7193,7 +7361,7 @@ snapshots: detect-browser: 5.3.0 query-string: 7.1.3 uint8arrays: 3.1.0 - viem: 2.23.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + viem: 2.23.2(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -7208,6 +7376,7 @@ snapshots: - '@react-native-async-storage/async-storage' - '@upstash/redis' - '@vercel/blob' + - '@vercel/functions' - '@vercel/kv' - aws4fetch - bufferutil @@ -7218,6 +7387,51 @@ snapshots: - utf-8-validate - zod + '@walletconnect/utils@2.23.1(typescript@5.8.3)(zod@3.22.4)': + dependencies: + '@msgpack/msgpack': 3.1.2 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1 + '@walletconnect/logger': 3.0.1 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.23.1 + '@walletconnect/window-getters': 1.0.1 + '@walletconnect/window-metadata': 1.0.1 + blakejs: 1.2.1 + bs58: 6.0.0 + detect-browser: 5.3.0 + ox: 0.9.3(typescript@5.8.3)(zod@3.22.4) + uint8arrays: 3.1.1 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - ioredis + - typescript + - uploadthing + - zod + '@walletconnect/window-getters@1.0.1': dependencies: tslib: 1.14.1 @@ -7237,15 +7451,20 @@ snapshots: typescript: 5.8.3 zod: 3.22.4 - acorn-jsx@5.3.2(acorn@8.14.1): + abitype@1.2.3(typescript@5.8.3)(zod@3.22.4): + optionalDependencies: + typescript: 5.8.3 + zod: 3.22.4 + + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: - acorn: 8.14.1 + acorn: 8.15.0 - acorn@8.14.1: {} + acorn@8.15.0: {} aes-js@3.0.0: {} - agent-base@7.1.3: {} + agent-base@7.1.4: {} ajv@6.12.6: dependencies: @@ -7256,13 +7475,13 @@ snapshots: ansi-regex@5.0.1: {} - ansi-regex@6.1.0: {} + ansi-regex@6.2.2: {} ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - ansi-styles@6.2.1: {} + ansi-styles@6.2.3: {} any-promise@1.3.0: {} @@ -7280,21 +7499,12 @@ snapshots: call-bound: 1.0.4 is-array-buffer: 3.0.5 - array-includes@3.1.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - is-string: 1.1.1 - array-includes@3.1.9: dependencies: call-bind: 1.0.8 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 is-string: 1.1.1 @@ -7302,11 +7512,12 @@ snapshots: array-union@2.1.0: {} - array.prototype.findlastindex@1.2.5: + array.prototype.findlastindex@1.2.6: dependencies: call-bind: 1.0.8 + call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.1 es-errors: 1.3.0 es-object-atoms: 1.1.1 es-shim-unscopables: 1.1.0 @@ -7315,14 +7526,14 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.1 es-shim-unscopables: 1.1.0 array.prototype.flatmap@1.3.3: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.1 es-shim-unscopables: 1.1.0 arraybuffer.prototype.slice@1.0.4: @@ -7330,7 +7541,7 @@ snapshots: array-buffer-byte-length: 1.0.2 call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.1 es-errors: 1.3.0 get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 @@ -7353,12 +7564,12 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - axe-core@4.10.3: {} + axe-core@4.11.1: {} axios@1.6.7: dependencies: - follow-redirects: 1.15.9 - form-data: 4.0.3 + follow-redirects: 1.15.11 + form-data: 4.0.5 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug @@ -7367,9 +7578,9 @@ snapshots: babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.28.4 cosmiconfig: 7.1.0 - resolve: 1.22.10 + resolve: 1.22.11 balanced-match@1.0.2: {} @@ -7379,6 +7590,8 @@ snapshots: base64-js@1.5.1: {} + baseline-browser-mapping@2.9.11: {} + bech32@1.1.4: {} big-integer@1.6.52: {} @@ -7392,20 +7605,22 @@ snapshots: buffers: 0.1.1 chainsaw: 0.1.0 + blakejs@1.2.1: {} + bluebird@3.4.7: {} bn.js@4.12.2: {} bn.js@5.2.2: {} - bowser@2.11.0: {} + bowser@2.13.1: {} - brace-expansion@1.1.11: + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.1: + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -7415,12 +7630,13 @@ snapshots: brorand@1.1.0: {} - browserslist@4.25.0: + browserslist@4.28.1: dependencies: - caniuse-lite: 1.0.30001721 - electron-to-chromium: 1.5.165 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.0) + baseline-browser-mapping: 2.9.11 + caniuse-lite: 1.0.30001762 + electron-to-chromium: 1.5.267 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) bs58@5.0.0: dependencies: @@ -7430,9 +7646,6 @@ snapshots: dependencies: base-x: 5.0.1 - buffer-from@1.1.2: - optional: true - buffer-indexof-polyfill@1.0.2: {} buffer@6.0.3: @@ -7442,7 +7655,7 @@ snapshots: buffers@0.1.1: {} - bufferutil@4.0.9: + bufferutil@4.1.0: dependencies: node-gyp-build: 4.8.4 @@ -7474,15 +7687,15 @@ snapshots: camelcase@5.3.1: {} - caniuse-lite@1.0.30001721: {} + caniuse-lite@1.0.30001762: {} - chai@5.2.0: + chai@5.3.3: dependencies: assertion-error: 2.0.1 - check-error: 2.1.1 + check-error: 2.1.3 deep-eql: 5.0.2 - loupe: 3.1.3 - pathval: 2.0.0 + loupe: 3.2.1 + pathval: 2.0.1 chainsaw@0.1.0: dependencies: @@ -7495,7 +7708,7 @@ snapshots: chalk@5.3.0: {} - check-error@2.1.1: {} + check-error@2.1.3: {} chokidar@3.6.0: dependencies: @@ -7537,9 +7750,6 @@ snapshots: commander@12.0.0: {} - commander@2.20.3: - optional: true - commander@4.1.1: {} concat-map@0.0.1: {} @@ -7550,7 +7760,7 @@ snapshots: cookie-es@1.2.2: {} - cookie@1.0.2: {} + cookie@1.1.1: {} core-util-is@1.0.3: {} @@ -7586,16 +7796,16 @@ snapshots: dependencies: uncrypto: 0.1.3 - css-what@6.1.0: {} + css-what@6.2.2: {} cssesc@3.0.0: {} - cssstyle@4.3.1: + cssstyle@4.6.0: dependencies: '@asamuzakjp/css-color': 3.2.0 rrweb-cssom: 0.8.0 - csstype@3.1.3: {} + csstype@3.2.3: {} damerau-levenshtein@1.0.8: {} @@ -7624,7 +7834,7 @@ snapshots: date-fns@2.30.0: dependencies: - '@babel/runtime': 7.27.4 + '@babel/runtime': 7.28.4 dayjs@1.11.13: {} @@ -7632,25 +7842,17 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.3.7: - dependencies: - ms: 2.1.3 - - debug@4.4.0: - dependencies: - ms: 2.1.3 - - debug@4.4.1: + debug@4.4.3: dependencies: ms: 2.1.3 decamelize@1.2.0: {} - decimal.js@10.5.0: {} + decimal.js@10.6.0: {} decode-uri-component@0.2.2: {} - dedent@1.6.0(babel-plugin-macros@3.1.0): + dedent@1.7.1(babel-plugin-macros@3.1.0): optionalDependencies: babel-plugin-macros: 3.1.0 @@ -7700,10 +7902,10 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.27.4 - csstype: 3.1.3 + '@babel/runtime': 7.28.4 + csstype: 3.2.3 - dotenv@16.5.0: {} + dotenv@16.6.1: {} dunder-proto@1.0.1: dependencies: @@ -7717,21 +7919,21 @@ snapshots: duplexify@4.1.3: dependencies: - end-of-stream: 1.4.4 + end-of-stream: 1.4.5 inherits: 2.0.4 readable-stream: 3.6.2 stream-shift: 1.0.3 eastasianwidth@0.2.0: {} - eciesjs@0.4.15: + eciesjs@0.4.16: dependencies: - '@ecies/ciphers': 0.2.3(@noble/ciphers@1.3.0) + '@ecies/ciphers': 0.2.5(@noble/ciphers@1.3.0) '@noble/ciphers': 1.3.0 - '@noble/curves': 1.9.1 + '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 - electron-to-chromium@1.5.165: {} + electron-to-chromium@1.5.267: {} elliptic@6.6.1: dependencies: @@ -7749,16 +7951,16 @@ snapshots: encode-utf8@1.0.3: {} - end-of-stream@1.4.4: + end-of-stream@1.4.5: dependencies: once: 1.4.0 - engine.io-client@6.6.3(bufferutil@4.0.9)(utf-8-validate@5.0.10): + engine.io-client@6.6.4(bufferutil@4.1.0)(utf-8-validate@5.0.10): dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.3.7 + debug: 4.4.3 engine.io-parser: 5.2.3 - ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) xmlhttprequest-ssl: 2.1.2 transitivePeerDependencies: - bufferutil @@ -7767,67 +7969,13 @@ snapshots: engine.io-parser@5.2.3: {} - entities@6.0.0: {} + entities@6.0.1: {} - error-ex@1.3.2: + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 - es-abstract@1.23.9: - dependencies: - array-buffer-byte-length: 1.0.2 - arraybuffer.prototype.slice: 1.0.4 - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.4 - data-view-buffer: 1.0.2 - data-view-byte-length: 1.0.2 - data-view-byte-offset: 1.0.1 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.0 - function.prototype.name: 1.1.8 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - get-symbol-description: 1.1.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - internal-slot: 1.1.0 - is-array-buffer: 3.0.5 - is-callable: 1.2.7 - is-data-view: 1.0.2 - is-regex: 1.2.1 - is-shared-array-buffer: 1.0.4 - is-string: 1.1.1 - is-typed-array: 1.1.15 - is-weakref: 1.1.1 - math-intrinsics: 1.1.0 - object-inspect: 1.13.4 - object-keys: 1.1.1 - object.assign: 4.1.7 - own-keys: 1.0.1 - regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.3 - safe-push-apply: 1.0.0 - safe-regex-test: 1.1.0 - set-proto: 1.0.0 - string.prototype.trim: 1.2.10 - string.prototype.trimend: 1.0.9 - string.prototype.trimstart: 1.0.8 - typed-array-buffer: 1.0.3 - typed-array-byte-length: 1.0.3 - typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.7 - unbox-primitive: 1.1.0 - which-typed-array: 1.1.18 - - es-abstract@1.24.0: + es-abstract@1.24.1: dependencies: array-buffer-byte-length: 1.0.2 arraybuffer.prototype.slice: 1.0.4 @@ -7913,6 +8061,8 @@ snapshots: es-toolkit@1.33.0: {} + es-toolkit@1.39.3: {} + esbuild@0.19.12: optionalDependencies: '@esbuild/aix-ppc64': 0.19.12 @@ -7965,33 +8115,34 @@ snapshots: '@esbuild/win32-ia32': 0.20.0 '@esbuild/win32-x64': 0.20.0 - esbuild@0.25.0: + esbuild@0.25.12: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.0 - '@esbuild/android-arm': 0.25.0 - '@esbuild/android-arm64': 0.25.0 - '@esbuild/android-x64': 0.25.0 - '@esbuild/darwin-arm64': 0.25.0 - '@esbuild/darwin-x64': 0.25.0 - '@esbuild/freebsd-arm64': 0.25.0 - '@esbuild/freebsd-x64': 0.25.0 - '@esbuild/linux-arm': 0.25.0 - '@esbuild/linux-arm64': 0.25.0 - '@esbuild/linux-ia32': 0.25.0 - '@esbuild/linux-loong64': 0.25.0 - '@esbuild/linux-mips64el': 0.25.0 - '@esbuild/linux-ppc64': 0.25.0 - '@esbuild/linux-riscv64': 0.25.0 - '@esbuild/linux-s390x': 0.25.0 - '@esbuild/linux-x64': 0.25.0 - '@esbuild/netbsd-arm64': 0.25.0 - '@esbuild/netbsd-x64': 0.25.0 - '@esbuild/openbsd-arm64': 0.25.0 - '@esbuild/openbsd-x64': 0.25.0 - '@esbuild/sunos-x64': 0.25.0 - '@esbuild/win32-arm64': 0.25.0 - '@esbuild/win32-ia32': 0.25.0 - '@esbuild/win32-x64': 0.25.0 + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 escalade@3.2.0: {} @@ -8009,11 +8160,11 @@ snapshots: dependencies: debug: 3.2.7 is-core-module: 2.16.1 - resolve: 1.22.10 + resolve: 1.22.11 transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.33.1(eslint@9.28.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.28.0): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.33.1(eslint@9.28.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.28.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -8026,15 +8177,15 @@ snapshots: eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@9.28.0)(typescript@5.8.3))(eslint@9.28.0): dependencies: '@rtsao/scc': 1.1.0 - array-includes: 3.1.8 - array.prototype.findlastindex: 1.2.5 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 array.prototype.flat: 1.3.3 array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 eslint: 9.28.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.33.1(eslint@9.28.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.28.0) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.33.1(eslint@9.28.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.28.0) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -8058,7 +8209,7 @@ snapshots: array-includes: 3.1.9 array.prototype.flatmap: 1.3.3 ast-types-flow: 0.0.8 - axe-core: 4.10.3 + axe-core: 4.11.1 axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 @@ -8075,8 +8226,8 @@ snapshots: dependencies: eslint: 9.28.0 prettier: 3.5.3 - prettier-linter-helpers: 1.0.0 - synckit: 0.11.8 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.11 optionalDependencies: eslint-config-prettier: 10.1.5(eslint@9.28.0) @@ -8088,39 +8239,39 @@ snapshots: dependencies: eslint: 9.28.0 - eslint-scope@8.3.0: + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.2.0: {} + eslint-visitor-keys@4.2.1: {} eslint@9.28.0: dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0) - '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.20.0 - '@eslint/config-helpers': 0.2.2 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.28.0) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.20.1 + '@eslint/config-helpers': 0.2.3 '@eslint/core': 0.14.0 - '@eslint/eslintrc': 3.3.1 + '@eslint/eslintrc': 3.3.3 '@eslint/js': 9.28.0 - '@eslint/plugin-kit': 0.3.1 - '@humanfs/node': 0.16.6 + '@eslint/plugin-kit': 0.3.5 + '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1 + debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint-scope: 8.3.0 - eslint-visitor-keys: 4.2.0 - espree: 10.3.0 - esquery: 1.6.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 8.0.0 @@ -8137,13 +8288,13 @@ snapshots: transitivePeerDependencies: - supports-color - espree@10.3.0: + espree@10.4.0: dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) - eslint-visitor-keys: 4.2.0 + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 - esquery@1.6.0: + esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -8155,7 +8306,7 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 esutils@2.0.3: {} @@ -8193,7 +8344,7 @@ snapshots: '@scure/bip32': 1.4.0 '@scure/bip39': 1.3.0 - ethers@5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): + ethers@5.8.0(bufferutil@4.1.0)(utf-8-validate@5.0.10): dependencies: '@ethersproject/abi': 5.8.0 '@ethersproject/abstract-provider': 5.8.0 @@ -8213,7 +8364,7 @@ snapshots: '@ethersproject/networks': 5.8.0 '@ethersproject/pbkdf2': 5.8.0 '@ethersproject/properties': 5.8.0 - '@ethersproject/providers': 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@ethersproject/providers': 5.8.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) '@ethersproject/random': 5.8.0 '@ethersproject/rlp': 5.8.0 '@ethersproject/sha2': 5.8.0 @@ -8261,7 +8412,7 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 3.0.0 - expect-type@1.2.1: {} + expect-type@1.3.0: {} extension-port-stream@3.0.0: dependencies: @@ -8288,13 +8439,13 @@ snapshots: fast-safe-stringify@2.1.1: {} - fastq@1.19.1: + fastq@1.20.1: dependencies: reusify: 1.1.0 - fdir@6.4.5(picomatch@4.0.2): + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: - picomatch: 4.0.2 + picomatch: 4.0.3 file-entry-cache@8.0.0: dependencies: @@ -8325,7 +8476,7 @@ snapshots: flatted@3.3.3: {} - follow-redirects@1.15.9: {} + follow-redirects@1.15.11: {} for-each@0.3.5: dependencies: @@ -8336,7 +8487,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data@4.0.3: + form-data@4.0.5: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 @@ -8347,7 +8498,7 @@ snapshots: fs-extra@11.2.0: dependencies: graceful-fs: 4.2.11 - jsonfile: 6.1.0 + jsonfile: 6.2.0 universalify: 2.0.1 fs.realpath@1.0.0: {} @@ -8378,6 +8529,8 @@ snapshots: functions-have-names@1.2.3: {} + generator-function@2.0.1: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -8437,8 +8590,6 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 - globals@11.12.0: {} - globals@14.0.0: {} globals@16.2.0: {} @@ -8468,16 +8619,16 @@ snapshots: graphemer@1.4.0: {} - h3@1.15.3: + h3@1.15.4: dependencies: cookie-es: 1.2.2 crossws: 0.3.5 defu: 6.1.4 destr: 2.0.5 iron-webcrypto: 1.2.1 - node-mock-http: 1.0.0 + node-mock-http: 1.0.4 radix3: 1.1.2 - ufo: 1.6.1 + ufo: 1.6.2 uncrypto: 0.1.3 has-bigints@1.1.0: {} @@ -8523,23 +8674,23 @@ snapshots: http-proxy-agent@7.0.2: dependencies: - agent-base: 7.1.3 - debug: 4.4.1 + agent-base: 7.1.4 + debug: 4.4.3 transitivePeerDependencies: - supports-color http-proxy@1.18.1: dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.9 + follow-redirects: 1.15.11 requires-port: 1.0.0 transitivePeerDependencies: - debug https-proxy-agent@7.0.6: dependencies: - agent-base: 7.1.3 - debug: 4.4.1 + agent-base: 7.1.4 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -8644,9 +8795,10 @@ snapshots: is-fullwidth-code-point@3.0.0: {} - is-generator-function@1.1.0: + is-generator-function@1.1.2: dependencies: call-bound: 1.0.4 + generator-function: 2.0.1 get-proto: 1.0.1 has-tostringtag: 1.0.2 safe-regex-test: 1.1.0 @@ -8698,7 +8850,7 @@ snapshots: is-typed-array@1.1.15: dependencies: - which-typed-array: 1.1.18 + which-typed-array: 1.1.19 is-weakmap@2.0.2: {} @@ -8719,17 +8871,17 @@ snapshots: isexe@3.1.1: {} - isows@1.0.3(ws@8.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + isows@1.0.3(ws@8.13.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)): dependencies: - ws: 8.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.13.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) - isows@1.0.6(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + isows@1.0.6(ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)): dependencies: - ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) - isows@1.0.7(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + isows@1.0.7(ws@8.18.2(bufferutil@4.1.0)(utf-8-validate@5.0.10)): dependencies: - ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.18.2(bufferutil@4.1.0)(utf-8-validate@5.0.10) jackspeak@2.3.6: dependencies: @@ -8743,20 +8895,20 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@4.1.0: + js-yaml@4.1.1: dependencies: argparse: 2.0.1 - jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): + jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10): dependencies: - cssstyle: 4.3.1 + cssstyle: 4.6.0 data-urls: 5.0.0 - decimal.js: 10.5.0 + decimal.js: 10.6.0 html-encoding-sniffer: 4.0.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.20 + nwsapi: 2.2.23 parse5: 7.3.0 rrweb-cssom: 0.8.0 saxes: 6.0.0 @@ -8767,7 +8919,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -8797,7 +8949,7 @@ snapshots: json5@2.2.3: {} - jsonfile@6.1.0: + jsonfile@6.2.0: dependencies: universalify: 2.0.1 optionalDependencies: @@ -8850,21 +9002,21 @@ snapshots: listenercount@1.0.1: {} - lit-element@4.2.0: + lit-element@4.2.2: dependencies: - '@lit-labs/ssr-dom-shim': 1.3.0 - '@lit/reactive-element': 2.1.0 - lit-html: 3.3.0 + '@lit-labs/ssr-dom-shim': 1.5.0 + '@lit/reactive-element': 2.1.2 + lit-html: 3.3.2 - lit-html@3.3.0: + lit-html@3.3.2: dependencies: '@types/trusted-types': 2.0.7 lit@3.3.0: dependencies: - '@lit/reactive-element': 2.1.0 - lit-element: 4.2.0 - lit-html: 3.3.0 + '@lit/reactive-element': 2.1.2 + lit-element: 4.2.2 + lit-html: 3.3.2 load-tsconfig@0.2.5: {} @@ -8884,7 +9036,7 @@ snapshots: dependencies: js-tokens: 4.0.0 - loupe@3.1.3: {} + loupe@3.2.1: {} lru-cache@10.4.3: {} @@ -8892,15 +9044,19 @@ snapshots: dependencies: yallist: 3.1.1 - magic-string@0.30.17: + lucide-react@0.562.0(react@19.1.0): + dependencies: + react: 19.1.0 + + magic-string@0.30.21: dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 math-intrinsics@1.1.0: {} media-query-parser@2.0.2: dependencies: - '@babel/runtime': 7.27.4 + '@babel/runtime': 7.28.4 merge-stream@2.0.0: {} @@ -8929,11 +9085,11 @@ snapshots: minimatch@3.1.2: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 1.1.12 minimatch@9.0.5: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimist@1.2.8: {} @@ -8965,7 +9121,7 @@ snapshots: node-addon-api@2.0.2: {} - node-fetch-native@1.6.6: {} + node-fetch-native@1.6.7: {} node-fetch@2.7.0: dependencies: @@ -8973,9 +9129,9 @@ snapshots: node-gyp-build@4.8.4: {} - node-mock-http@1.0.0: {} + node-mock-http@1.0.4: {} - node-releases@2.0.19: {} + node-releases@2.0.27: {} normalize-path@3.0.0: {} @@ -8987,11 +9143,11 @@ snapshots: dependencies: path-key: 4.0.0 - nwsapi@2.2.20: {} + nwsapi@2.2.23: {} obj-multiplex@1.0.0: dependencies: - end-of-stream: 1.4.4 + end-of-stream: 1.4.5 once: 1.4.0 readable-stream: 2.3.8 @@ -9016,14 +9172,14 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.1 es-object-atoms: 1.1.1 object.groupby@1.0.3: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.1 object.values@1.2.1: dependencies: @@ -9032,14 +9188,16 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 - ofetch@1.4.1: + ofetch@1.5.1: dependencies: destr: 2.0.5 - node-fetch-native: 1.6.6 - ufo: 1.6.1 + node-fetch-native: 1.6.7 + ufo: 1.6.2 on-exit-leak-free@0.2.0: {} + on-exit-leak-free@2.1.2: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -9069,11 +9227,11 @@ snapshots: ox@0.6.7(typescript@5.8.3)(zod@3.22.4): dependencies: - '@adraffy/ens-normalize': 1.11.0 - '@noble/curves': 1.8.1 - '@noble/hashes': 1.7.1 - '@scure/bip32': 1.6.2 - '@scure/bip39': 1.5.4 + '@adraffy/ens-normalize': 1.11.1 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 abitype: 1.0.8(typescript@5.8.3)(zod@3.22.4) eventemitter3: 5.0.1 optionalDependencies: @@ -9083,9 +9241,9 @@ snapshots: ox@0.7.1(typescript@5.8.3)(zod@3.22.4): dependencies: - '@adraffy/ens-normalize': 1.11.0 + '@adraffy/ens-normalize': 1.11.1 '@noble/ciphers': 1.3.0 - '@noble/curves': 1.9.1 + '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 @@ -9096,6 +9254,21 @@ snapshots: transitivePeerDependencies: - zod + ox@0.9.3(typescript@5.8.3)(zod@3.22.4): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.8.3)(zod@3.22.4) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - zod + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -9122,14 +9295,14 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.26.2 - error-ex: 1.3.2 + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 parse5@7.3.0: dependencies: - entities: 6.0.0 + entities: 6.0.1 path-exists@4.0.0: {} @@ -9150,13 +9323,13 @@ snapshots: pathe@2.0.3: {} - pathval@2.0.0: {} + pathval@2.0.1: {} picocolors@1.1.1: {} picomatch@2.3.1: {} - picomatch@4.0.2: {} + picomatch@4.0.3: {} pify@3.0.0: {} @@ -9167,8 +9340,28 @@ snapshots: duplexify: 4.1.3 split2: 4.2.0 + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + pino-std-serializers@4.0.0: {} + pino-std-serializers@7.0.0: {} + + pino@10.0.0: + dependencies: + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + slow-redact: 0.3.2 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + pino@7.11.0: dependencies: atomic-sleep: 1.0.0 @@ -9199,24 +9392,24 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-load-config@4.0.2(postcss@8.5.4): + postcss-load-config@4.0.2(postcss@8.5.6): dependencies: lilconfig: 3.1.3 - yaml: 2.8.0 + yaml: 2.8.2 optionalDependencies: - postcss: 8.5.4 + postcss: 8.5.6 - postcss@8.5.4: + postcss@8.5.6: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 - preact@10.26.8: {} + preact@10.28.2: {} prelude-ls@1.2.1: {} - prettier-linter-helpers@1.0.0: + prettier-linter-helpers@1.0.1: dependencies: fast-diff: 1.3.0 @@ -9226,6 +9419,8 @@ snapshots: process-warning@1.0.0: {} + process-warning@5.0.0: {} + progress@2.0.3: {} prop-types@15.8.1: @@ -9238,9 +9433,9 @@ snapshots: proxy-from-env@1.1.0: {} - pump@3.0.2: + pump@3.0.3: dependencies: - end-of-stream: 1.4.4 + end-of-stream: 1.4.5 once: 1.4.0 punycode@2.3.1: {} @@ -9280,7 +9475,7 @@ snapshots: react-is@18.3.1: {} - react-is@19.1.0: {} + react-is@19.2.3: {} react-refresh@0.17.0: {} @@ -9311,9 +9506,9 @@ snapshots: react-router@7.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - cookie: 1.0.2 + cookie: 1.1.1 react: 19.1.0 - set-cookie-parser: 2.7.1 + set-cookie-parser: 2.7.2 optionalDependencies: react-dom: 19.1.0(react@19.1.0) @@ -9327,7 +9522,7 @@ snapshots: react-transition-group@4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - '@babel/runtime': 7.27.4 + '@babel/runtime': 7.28.4 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -9360,19 +9555,19 @@ snapshots: real-require@0.1.0: {} + real-require@0.2.0: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.1 es-errors: 1.3.0 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 get-proto: 1.0.1 which-builtin-type: 1.2.1 - regenerator-runtime@0.14.1: {} - regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 @@ -9392,7 +9587,7 @@ snapshots: resolve-from@5.0.0: {} - resolve@1.22.10: + resolve@1.22.11: dependencies: is-core-module: 2.16.1 path-parse: 1.0.7 @@ -9404,30 +9599,35 @@ snapshots: dependencies: glob: 7.2.3 - rollup@4.41.1: + rollup@4.55.1: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.41.1 - '@rollup/rollup-android-arm64': 4.41.1 - '@rollup/rollup-darwin-arm64': 4.41.1 - '@rollup/rollup-darwin-x64': 4.41.1 - '@rollup/rollup-freebsd-arm64': 4.41.1 - '@rollup/rollup-freebsd-x64': 4.41.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.41.1 - '@rollup/rollup-linux-arm-musleabihf': 4.41.1 - '@rollup/rollup-linux-arm64-gnu': 4.41.1 - '@rollup/rollup-linux-arm64-musl': 4.41.1 - '@rollup/rollup-linux-loongarch64-gnu': 4.41.1 - '@rollup/rollup-linux-powerpc64le-gnu': 4.41.1 - '@rollup/rollup-linux-riscv64-gnu': 4.41.1 - '@rollup/rollup-linux-riscv64-musl': 4.41.1 - '@rollup/rollup-linux-s390x-gnu': 4.41.1 - '@rollup/rollup-linux-x64-gnu': 4.41.1 - '@rollup/rollup-linux-x64-musl': 4.41.1 - '@rollup/rollup-win32-arm64-msvc': 4.41.1 - '@rollup/rollup-win32-ia32-msvc': 4.41.1 - '@rollup/rollup-win32-x64-msvc': 4.41.1 + '@rollup/rollup-android-arm-eabi': 4.55.1 + '@rollup/rollup-android-arm64': 4.55.1 + '@rollup/rollup-darwin-arm64': 4.55.1 + '@rollup/rollup-darwin-x64': 4.55.1 + '@rollup/rollup-freebsd-arm64': 4.55.1 + '@rollup/rollup-freebsd-x64': 4.55.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.55.1 + '@rollup/rollup-linux-arm-musleabihf': 4.55.1 + '@rollup/rollup-linux-arm64-gnu': 4.55.1 + '@rollup/rollup-linux-arm64-musl': 4.55.1 + '@rollup/rollup-linux-loong64-gnu': 4.55.1 + '@rollup/rollup-linux-loong64-musl': 4.55.1 + '@rollup/rollup-linux-ppc64-gnu': 4.55.1 + '@rollup/rollup-linux-ppc64-musl': 4.55.1 + '@rollup/rollup-linux-riscv64-gnu': 4.55.1 + '@rollup/rollup-linux-riscv64-musl': 4.55.1 + '@rollup/rollup-linux-s390x-gnu': 4.55.1 + '@rollup/rollup-linux-x64-gnu': 4.55.1 + '@rollup/rollup-linux-x64-musl': 4.55.1 + '@rollup/rollup-openbsd-x64': 4.55.1 + '@rollup/rollup-openharmony-arm64': 4.55.1 + '@rollup/rollup-win32-arm64-msvc': 4.55.1 + '@rollup/rollup-win32-ia32-msvc': 4.55.1 + '@rollup/rollup-win32-x64-gnu': 4.55.1 + '@rollup/rollup-win32-x64-msvc': 4.55.1 fsevents: 2.3.3 rrweb-cssom@0.8.0: {} @@ -9473,11 +9673,11 @@ snapshots: semver@6.3.1: {} - semver@7.7.2: {} + semver@7.7.3: {} set-blocking@2.0.0: {} - set-cookie-parser@2.7.1: {} + set-cookie-parser@2.7.2: {} set-function-length@1.2.2: dependencies: @@ -9503,10 +9703,11 @@ snapshots: setimmediate@1.0.5: {} - sha.js@2.4.11: + sha.js@2.4.12: dependencies: inherits: 2.0.4 safe-buffer: 5.2.1 + to-buffer: 1.2.2 shebang-command@2.0.0: dependencies: @@ -9550,21 +9751,23 @@ snapshots: slash@3.0.0: {} - socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): + slow-redact@0.3.2: {} + + socket.io-client@4.8.3(bufferutil@4.1.0)(utf-8-validate@5.0.10): dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.3.7 - engine.io-client: 6.6.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) - socket.io-parser: 4.2.4 + debug: 4.4.3 + engine.io-client: 6.6.4(bufferutil@4.1.0)(utf-8-validate@5.0.10) + socket.io-parser: 4.2.5 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - socket.io-parser@4.2.4: + socket.io-parser@4.2.5: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.3.7 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -9574,18 +9777,13 @@ snapshots: dependencies: atomic-sleep: 1.0.0 - source-map-js@1.2.1: {} - - source-map-support@0.5.21: + sonic-boom@4.2.0: dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - optional: true + atomic-sleep: 1.0.0 - source-map@0.5.7: {} + source-map-js@1.2.1: {} - source-map@0.6.1: - optional: true + source-map@0.5.7: {} source-map@0.8.0-beta.0: dependencies: @@ -9597,7 +9795,7 @@ snapshots: stackback@0.0.2: {} - std-env@3.9.0: {} + std-env@3.10.0: {} stop-iteration-iterator@1.1.0: dependencies: @@ -9618,13 +9816,13 @@ snapshots: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 string.prototype.includes@2.0.1: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 string.prototype.trim@1.2.10: dependencies: @@ -9632,7 +9830,7 @@ snapshots: call-bound: 1.0.4 define-data-property: 1.1.4 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.1 es-object-atoms: 1.1.1 has-property-descriptors: 1.0.2 @@ -9661,9 +9859,9 @@ snapshots: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.0: + strip-ansi@7.1.2: dependencies: - ansi-regex: 6.1.0 + ansi-regex: 6.2.2 strip-bom@3.0.0: {} @@ -9675,14 +9873,14 @@ snapshots: stylis@4.2.0: {} - sucrase@3.35.0: + sucrase@3.35.1: dependencies: - '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/gen-mapping': 0.3.13 commander: 4.1.1 - glob: 10.3.10 lines-and-columns: 1.2.4 mz: 2.7.0 pirates: 4.0.7 + tinyglobby: 0.2.15 ts-interface-checker: 0.1.13 superstruct@1.0.4: {} @@ -9695,17 +9893,9 @@ snapshots: symbol-tree@3.2.4: {} - synckit@0.11.8: + synckit@0.11.11: dependencies: - '@pkgr/core': 0.2.7 - - terser@5.39.0: - dependencies: - '@jridgewell/source-map': 0.3.6 - acorn: 8.14.1 - commander: 2.20.3 - source-map-support: 0.5.21 - optional: true + '@pkgr/core': 0.2.9 thenify-all@1.6.0: dependencies: @@ -9719,27 +9909,31 @@ snapshots: dependencies: real-require: 0.1.0 + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + tinybench@2.9.0: {} tinycolor2@1.6.0: {} tinyexec@0.3.2: {} - tinyglobby@0.2.14: + tinyglobby@0.2.15: dependencies: - fdir: 6.4.5(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 tinygradient@1.1.5: dependencies: '@types/tinycolor2': 1.4.6 tinycolor2: 1.6.0 - tinypool@1.1.0: {} + tinypool@1.1.1: {} tinyrainbow@2.0.0: {} - tinyspy@4.0.3: {} + tinyspy@4.0.4: {} tldts-core@6.1.86: {} @@ -9747,6 +9941,12 @@ snapshots: dependencies: tldts-core: 6.1.86 + to-buffer@1.2.2: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -9769,7 +9969,7 @@ snapshots: tree-kill@1.2.2: {} - ts-api-utils@2.1.0(typescript@5.8.3): + ts-api-utils@2.4.0(typescript@5.8.3): dependencies: typescript: 5.8.3 @@ -9786,24 +9986,24 @@ snapshots: tslib@2.8.1: {} - tsup@8.0.2(postcss@8.5.4)(typescript@5.8.3): + tsup@8.0.2(postcss@8.5.6)(typescript@5.8.3): dependencies: bundle-require: 4.2.1(esbuild@0.19.12) cac: 6.7.14 chokidar: 3.6.0 - debug: 4.4.1 + debug: 4.4.3 esbuild: 0.19.12 execa: 5.1.1 globby: 11.1.0 joycon: 3.1.1 - postcss-load-config: 4.0.2(postcss@8.5.4) + postcss-load-config: 4.0.2(postcss@8.5.6) resolve-from: 5.0.0 - rollup: 4.41.1 + rollup: 4.55.1 source-map: 0.8.0-beta.0 - sucrase: 3.35.0 + sucrase: 3.35.1 tree-kill: 1.2.2 optionalDependencies: - postcss: 8.5.4 + postcss: 8.5.6 typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -9858,14 +10058,18 @@ snapshots: typescript@5.8.3: {} - ua-parser-js@1.0.40: {} + ua-parser-js@1.0.41: {} - ufo@1.6.1: {} + ufo@1.6.2: {} uint8arrays@3.1.0: dependencies: multiformats: 9.9.0 + uint8arrays@3.1.1: + dependencies: + multiformats: 9.9.0 + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -9879,16 +10083,16 @@ snapshots: universalify@2.0.1: {} - unstorage@1.16.0(idb-keyval@6.2.2): + unstorage@1.17.3(idb-keyval@6.2.2): dependencies: anymatch: 3.1.3 chokidar: 4.0.3 destr: 2.0.5 - h3: 1.15.3 + h3: 1.15.4 lru-cache: 10.4.3 - node-fetch-native: 1.6.6 - ofetch: 1.4.1 - ufo: 1.6.1 + node-fetch-native: 1.6.7 + ofetch: 1.5.1 + ufo: 1.6.2 optionalDependencies: idb-keyval: 6.2.2 @@ -9911,9 +10115,9 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 - update-browserslist-db@1.1.3(browserslist@4.25.0): + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: - browserslist: 4.25.0 + browserslist: 4.28.1 escalade: 3.2.0 picocolors: 1.1.1 @@ -9954,7 +10158,7 @@ snapshots: dependencies: inherits: 2.0.4 is-arguments: 1.2.0 - is-generator-function: 1.1.0 + is-generator-function: 1.1.2 is-typed-array: 1.1.15 which-typed-array: 1.1.19 @@ -9971,16 +10175,16 @@ snapshots: '@types/react': 19.1.6 react: 19.1.0 - viem@2.23.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4): + viem@2.23.2(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4): dependencies: '@noble/curves': 1.8.1 '@noble/hashes': 1.7.1 '@scure/bip32': 1.6.2 '@scure/bip39': 1.5.4 abitype: 1.0.8(typescript@5.8.3)(zod@3.22.4) - isows: 1.0.6(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + isows: 1.0.6(ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) ox: 0.6.7(typescript@5.8.3)(zod@3.22.4) - ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: @@ -9988,16 +10192,16 @@ snapshots: - utf-8-validate - zod - viem@2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4): + viem@2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 abitype: 1.0.8(typescript@5.8.3)(zod@3.22.4) - isows: 1.0.7(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + isows: 1.0.7(ws@8.18.2(bufferutil@4.1.0)(utf-8-validate@5.0.10)) ox: 0.7.1(typescript@5.8.3)(zod@3.22.4) - ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.18.2(bufferutil@4.1.0)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: @@ -10005,7 +10209,7 @@ snapshots: - utf-8-validate - zod - viem@2.9.9(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4): + viem@2.9.9(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4): dependencies: '@adraffy/ens-normalize': 1.10.0 '@noble/curves': 1.2.0 @@ -10013,8 +10217,8 @@ snapshots: '@scure/bip32': 1.3.2 '@scure/bip39': 1.2.1 abitype: 1.0.0(typescript@5.8.3)(zod@3.22.4) - isows: 1.0.3(ws@8.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - ws: 8.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + isows: 1.0.3(ws@8.13.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + ws: 8.13.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: @@ -10022,13 +10226,13 @@ snapshots: - utf-8-validate - zod - vite-node@3.2.1(@types/node@24.0.1)(terser@5.39.0)(yaml@2.8.0): + vite-node@3.2.1(@types/node@24.0.1)(yaml@2.8.2): dependencies: cac: 6.7.14 - debug: 4.4.1 + debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@24.0.1)(terser@5.39.0)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.0.1)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -10043,49 +10247,48 @@ snapshots: - tsx - yaml - vite@6.3.5(@types/node@24.0.1)(terser@5.39.0)(yaml@2.8.0): + vite@6.3.5(@types/node@24.0.1)(yaml@2.8.2): dependencies: - esbuild: 0.25.0 - fdir: 6.4.5(picomatch@4.0.2) - picomatch: 4.0.2 - postcss: 8.5.4 - rollup: 4.41.1 - tinyglobby: 0.2.14 + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.55.1 + tinyglobby: 0.2.15 optionalDependencies: '@types/node': 24.0.1 fsevents: 2.3.3 - terser: 5.39.0 - yaml: 2.8.0 + yaml: 2.8.2 - vitest@3.2.1(@types/debug@4.1.12)(@types/node@24.0.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(terser@5.39.0)(yaml@2.8.0): + vitest@3.2.1(@types/debug@4.1.12)(@types/node@24.0.1)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(yaml@2.8.2): dependencies: - '@types/chai': 5.2.2 + '@types/chai': 5.2.3 '@vitest/expect': 3.2.1 - '@vitest/mocker': 3.2.1(vite@6.3.5(@types/node@24.0.1)(terser@5.39.0)(yaml@2.8.0)) - '@vitest/pretty-format': 3.2.1 + '@vitest/mocker': 3.2.1(vite@6.3.5(@types/node@24.0.1)(yaml@2.8.2)) + '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.1 '@vitest/snapshot': 3.2.1 '@vitest/spy': 3.2.1 '@vitest/utils': 3.2.1 - chai: 5.2.0 - debug: 4.4.1 - expect-type: 1.2.1 - magic-string: 0.30.17 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 pathe: 2.0.3 - picomatch: 4.0.2 - std-env: 3.9.0 + picomatch: 4.0.3 + std-env: 3.10.0 tinybench: 2.9.0 tinyexec: 0.3.2 - tinyglobby: 0.2.14 - tinypool: 1.1.0 + tinyglobby: 0.2.15 + tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@24.0.1)(terser@5.39.0)(yaml@2.8.0) - vite-node: 3.2.1(@types/node@24.0.1)(terser@5.39.0)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.0.1)(yaml@2.8.2) + vite-node: 3.2.1(@types/node@24.0.1)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 '@types/node': 24.0.1 - jsdom: 26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + jsdom: 26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) transitivePeerDependencies: - jiti - less @@ -10104,14 +10307,14 @@ snapshots: dependencies: xml-name-validator: 5.0.0 - wagmi@2.15.5(@tanstack/query-core@5.80.2)(@tanstack/react-query@5.80.3(react@19.1.0))(@types/react@19.1.6)(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4))(zod@3.22.4): + wagmi@2.15.5(@tanstack/query-core@5.80.2)(@tanstack/react-query@5.80.3(react@19.1.0))(@types/react@19.1.6)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4))(zod@3.22.4): dependencies: '@tanstack/react-query': 5.80.3(react@19.1.0) - '@wagmi/connectors': 5.8.4(@types/react@19.1.6)(@wagmi/core@2.17.2(@tanstack/query-core@5.80.2)(@types/react@19.1.6)(react@19.1.0)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)))(bufferutil@4.0.9)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4))(zod@3.22.4) - '@wagmi/core': 2.17.2(@tanstack/query-core@5.80.2)(@types/react@19.1.6)(react@19.1.0)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)) + '@wagmi/connectors': 5.8.4(@types/react@19.1.6)(@wagmi/core@2.17.2(@tanstack/query-core@5.80.2)(@types/react@19.1.6)(react@19.1.0)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)))(bufferutil@4.1.0)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4))(zod@3.22.4) + '@wagmi/core': 2.17.2(@tanstack/query-core@5.80.2)(@types/react@19.1.6)(react@19.1.0)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4)) react: 19.1.0 use-sync-external-store: 1.4.0(react@19.1.0) - viem: 2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + viem: 2.30.6(bufferutil@4.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: @@ -10130,6 +10333,7 @@ snapshots: - '@types/react' - '@upstash/redis' - '@vercel/blob' + - '@vercel/functions' - '@vercel/kv' - aws4fetch - bufferutil @@ -10188,13 +10392,13 @@ snapshots: is-async-function: 2.1.1 is-date-object: 1.1.0 is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.0 + is-generator-function: 1.1.2 is-regex: 1.2.1 is-weakref: 1.1.1 isarray: 2.0.5 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 - which-typed-array: 1.1.18 + which-typed-array: 1.1.19 which-collection@1.0.2: dependencies: @@ -10205,15 +10409,6 @@ snapshots: which-module@2.0.1: {} - which-typed-array@1.1.18: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.4 - for-each: 0.3.5 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - which-typed-array@1.1.19: dependencies: available-typed-arrays: 1.0.7 @@ -10253,35 +10448,40 @@ snapshots: wrap-ansi@8.1.0: dependencies: - ansi-styles: 6.2.1 + ansi-styles: 6.2.3 string-width: 5.1.2 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 wrappy@1.0.2: {} - ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10): + ws@7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.1.0 + utf-8-validate: 5.0.10 + + ws@8.13.0(bufferutil@4.1.0)(utf-8-validate@5.0.10): optionalDependencies: - bufferutil: 4.0.9 + bufferutil: 4.1.0 utf-8-validate: 5.0.10 - ws@8.13.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): + ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10): optionalDependencies: - bufferutil: 4.0.9 + bufferutil: 4.1.0 utf-8-validate: 5.0.10 - ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): + ws@8.18.2(bufferutil@4.1.0)(utf-8-validate@5.0.10): optionalDependencies: - bufferutil: 4.0.9 + bufferutil: 4.1.0 utf-8-validate: 5.0.10 - ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): + ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10): optionalDependencies: - bufferutil: 4.0.9 + bufferutil: 4.1.0 utf-8-validate: 5.0.10 - ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10): + ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10): optionalDependencies: - bufferutil: 4.0.9 + bufferutil: 4.1.0 utf-8-validate: 5.0.10 xml-name-validator@5.0.0: {} @@ -10300,7 +10500,7 @@ snapshots: yaml@1.10.2: {} - yaml@2.8.0: {} + yaml@2.8.2: {} yargs-parser@18.1.3: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..36c4a41 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +onlyBuiltDependencies: + - "@safe-global/safe-smart-account" + diff --git a/src/__tests__/canonGuardService.integration.test.ts b/src/__tests__/canonGuardService.integration.test.ts new file mode 100644 index 0000000..c5998c3 --- /dev/null +++ b/src/__tests__/canonGuardService.integration.test.ts @@ -0,0 +1,46 @@ +import { Address } from "viem"; +import { optimism } from "viem/chains"; +import { describe, it, expect, beforeAll } from "vitest"; +import { OPTIMISM_MAINNET_RPC } from "~/constants/addresses"; +import { CanonGuardService } from "~/services/canonGuardService"; +import { ClientService } from "~/services/clientService"; + +const NEW_GUARD_ADDRESS: Address = "0xe53d45e11897B1FB5aC94f675589072E838CFd1d"; + +const EXPECTED_QUEUED_TRANSACTIONS = [ + "0x00cE00912a2dEbE04E2dB6C713b846dE3ba44d68", + "0xf64343c0d8c90BF10117285661D74FcB0f46C27d", + "0xaEf36aE38FE2969B95aBB4432e879BA3255f1aFE", + "0x1BaBb4e2136bF396a5335C333B25496211a75267", +] as const; + +const EXPECTED_PRE_APPROVED_ITEMS = [ + "0xaEf36aE38FE2969B95aBB4432e879BA3255f1aFE", + "0x1BaBb4e2136bF396a5335C333B25496211a75267", +] as const; + +describe("CanonGuardService Integration", () => { + let service: CanonGuardService; + + beforeAll(() => { + const client = new ClientService(OPTIMISM_MAINNET_RPC, optimism); + service = new CanonGuardService(client); + }); + + it("fetches real Canon Guard vault data", async () => { + const data = await service.getCanonGuardData(NEW_GUARD_ADDRESS, 3); + + expect(data.queuedTransactions).toHaveLength(EXPECTED_QUEUED_TRANSACTIONS.length); + expect(data.preApprovedItems).toHaveLength(EXPECTED_PRE_APPROVED_ITEMS.length); + expect(data.executionHistory).toHaveLength(0); + + expect(data.queuedTransactions[0].actionBuilder.address).toBe(EXPECTED_QUEUED_TRANSACTIONS[0]); + expect(data.queuedTransactions[0].state).toBe("executable"); + expect(data.queuedTransactions[0].actionBuilder.factoryType).toBe("simple_transfers"); + expect(data.queuedTransactions[0].actionBuilder.factoryLabel).toBe("Simple Transfers Factory"); + + expect(data.preApprovedItems[0].address).toBe(EXPECTED_PRE_APPROVED_ITEMS[0]); + expect(data.preApprovedItems[0].type).toBe("builder"); + expect(data.preApprovedItems[0].factoryType).toBe("simple_transfers"); + }); +}); diff --git a/src/__tests__/canonGuardService.test.ts b/src/__tests__/canonGuardService.test.ts new file mode 100644 index 0000000..3af1911 --- /dev/null +++ b/src/__tests__/canonGuardService.test.ts @@ -0,0 +1,61 @@ +import { Address } from "viem"; +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { CanonGuardService } from "~/services/canonGuardService"; +import { ClientService } from "~/services/clientService"; + +describe("CanonGuardService", () => { + let service: CanonGuardService; + let mockMulticall: ReturnType; + let mockReadContract: ReturnType; + + beforeEach(() => { + mockMulticall = vi.fn(); + mockReadContract = vi.fn(); + + const mockClient = { multicall: mockMulticall, readContract: mockReadContract }; + const mockClientService = { getClient: () => mockClient }; + + service = new CanonGuardService(mockClientService as unknown as ClientService); + }); + + it("returns empty data when no actions exist", async () => { + mockReadContract.mockResolvedValue([]); + + const result = await service.getCanonGuardData("0x123" as Address, 1); + + expect(result.queuedTransactions).toEqual([]); + expect(result.preApprovedItems).toEqual([]); + expect(result.executionHistory).toEqual([]); + }); + + it("processes queued transactions", async () => { + const now = Math.floor(Date.now() / 1000); + + mockReadContract.mockResolvedValueOnce(["0xaction1"]); + + mockReadContract.mockResolvedValueOnce(BigInt(1)); + + mockMulticall.mockResolvedValueOnce([ + { status: "success", result: ["0x1234", BigInt(now + 100), BigInt(now + 1000)] }, + { status: "success", result: "0xhash" }, + { status: "success", result: BigInt(now + 500) }, + { status: "success", result: [] }, + ]); + + mockReadContract.mockResolvedValueOnce([{ target: "0xtoken", data: "0xa9059cbb", value: 0n }]); + + const result = await service.getCanonGuardData("0x123" as Address, 1); + + expect(result.queuedTransactions).toHaveLength(1); + }); + + it("handles errors gracefully", async () => { + mockReadContract.mockRejectedValue(new Error("Network error")); + + const result = await service.getCanonGuardData("0x123" as Address, 1); + + expect(result.queuedTransactions).toEqual([]); + expect(result.preApprovedItems).toEqual([]); + expect(result.executionHistory).toEqual([]); + }); +}); diff --git a/src/__tests__/safeService.integration.test.ts b/src/__tests__/safeService.integration.test.ts index 4c0dd72..2af8649 100644 --- a/src/__tests__/safeService.integration.test.ts +++ b/src/__tests__/safeService.integration.test.ts @@ -24,8 +24,8 @@ describe("SafeService Integration Tests", () => { const guardAddress = await safeService.getGuardAddress(DEMO_SAFE_WITH_GUARD); expect(vaultInfo.hasCanonGuard).toBe(true); - expect(vaultInfo.guardAddress).toBe(DEMO_GUARD_ADDRESS); - expect(guardAddress).toBe(DEMO_GUARD_ADDRESS); + expect(vaultInfo.guardAddress?.toLowerCase()).toBe(DEMO_GUARD_ADDRESS.toLowerCase()); + expect(guardAddress?.toLowerCase()).toBe(DEMO_GUARD_ADDRESS.toLowerCase()); expect(vaultInfo.owners).toContain(DEMO_SAFE_OWNER); }); diff --git a/src/abis/canonGuard.ts b/src/abis/canonGuard.ts new file mode 100644 index 0000000..0b0a939 --- /dev/null +++ b/src/abis/canonGuard.ts @@ -0,0 +1,655 @@ +import IActionsBuilderABI from "@defi-wonderland/canon-guard-interfaces/abi/IActionsBuilder.json"; +import IApproveActionFactoryABI from "@defi-wonderland/canon-guard-interfaces/abi/IApproveActionFactory.json"; +import ISafeEntrypointABI from "@defi-wonderland/canon-guard-interfaces/abi/ISafeEntrypoint.json"; +import ISimpleActionsFactoryABI from "@defi-wonderland/canon-guard-interfaces/abi/ISimpleActionsFactory.json"; +import { Abi } from "viem"; + +export const canonGuardEntrypointAbi = ISafeEntrypointABI.abi as Abi; +export const actionBuilderAbi = IActionsBuilderABI.abi as Abi; +export const simpleActionsFactoryAbi = ISimpleActionsFactoryABI.abi as Abi; +export const approveActionFactoryAbi = IApproveActionFactoryABI.abi as Abi; + +// Canon Guard ABI for transaction management functions +export const canonGuardAbi = [ + // MAX_APPROVAL_DURATION - immutable value for approval validation + { + type: "function", + name: "MAX_APPROVAL_DURATION", + inputs: [], + outputs: [{ name: "_maxApprovalDuration", type: "uint256" }], + stateMutability: "view", + }, + // SHORT_TX_EXECUTION_DELAY - fast path delay + { + type: "function", + name: "SHORT_TX_EXECUTION_DELAY", + inputs: [], + outputs: [{ name: "_shortTxExecutionDelay", type: "uint256" }], + stateMutability: "view", + }, + // LONG_TX_EXECUTION_DELAY - slow path delay + { + type: "function", + name: "LONG_TX_EXECUTION_DELAY", + inputs: [], + outputs: [{ name: "_longTxExecutionDelay", type: "uint256" }], + stateMutability: "view", + }, + // TX_EXPIRY_DELAY - execution timeframe + { + type: "function", + name: "TX_EXPIRY_DELAY", + inputs: [], + outputs: [{ name: "_txExpiryDelay", type: "uint256" }], + stateMutability: "view", + }, + // queueTransaction - add a transaction to the Canon Guard queue + { + type: "function", + name: "queueTransaction", + inputs: [{ name: "_actionsBuilder", type: "address" }], + outputs: [], + stateMutability: "nonpayable", + }, + // executeTransaction - execute a queued transaction + { + type: "function", + name: "executeTransaction", + inputs: [{ name: "_actionsBuilder", type: "address" }], + outputs: [], + stateMutability: "nonpayable", + }, + // getSafeTransactionHash - get the hash to sign for a queued transaction (current nonce) + { + type: "function", + name: "getSafeTransactionHash", + inputs: [{ name: "_actionsBuilder", type: "address" }], + outputs: [{ name: "_safeTxHash", type: "bytes32" }], + stateMutability: "view", + }, + // getSafeTransactionHash - get the hash for a specific nonce (overloaded) + { + type: "function", + name: "getSafeTransactionHash", + inputs: [ + { name: "_actionsBuilder", type: "address" }, + { name: "_safeNonce", type: "uint256" }, + ], + outputs: [{ name: "_safeTxHash", type: "bytes32" }], + stateMutability: "view", + }, + // transactionsInfo - get transaction details for an action builder + { + type: "function", + name: "transactionsInfo", + inputs: [{ name: "_actionsBuilder", type: "address" }], + outputs: [ + { name: "_proposer", type: "address" }, + { name: "_actionsData", type: "bytes" }, + { name: "_executableAt", type: "uint256" }, + { name: "_expiresAt", type: "uint256" }, + { name: "_isPreApproved", type: "bool" }, + ], + stateMutability: "view", + }, + // getApprovedHashSigners - get signers who approved for a specific nonce + { + type: "function", + name: "getApprovedHashSigners", + inputs: [ + { name: "_actionsBuilder", type: "address" }, + { name: "_safeNonce", type: "uint256" }, + ], + outputs: [{ name: "_approvedHashSigners", type: "address[]" }], + stateMutability: "view", + }, + // getSafeNonce - get current Safe nonce + { + type: "function", + name: "getSafeNonce", + inputs: [], + outputs: [{ name: "_safeNonce", type: "uint256" }], + stateMutability: "view", + }, + // getQueuedActionBuilders - get all action builders in queue + { + type: "function", + name: "getQueuedActionBuilders", + inputs: [], + outputs: [{ name: "_queuedActionBuilders", type: "address[]" }], + stateMutability: "view", + }, + // cancelEnqueuedTransaction - cancel a queued transaction (only proposer can cancel non-expired) + { + type: "function", + name: "cancelEnqueuedTransaction", + inputs: [{ name: "_actionsBuilder", type: "address" }], + outputs: [], + stateMutability: "nonpayable", + }, + // emergencyMode - check if emergency mode is active + { + type: "function", + name: "emergencyMode", + inputs: [], + outputs: [{ name: "_emergencyMode", type: "bool" }], + stateMutability: "view", + }, + // emergencyTrigger - address that can turn ON emergency mode + { + type: "function", + name: "emergencyTrigger", + inputs: [], + outputs: [{ name: "_emergencyTrigger", type: "address" }], + stateMutability: "view", + }, + // emergencyCaller - address that can turn OFF emergency mode and execute/cancel during emergency + { + type: "function", + name: "emergencyCaller", + inputs: [], + outputs: [{ name: "_emergencyCaller", type: "address" }], + stateMutability: "view", + }, + // setEmergencyMode - activate emergency mode (only callable by emergencyTrigger) + { + type: "function", + name: "setEmergencyMode", + inputs: [], + outputs: [], + stateMutability: "nonpayable", + }, +] as const; + +// Pre-Approve Action Factory ABI for creating pre-approval actions +export const preApproveActionFactoryAbi = [ + // createPreApproveAction - deploy a new pre-approval action + { + type: "function", + name: "createPreApproveAction", + inputs: [ + { name: "_actionsBuilder", type: "address" }, + { name: "_approvalDuration", type: "uint256" }, + ], + outputs: [{ name: "_preApproveAction", type: "address" }], + stateMutability: "nonpayable", + }, + // PreApproveActionCreated event - emitted when a pre-approval is deployed + { + type: "event", + name: "PreApproveActionCreated", + inputs: [ + { name: "_preApproveAction", type: "address", indexed: true }, + { name: "_actionsBuilder", type: "address", indexed: true }, + { name: "_approvalDuration", type: "uint256", indexed: false }, + ], + anonymous: false, + }, +] as const; + +// TODO: Use package ABI when it includes the SimpleTransfersCreated event +// The package ABI is missing the event, so we define it manually here +export const simpleTransfersFactoryAbi = [ + // Function: createSimpleTransfers + { + type: "function", + name: "createSimpleTransfers", + inputs: [ + { + name: "_transferActions", + type: "tuple[]", + internalType: "struct ISimpleTransfers.TransferAction[]", + components: [ + { name: "token", type: "address", internalType: "address" }, + { name: "to", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + ], + }, + ], + outputs: [{ name: "_simpleTransfers", type: "address", internalType: "address" }], + stateMutability: "nonpayable", + }, + // Event: SimpleTransfersCreated (missing from package ABI!) + { + type: "event", + name: "SimpleTransfersCreated", + inputs: [{ name: "_simpleTransfers", type: "address", indexed: true, internalType: "address" }], + anonymous: false, + }, +] as const; + +// Minimal ABI for CanonGuardFactory.isChild(address) check +export const canonGuardFactoryAbi = [ + { + type: "function", + name: "isChild", + inputs: [{ name: "_contract", type: "address" }], + outputs: [{ name: "_isChild", type: "bool" }], + stateMutability: "view", + }, +] as const; + +// Safe ABI for hash approval and threshold/owners +export const safeAbi = [ + { + type: "function", + name: "approveHash", + inputs: [{ name: "hashToApprove", type: "bytes32" }], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "getThreshold", + inputs: [], + outputs: [{ name: "", type: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "getOwners", + inputs: [], + outputs: [{ name: "", type: "address[]" }], + stateMutability: "view", + }, +] as const; + +// TODO: Import from @defi-wonderland/canon-guard-interfaces when ICanonGuardRegistry is added to the package +// Full ABI from contracts/src/interfaces/periphery/ICanonGuardRegistry.sol +export const canonGuardRegistryAbi = [ + // Errors + { inputs: [], name: "ArrayLengthMismatch", type: "error" }, + { inputs: [], name: "EmptyLabel", type: "error" }, + { inputs: [], name: "EntityNotFound", type: "error" }, + { inputs: [], name: "NotSafeSigner", type: "error" }, + // Events + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "_canonGuard", type: "address" }, + { indexed: true, internalType: "address", name: "_entity", type: "address" }, + { indexed: false, internalType: "string", name: "_label", type: "string" }, + { indexed: false, internalType: "uint256", name: "_lastEditedAt", type: "uint256" }, + ], + name: "EntityRecorded", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "_canonGuard", type: "address" }, + { indexed: true, internalType: "address", name: "_entity", type: "address" }, + ], + name: "EntityRemoved", + type: "event", + }, + // Functions + { + inputs: [ + { internalType: "address", name: "_canonGuard", type: "address" }, + { internalType: "address", name: "_entity", type: "address" }, + ], + name: "entityLabel", + outputs: [ + { + components: [ + { internalType: "string", name: "label", type: "string" }, + { internalType: "uint256", name: "lastEditedAt", type: "uint256" }, + ], + internalType: "struct ICanonGuardRegistry.Edition", + name: "_edition", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "_canonGuard", type: "address" }, + { internalType: "uint256", name: "_offset", type: "uint256" }, + { internalType: "uint256", name: "_limit", type: "uint256" }, + ], + name: "read", + outputs: [ + { + components: [ + { internalType: "address", name: "entity", type: "address" }, + { + components: [ + { internalType: "string", name: "label", type: "string" }, + { internalType: "uint256", name: "lastEditedAt", type: "uint256" }, + ], + internalType: "struct ICanonGuardRegistry.Edition", + name: "edition", + type: "tuple", + }, + ], + internalType: "struct ICanonGuardRegistry.EntityWithEdition[]", + name: "_entities", + type: "tuple[]", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "_canonGuard", type: "address" }, + { internalType: "address[]", name: "_entities", type: "address[]" }, + { internalType: "string[]", name: "_labels", type: "string[]" }, + ], + name: "record", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "_canonGuard", type: "address" }, + { internalType: "address[]", name: "_entities", type: "address[]" }, + ], + name: "remove", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "_canonGuard", type: "address" }], + name: "totalEntities", + outputs: [{ internalType: "uint256", name: "_total", type: "uint256" }], + stateMutability: "view", + type: "function", + }, +] as const; + +// PARENT() function ABI for action builders +// All action builders inherit from ActionsBuilder which has a PARENT() function that returns the factory address +export const actionBuilderParentAbi = [ + { + type: "function", + name: "PARENT", + inputs: [], + outputs: [{ name: "", type: "address" }], + stateMutability: "view", + }, +] as const; + +// HUB() function ABI for hub children (implements IActionHubChild) +// If an action builder has this method, it's a child of a hub +export const actionHubChildAbi = [ + { + type: "function", + name: "HUB", + inputs: [], + outputs: [{ name: "_hub", type: "address" }], + stateMutability: "view", + }, +] as const; + +// Minimal ABI for approvalExpiries on Canon Guard entrypoint +// Returns uint256 timestamp of when the approval expires for a given action builder +export const approvalExpiriesAbi = [ + { + type: "function", + name: "approvalExpiries", + inputs: [{ name: "_actionBuilder", type: "address" }], + outputs: [{ name: "", type: "uint256" }], + stateMutability: "view", + }, +] as const; + +// PreApproveAction ABI for getting the underlying action builder +// PreApproveAction wraps another action builder and exposes it via ACTIONS_BUILDER() +export const preApproveActionAbi = [ + { + type: "function", + name: "ACTIONS_BUILDER", + inputs: [], + outputs: [{ name: "_actionsBuilder", type: "address" }], + stateMutability: "view", + }, +] as const; + +// ArbitraryActionsFactory ABI with createArbitraryAction (singular), createArbitraryActions (plural), and event +// Signature is now optional - pass empty string if not provided +export const arbitraryActionsFactoryAbi = [ + // Function: createArbitraryAction - deploys a new ArbitraryActions contract with a single action + { + type: "function", + name: "createArbitraryAction", + inputs: [ + { + name: "_arbitraryAction", + type: "tuple", + internalType: "struct IArbitraryActions.ArbitraryAction", + components: [ + { name: "target", type: "address", internalType: "address" }, + { name: "signature", type: "string", internalType: "string" }, + { name: "data", type: "bytes", internalType: "bytes" }, + { name: "value", type: "uint256", internalType: "uint256" }, + ], + }, + ], + outputs: [{ name: "_arbitraryActions", type: "address", internalType: "address" }], + stateMutability: "nonpayable", + }, + // Function: createArbitraryActions (plural) - deploys a new ArbitraryActions contract with multiple actions + { + type: "function", + name: "createArbitraryActions", + inputs: [ + { + name: "_arbActions", + type: "tuple[]", + internalType: "struct IArbitraryActions.ArbitraryAction[]", + components: [ + { name: "target", type: "address", internalType: "address" }, + { name: "signature", type: "string", internalType: "string" }, + { name: "data", type: "bytes", internalType: "bytes" }, + { name: "value", type: "uint256", internalType: "uint256" }, + ], + }, + ], + outputs: [{ name: "_arbitraryActions", type: "address", internalType: "address" }], + stateMutability: "nonpayable", + }, + // Event: ArbitraryActionsCreated - emitted when an ArbitraryActions contract is deployed + { + type: "event", + name: "ArbitraryActionsCreated", + inputs: [{ name: "_arbitraryActions", type: "address", indexed: true, internalType: "address" }], + anonymous: false, + }, +] as const; + +// AllowanceClaimorFactory ABI for creating allowance claimor action builders +// Allowance claimor allows claiming token allowances from a token owner to a recipient +export const allowanceClaimorFactoryAbi = [ + // Function: createAllowanceClaimor - deploys a new AllowanceClaimor contract + { + type: "function", + name: "createAllowanceClaimor", + inputs: [ + { name: "_token", type: "address", internalType: "address" }, + { name: "_tokenOwner", type: "address", internalType: "address" }, + { name: "_tokenRecipient", type: "address", internalType: "address" }, + ], + outputs: [{ name: "_allowanceClaimor", type: "address", internalType: "address" }], + stateMutability: "nonpayable", + }, + // Event: AllowanceClaimorCreated - emitted when an AllowanceClaimor contract is deployed + { + type: "event", + name: "AllowanceClaimorCreated", + inputs: [ + { name: "_allowanceClaimor", type: "address", indexed: true, internalType: "address" }, + { name: "_token", type: "address", indexed: true, internalType: "address" }, + { name: "_tokenOwner", type: "address", indexed: true, internalType: "address" }, + { name: "_tokenRecipient", type: "address", indexed: false, internalType: "address" }, + ], + anonymous: false, + }, +] as const; + +// CappedTokenTransfersHubFactory ABI for creating capped token transfer hubs +// Hubs manage groups of related actions with shared approval patterns and budget constraints +export const cappedTokenTransfersHubFactoryAbi = [ + // Function: createCappedTokenTransfersHub - deploys a new CappedTokenTransfersHub contract + { + type: "function", + name: "createCappedTokenTransfersHub", + inputs: [ + { name: "_safe", type: "address", internalType: "address" }, + { name: "_recipient", type: "address", internalType: "address" }, + { name: "_tokens", type: "address[]", internalType: "address[]" }, + { name: "_caps", type: "uint256[]", internalType: "uint256[]" }, + { name: "_epochLength", type: "uint256", internalType: "uint256" }, + ], + outputs: [{ name: "_cappedTokenTransfersHub", type: "address", internalType: "address" }], + stateMutability: "nonpayable", + }, + // Event: CappedTokenTransfersHubCreated - emitted when a CappedTokenTransfersHub is deployed + { + type: "event", + name: "CappedTokenTransfersHubCreated", + inputs: [ + { name: "_cappedTokenTransfersHub", type: "address", indexed: true, internalType: "address" }, + { name: "_safe", type: "address", indexed: true, internalType: "address" }, + { name: "_recipient", type: "address", indexed: true, internalType: "address" }, + ], + anonymous: false, + }, +] as const; + +// CappedTokenTransfersHub ABI for interacting with hub instances +// Used to read hub configuration and deploy child action builders +export const cappedTokenTransfersHubAbi = [ + // tokens() - Get list of allowed tokens in the hub + { + type: "function", + name: "tokens", + inputs: [], + outputs: [{ name: "_tokens", type: "address[]", internalType: "address[]" }], + stateMutability: "view", + }, + // cap(token) - Get maximum cap per epoch for a token + { + type: "function", + name: "cap", + inputs: [{ name: "_token", type: "address", internalType: "address" }], + outputs: [{ name: "_cap", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + // capLeft(token) - Get remaining cap for current epoch + { + type: "function", + name: "capLeft", + inputs: [{ name: "_token", type: "address", internalType: "address" }], + outputs: [{ name: "_capLeft", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + // RECIPIENT() - Get the recipient address + { + type: "function", + name: "RECIPIENT", + inputs: [], + outputs: [{ name: "_recipient", type: "address", internalType: "address" }], + stateMutability: "view", + }, + // EPOCH_LENGTH() - Get epoch duration in seconds + { + type: "function", + name: "EPOCH_LENGTH", + inputs: [], + outputs: [{ name: "_epochLength", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + // PARENT() - Get the parent (factory) address + { + type: "function", + name: "PARENT", + inputs: [], + outputs: [{ name: "_parent", type: "address", internalType: "address" }], + stateMutability: "view", + }, + // isHubChild(address) - Check if an action builder is a child of this hub + { + type: "function", + name: "isHubChild", + inputs: [{ name: "_actionsBuilder", type: "address", internalType: "address" }], + outputs: [{ name: "_isChild", type: "bool", internalType: "bool" }], + stateMutability: "view", + }, + // createNewActionsBuilder(token, amount) - Deploy new CappedTokenTransfers child + { + type: "function", + name: "createNewActionsBuilder", + inputs: [ + { name: "_token", type: "address", internalType: "address" }, + { name: "_amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [{ name: "_actionsBuilder", type: "address", internalType: "address" }], + stateMutability: "nonpayable", + }, + // totalSpent(token) - Get total amount spent for a token in current epoch + { + type: "function", + name: "totalSpent", + inputs: [{ name: "_token", type: "address", internalType: "address" }], + outputs: [{ name: "_totalSpent", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + // lastEpoch(token) - Get the last epoch number for a token + { + type: "function", + name: "lastEpoch", + inputs: [{ name: "_token", type: "address", internalType: "address" }], + outputs: [{ name: "_lastEpoch", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + // Event: CappedTokenTransfersCreated - emitted when a child action builder is deployed + { + type: "event", + name: "CappedTokenTransfersCreated", + inputs: [ + { name: "_actionsBuilder", type: "address", indexed: false, internalType: "address" }, + { name: "_token", type: "address", indexed: false, internalType: "address" }, + { name: "_amount", type: "uint256", indexed: false, internalType: "uint256" }, + ], + anonymous: false, + }, +] as const; + +// ChangeSafeGuardAction ABI - for reading the target guard address +export const changeSafeGuardActionAbi = [ + { + type: "function", + name: "SAFE_GUARD", + inputs: [], + outputs: [{ name: "_safeGuard", type: "address" }], + stateMutability: "view", + }, +] as const; + +// ChangeSafeGuardActionFactory ABI - for detaching the Canon Guard from a Safe +export const changeSafeGuardActionFactoryAbi = [ + // createChangeSafeGuardAction - deploys a ChangeSafeGuardAction contract + { + type: "function", + name: "createChangeSafeGuardAction", + inputs: [{ name: "_safeGuard", type: "address", internalType: "address" }], + outputs: [{ name: "_changeSafeGuardAction", type: "address", internalType: "address" }], + stateMutability: "nonpayable", + }, + // Event: ChangeSafeGuardActionCreated - emitted when a ChangeSafeGuardAction is deployed + { + type: "event", + name: "ChangeSafeGuardActionCreated", + inputs: [ + { name: "_changeSafeGuardAction", type: "address", indexed: true, internalType: "address" }, + { name: "_safeGuard", type: "address", indexed: true, internalType: "address" }, + ], + anonymous: false, + }, +] as const; diff --git a/src/abis/index.ts b/src/abis/index.ts new file mode 100644 index 0000000..cae608f --- /dev/null +++ b/src/abis/index.ts @@ -0,0 +1,2 @@ +export * from "./safe"; +export * from "./canonGuard"; diff --git a/src/components/ActionCard.tsx b/src/components/ActionCard.tsx index 379dafb..723cda6 100644 --- a/src/components/ActionCard.tsx +++ b/src/components/ActionCard.tsx @@ -1,4 +1,5 @@ -import { Box, Tooltip, styled } from "@mui/material"; +import { Box, styled } from "@mui/material"; +import { CopyableText } from "~/components/shared/CopyButton"; import { SafeActionCard, SafeCardContent, @@ -7,8 +8,7 @@ import { SafeStatusChip, SafeAddress, } from "~/components/shared/StyledComponents"; -import { safeDesignTokens } from "~/config/themes/safeTheme"; -import { FACTORY_LABELS } from "~/services/canonGuardService"; +import { safeDesignTokens, canonHeaderTokens } from "~/config/themes/safeTheme"; import { QueuedTransaction } from "~/types/canon-guard"; import { formatTimeRemaining, truncateAddress, formatDate } from "~/utils"; @@ -18,29 +18,26 @@ interface ActionCardProps { } export const ActionCard = ({ action, showApprovalInfo = false }: ActionCardProps) => { - const copyToClipboard = (text: string) => navigator.clipboard.writeText(text); - - const timeRemainingToExecutable = Math.max(0, action.executableAt.getTime() - Date.now()); - - const factoryLabel = FACTORY_LABELS[action.actionBuilder.factoryAddress] || "Unknown Factory"; + const timeUntilExecutable = Math.max(0, action.executableAt.getTime() - Date.now()); + const isExecutableNow = timeUntilExecutable === 0; return ( - - - {factoryLabel} + + + {action.actionBuilder.factoryLabel} {action.actionBuilder.isApproved && } - + - - - copyToClipboard(action.actionBuilder.factoryAddress)}> - {truncateAddress(action.actionBuilder.factoryAddress)} - - - - + + {truncateAddress(action.actionBuilder.actionBuilderAddress)} + + {showApprovalInfo && ( @@ -53,7 +50,10 @@ export const ActionCard = ({ action, showApprovalInfo = false }: ActionCardProps Queued: {formatDate(action.queuedAt)} - • Executable in: {formatTimeRemaining(timeRemainingToExecutable)} + + {isExecutableNow && "• Ready to execute"} + {!isExecutableNow && `• Executable in: ${formatTimeRemaining(timeUntilExecutable)}`} + @@ -81,7 +81,7 @@ const ActionCardAddress = styled(SafeAddress)(() => ({ fontSize: "0.8rem", })); -const FlexRowSpaceBetween = styled(Box)(() => ({ +const CardHeaderRow = styled(Box)(() => ({ display: "flex", alignItems: "center", justifyContent: "space-between", @@ -89,7 +89,7 @@ const FlexRowSpaceBetween = styled(Box)(() => ({ gap: safeDesignTokens.spacing.md, })); -const FlexLeft = styled(Box)(() => ({ +const CardTitleSection = styled(Box)(() => ({ display: "flex", alignItems: "center", gap: safeDesignTokens.spacing.sm, @@ -97,12 +97,6 @@ const FlexLeft = styled(Box)(() => ({ minWidth: 0, })); -const FlexRight = styled(Box)(() => ({ - display: "flex", - alignItems: "center", - flexShrink: 0, -})); - const InfoRow = styled(Box)(() => ({ display: "flex", alignItems: "center", diff --git a/src/components/CanonGuardApp.tsx b/src/components/CanonGuardApp.tsx new file mode 100644 index 0000000..218aa3b --- /dev/null +++ b/src/components/CanonGuardApp.tsx @@ -0,0 +1,171 @@ +import { useState, useEffect, useCallback } from "react"; +import { Box, styled } from "@mui/material"; +import { useLocation, Navigate } from "react-router-dom"; +import { Address } from "viem"; +import { getChainConfig } from "~/config/chains"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { CanonGuardConfigProvider } from "~/contexts"; +import { useCanonGuardConfig } from "~/hooks/useCanonGuardConfig"; +import { useQueueService } from "~/hooks/useServices"; +import { useStateContext } from "~/hooks/useStateContext"; +import { SafeInfo } from "~/types"; +import { CanonListSection } from "./CanonListSection"; +import { ChangeGuardSection } from "./ChangeGuardSection"; +import { CreateSection } from "./CreateSection"; +import { DeploymentModesPanel } from "./DeploymentModesPanel"; +import { DetachedModeBanner } from "./DetachedModeBanner"; +import { EmergencyModeBanner } from "./EmergencyModeBanner"; +import { Header } from "./Header"; +import { QueueActionSection } from "./QueueActionSection"; +import { QueueSection } from "./QueueSection"; +import { QueueSignSection } from "./QueueSignSection"; +import { SettingsSection } from "./SettingsSection"; + +interface CanonGuardAppProps { + safeInfo: SafeInfo; + onClearConfig: () => void; +} + +/** + * Inner component that consumes the Canon Guard config context. + * Separated to ensure context is available when hooks are called. + */ +const CanonGuardAppInner = ({ safeInfo, onClearConfig }: CanonGuardAppProps) => { + const queueService = useQueueService(); + const { chainId, guardAddress, isDetached } = useStateContext(); + const { emergencyMode } = useCanonGuardConfig(); + const location = useLocation(); + + const [queueCount, setQueueCount] = useState(0); + const [isDeploymentModesPanelOpen, setIsDeploymentModesPanelOpen] = useState(false); + + const chainConfig = getChainConfig(chainId); + + // Fetch queue count for navbar + const fetchQueueCount = useCallback(async () => { + if (!guardAddress) return; + try { + const count = await queueService.getQueueCount(guardAddress as Address); + setQueueCount(count); + } catch (error) { + console.error("Failed to fetch queue count:", error); + } + }, [queueService, guardAddress]); + + useEffect(() => { + fetchQueueCount(); + }, [fetchQueueCount]); + + // Callback to update queue count when items change + const handleQueueCountChange = useCallback((count: number) => { + setQueueCount(count); + }, []); + + // Determine which content to render based on route + const renderContent = () => { + const path = location.pathname; + + // Attach/Detach Guard flow (must come before /settings check) + if (path === "/settings/attach") { + return ; + } + if (path === "/settings/detach") { + return ; + } + + // Settings page + if (path === "/settings") { + return ; + } + + // Queue is the default when at root or /queue + if (path === "/" || path === "/queue") { + return ; + } + + // Queue Sign (from Queue item Sign button) + if (path === "/queue/sign") { + return ; + } + + // Queue Action (from Canon List) + if (path === "/queue-action") { + return ; + } + + // Canon List + if (path === "/canon-list") { + return ; + } + + // Create routes - render CreateSection which handles its own nested routing + if (path.startsWith("/create")) { + return ; + } + + // Default to queue for unknown routes + return ; + }; + + // Handle Learn More click - open deployment modes panel + const handleDetachedLearnMore = useCallback(() => { + setIsDeploymentModesPanelOpen(true); + }, []); + + // Handle closing the deployment modes panel + const handleCloseDeploymentModesPanel = useCallback(() => { + setIsDeploymentModesPanelOpen(false); + }, []); + + return ( + + {/* Emergency mode takes priority over detached mode */} + {emergencyMode === true ? ( + + ) : isDetached ? ( + + ) : null} +
+ {renderContent()} + + {/* Deployment Modes Panel */} + + + ); +}; + +/** + * Main Canon Guard App component. + * Wraps the inner component with the CanonGuardConfigProvider + * to provide shared config state to all child components. + */ +export const CanonGuardApp = ({ safeInfo, onClearConfig }: CanonGuardAppProps) => { + return ( + + + + ); +}; + +const PageContainer = styled(Box)({ + display: "flex", + flexDirection: "column", + minHeight: "100vh", + width: "100%", + backgroundColor: canonHeaderTokens.background.layer0, +}); + +const MainContent = styled(Box)({ + flex: 1, + overflow: "auto", + backgroundColor: canonHeaderTokens.background.layer0, +}); diff --git a/src/components/CanonListSection/ActionItem.tsx b/src/components/CanonListSection/ActionItem.tsx new file mode 100644 index 0000000..903adbb --- /dev/null +++ b/src/components/CanonListSection/ActionItem.tsx @@ -0,0 +1,379 @@ +import { useState, useRef } from "react"; +import { Box, Typography, styled } from "@mui/material"; +import { + BoxIcon, + PlusIcon, + EllipsisIcon, + ZapIcon, + ZapOffIcon, + VectorSquareIcon, + ChevronDownIcon, + ChevronUpIcon, +} from "~/components/icons"; +import { CopyableText } from "~/components/shared/CopyButton"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { ActionFactoryType } from "~/types/canon-guard"; +import { getFactoryDisplayName, getHubDisplayName } from "~/utils/factoryDisplay"; +import { ActionMenu } from "./ActionMenu"; +import type { Address } from "viem"; + +interface ActionItemProps { + title: string; + address: Address; + factoryType: ActionFactoryType; + isFastPath: boolean; + // Hub-related props + isHub?: boolean; + isHubChild?: boolean; + childrenCount?: number; + isExpanded?: boolean; + onToggleExpand?: () => void; + // Actions + onQueue?: () => void; + onAddToQueue?: () => void; + onRename?: () => void; + onProposePreApproval?: () => void; + onDeployChild?: () => void; + onRemove?: () => void; + isRemoving?: boolean; +} + +export const ActionItem = ({ + title, + address, + factoryType, + isFastPath, + isHub = false, + isHubChild = false, + childrenCount = 0, + isExpanded = false, + onToggleExpand, + onQueue, + onAddToQueue, + onRename, + onProposePreApproval, + onDeployChild, + onRemove, + isRemoving, +}: ActionItemProps) => { + const [isHovered, setIsHovered] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const moreButtonRef = useRef(null); + const isUntitled = !title || title === "Untitled Transaction"; + const displayTitle = isUntitled ? "Untitled Transaction" : title; + // Use hub display name for hubs, factory display name for others + const factoryDisplayName = isHub ? `HUB: ${getHubDisplayName(factoryType)}` : getFactoryDisplayName(factoryType); + + const handleMoreClick = (e: React.MouseEvent) => { + e.stopPropagation(); + setIsMenuOpen((prev) => !prev); + }; + + const handleMenuClose = () => { + setIsMenuOpen(false); + }; + + const handleRemove = () => { + onRemove?.(); + // Don't close menu immediately - let the loading state show + }; + + return ( + setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}> + {/* Left icon section */} + + {isHub ? ( + + ) : ( + + )} + + + {/* Main content section */} + + + + {displayTitle} + {/* Hub children always show address; others show on hover */} + + + {address} + + + + {/* Hub children don't show type label */} + {!isHubChild && {factoryDisplayName}} + + + + {/* Actions section */} + + + {/* Hubs show styled children count button */} + {isHub ? ( + + + + {childrenCount} + + {isExpanded ? ( + + ) : ( + + )} + + ) : ( + /* Both regular actions and hub children show QUEUE button */ + + QUEUE + + + )} + + + + + + + + {/* Hub children don't show Fast/Slow path - only hubs and regular actions show that */} + {!isHubChild && ( + + + {isFastPath ? ( + <> + + Fast-path + + ) : ( + <> + + Slow-path + + )} + + + )} + + + ); +}; + +const ItemContainer = styled(Box)({ + display: "flex", + alignItems: "stretch", // Makes all children fill full height + borderRadius: "8px", + width: "100%", + position: "relative", // For dropdown positioning +}); + +const IconSection = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isHubChild", +})<{ $isHubChild?: boolean }>(({ $isHubChild }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: $isHubChild ? "80px" : "100px", + backgroundColor: canonHeaderTokens.background.layer1, + padding: $isHubChild ? "16px 20px" : "20px 24px", + opacity: 0.8, + borderRadius: "8px 0 0 8px", +})); + +const ContentSection = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isHubChild", +})<{ $isHubChild?: boolean }>(({ $isHubChild }) => ({ + flex: 1, + display: "flex", + alignItems: "center", + backgroundColor: canonHeaderTokens.background.layer1, + padding: $isHubChild ? "12px 20px" : "16px 20px", + minWidth: 0, +})); + +const ContentInner = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isHubChild", +})<{ $isHubChild?: boolean }>(({ $isHubChild }) => ({ + display: "flex", + flexDirection: "column", + gap: $isHubChild ? "8px" : "24px", +})); + +const TitleSection = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "8px", +}); + +const Title = styled(Typography, { + shouldForwardProp: (prop) => prop !== "$isUntitled", +})<{ $isUntitled: boolean }>(({ $isUntitled }) => ({ + fontSize: "14px", + fontWeight: 600, + lineHeight: "20px", + color: $isUntitled ? canonHeaderTokens.foreground.accent20 : canonHeaderTokens.foreground.accent0, +})); + +const AddressRow = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isVisible", +})<{ $isVisible: boolean }>(({ $isVisible }) => ({ + display: "flex", + alignItems: "center", + gap: "8px", + opacity: $isVisible ? 1 : 0, + transition: "opacity 0.2s ease", + pointerEvents: $isVisible ? "auto" : "none", +})); + +const AddressText = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const TypeLabel = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, + textTransform: "uppercase", +}); + +const ActionsSection = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isHubChild", +})<{ $isHubChild?: boolean }>(({ $isHubChild }) => ({ + display: "flex", + flexDirection: "column", + alignItems: "flex-end", + justifyContent: "space-between", + backgroundColor: canonHeaderTokens.background.layer1, + padding: $isHubChild ? "12px" : "16px", + minWidth: $isHubChild ? "150px" : "200px", + alignSelf: "stretch", // Ensure it fills parent height + gap: "32px", // Minimum gap between buttons and path indicator + borderRadius: "0 8px 8px 0", +})); + +// Styled pill button for children count (matches Figma design) +const ChildrenButton = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "center", + gap: "16px", + height: "36px", + padding: "8px 12px 8px 16px", + border: "none", + borderRadius: "100px", + backgroundColor: canonHeaderTokens.background.layer1Variation, + cursor: "pointer", + transition: "opacity 0.2s ease", + "&:hover": { + opacity: 0.8, + }, +}); + +const ChildrenButtonContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", + width: "40px", +}); + +const ChildrenCountText = styled(Typography)({ + fontSize: "12px", + fontWeight: 600, + lineHeight: "16px", + letterSpacing: "0.6px", + textTransform: "uppercase", + color: canonHeaderTokens.foreground.accent10, +}); + +const ActionsTop = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "16px", +}); + +const QueueButton = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "center", + gap: "16px", + height: "36px", + padding: "8px 12px 8px 16px", + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, + borderRadius: "100px", + backgroundColor: "transparent", + cursor: "pointer", + transition: "background-color 0.2s ease", + "&:hover": { + backgroundColor: canonHeaderTokens.background.layer1Variation, + }, +}); + +const ButtonText = styled(Typography)({ + fontSize: "12px", + fontWeight: 600, + lineHeight: "16px", + letterSpacing: "0.6px", + textTransform: "uppercase", + color: canonHeaderTokens.foreground.accent10, +}); + +const MoreButtonWrapper = styled(Box)({ + position: "relative", +}); + +const MoreButton = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "10px", + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, + borderRadius: "100px", + backgroundColor: "transparent", + cursor: "pointer", + transition: "background-color 0.2s ease", + "&:hover": { + backgroundColor: canonHeaderTokens.background.layer1Variation, + }, +}); + +const ActionsBottom = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", + justifyContent: "flex-end", +}); + +const PathIndicator = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isFastPath", +})<{ $isFastPath: boolean }>(({ $isFastPath }) => ({ + display: "flex", + alignItems: "center", + gap: $isFastPath ? "6px" : "8px", + justifyContent: "flex-end", +})); + +const PathText = styled(Typography, { + shouldForwardProp: (prop) => prop !== "$isFastPath", +})<{ $isFastPath: boolean }>(({ $isFastPath }) => ({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: $isFastPath ? canonHeaderTokens.brand.green : canonHeaderTokens.status.red, +})); diff --git a/src/components/CanonListSection/ActionMenu.tsx b/src/components/CanonListSection/ActionMenu.tsx new file mode 100644 index 0000000..2dcbe2f --- /dev/null +++ b/src/components/CanonListSection/ActionMenu.tsx @@ -0,0 +1,187 @@ +import { useRef, useEffect } from "react"; +import { Box, styled } from "@mui/material"; +import { CircleFadingPlusIcon, SquarePenIcon, ZapIcon, ZapOffIcon, TrashIcon, PlusIcon } from "~/components/icons"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; + +interface ActionMenuProps { + isOpen: boolean; + onClose: () => void; + isHub?: boolean; + isHubChild?: boolean; + isFastPath?: boolean; + triggerRef?: React.RefObject; + onAddToQueue?: () => void; + onRename?: () => void; + onProposePreApproval?: () => void; + onDeployChild?: () => void; + onRemove?: () => void; + isRemoving?: boolean; +} + +export const ActionMenu = ({ + isOpen, + onClose, + isHub = false, + isHubChild = false, + isFastPath = false, + triggerRef, + onAddToQueue, + onRename, + onProposePreApproval, + onDeployChild, + onRemove, + isRemoving, +}: ActionMenuProps) => { + const menuRef = useRef(null); + + // Close menu when clicking outside (but not on the trigger button) + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Node; + // Ignore clicks on the menu itself + if (menuRef.current && menuRef.current.contains(target)) { + return; + } + // Ignore clicks on the trigger button (let the toggle handler handle it) + if (triggerRef?.current && triggerRef.current.contains(target)) { + return; + } + onClose(); + }; + + if (isOpen) { + // Add a small delay to prevent immediate close from the same click + setTimeout(() => { + document.addEventListener("mousedown", handleClickOutside); + }, 0); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isOpen, onClose, triggerRef]); + + if (!isOpen) return null; + + // Hub menu: Rename, Propose Pre-Approval, Deploy Child, Remove Hub + if (isHub) { + return ( + + + + Rename + + + + {isFastPath ? ( + + ) : ( + + )} + {isFastPath ? "Remove pre-approval" : "Pre-Approve"} + + + + + Deploy Child + + + + + {isRemoving ? "Removing..." : "Remove Hub"} + + + ); + } + + // Hub child menu: Add to Queue, Rename (no remove, no pre-approval) + if (isHubChild) { + return ( + + + + Add to Queue + + + + + Rename + + + ); + } + + // Regular action menu: Add to Queue, Rename, Pre-Approve, Remove + return ( + + + + Add to Queue + + + + + Rename + + + + {isFastPath ? ( + + ) : ( + + )} + {isFastPath ? "Remove pre-approval" : "Pre-Approve"} + + + + + {isRemoving ? "Removing..." : "Remove Action"} + + + ); +}; + +const MenuContainer = styled(Box)({ + position: "absolute", + top: "calc(100% + 8px)", + right: 0, + width: "264px", + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "12px", + border: `0.5px solid ${canonHeaderTokens.foreground.accent40}`, + boxShadow: "0px 20px 25px -5px rgba(0, 0, 0, 0.1), 0px 8px 10px -6px rgba(0, 0, 0, 0.1)", + overflow: "hidden", + zIndex: 100, +}); + +const MenuItem = styled("button")<{ disabled?: boolean }>(({ disabled }) => ({ + display: "flex", + alignItems: "center", + gap: "16px", + padding: "16px", + backgroundColor: "transparent", + border: "none", + cursor: disabled ? "not-allowed" : "pointer", + opacity: disabled ? 0.5 : 1, + width: "100%", + transition: "background-color 0.15s ease", + "&:hover": { + backgroundColor: disabled ? "transparent" : canonHeaderTokens.background.layer1Variation, + }, +})); + +const MenuItemText = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent10, +}); + +const MenuDivider = styled(Box)({ + height: "0.5px", + width: "100%", + backgroundColor: canonHeaderTokens.foreground.accent40, +}); diff --git a/src/components/CanonListSection/PreApproveDurationModal.tsx b/src/components/CanonListSection/PreApproveDurationModal.tsx new file mode 100644 index 0000000..903ef59 --- /dev/null +++ b/src/components/CanonListSection/PreApproveDurationModal.tsx @@ -0,0 +1,351 @@ +import { useState, useEffect, useMemo, useRef } from "react"; +import { Box, styled } from "@mui/material"; +import { useConfig } from "wagmi"; +import { readContract } from "wagmi/actions"; +import { canonGuardAbi } from "~/abis/canonGuard"; +import { XIcon, ZapIcon } from "~/components/icons"; +import { DurationInput } from "~/components/shared/DurationInput"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { humanizeDuration } from "~/hooks/useCanonGuardConfig"; +import { DURATION_TIME_MULTIPLIERS, type DurationTimeUnit } from "~/utils/timeUnits"; +import type { Address } from "viem"; + +interface PreApproveDurationModalProps { + isOpen: boolean; + onClose: () => void; + onSubmit: (durationSeconds: bigint) => void; + actionLabel?: string; + guardAddress: Address | null; + chainId: number | null; +} + +export const PreApproveDurationModal = ({ + isOpen, + onClose, + onSubmit, + actionLabel, + guardAddress, + chainId, +}: PreApproveDurationModalProps) => { + const config = useConfig(); + const modalRef = useRef(null); + + // Duration state + const [durationAmount, setDurationAmount] = useState("1"); + const [durationUnit, setDurationUnit] = useState("hours"); + const [maxApprovalDuration, setMaxApprovalDuration] = useState(null); + const [isLoadingMax, setIsLoadingMax] = useState(false); + + // Fetch MAX_APPROVAL_DURATION from Canon Guard contract + useEffect(() => { + const fetchMaxDuration = async () => { + if (!guardAddress || !chainId) return; + + setIsLoadingMax(true); + try { + const maxDuration = await readContract(config, { + address: guardAddress, + abi: canonGuardAbi, + functionName: "MAX_APPROVAL_DURATION", + chainId: chainId, + }); + setMaxApprovalDuration(maxDuration as bigint); + } catch (error) { + console.error("[PreApproveDurationModal] Failed to fetch MAX_APPROVAL_DURATION:", error); + } finally { + setIsLoadingMax(false); + } + }; + + if (isOpen) { + fetchMaxDuration(); + } + }, [config, guardAddress, chainId, isOpen]); + + // Calculate total duration in seconds and validate + const { totalDurationSeconds, isValid, errorMessage } = useMemo(() => { + const amount = parseFloat(durationAmount) || 0; + if (amount <= 0) { + return { totalDurationSeconds: 0n, isValid: false, errorMessage: "Duration must be greater than 0" }; + } + + const multiplier = DURATION_TIME_MULTIPLIERS[durationUnit]; + const totalSeconds = BigInt(Math.floor(amount * multiplier)); + + if (maxApprovalDuration !== null && totalSeconds > maxApprovalDuration) { + const maxHumanized = humanizeDuration(maxApprovalDuration); + return { + totalDurationSeconds: totalSeconds, + isValid: false, + errorMessage: `Exceeds maximum duration of ${maxHumanized}`, + }; + } + + return { totalDurationSeconds: totalSeconds, isValid: true, errorMessage: null }; + }, [durationAmount, durationUnit, maxApprovalDuration]); + + // Close modal when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (modalRef.current && !modalRef.current.contains(event.target as Node)) { + onClose(); + } + }; + + if (isOpen) { + setTimeout(() => { + document.addEventListener("mousedown", handleClickOutside); + }, 0); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isOpen, onClose]); + + // Close on escape key + useEffect(() => { + const handleEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") { + onClose(); + } + }; + + if (isOpen) { + document.addEventListener("keydown", handleEscape); + } + + return () => { + document.removeEventListener("keydown", handleEscape); + }; + }, [isOpen, onClose]); + + // Reset state when modal opens + useEffect(() => { + if (isOpen) { + setDurationAmount("1"); + setDurationUnit("hours"); + } + }, [isOpen]); + + const handleSubmit = () => { + if (!isValid) return; + onSubmit(totalDurationSeconds); + }; + + if (!isOpen) return null; + + return ( + + + + + + + + Pre-Approve Action + + + + + + + {actionLabel && ( + + Action: {actionLabel} + + )} + + + Set the duration for this pre-approval. Once deployed, the action can be executed without additional + signatures during this period. + + + + Duration + + {!isValid && errorMessage && ( + + {errorMessage} + + )} + + + + CANCEL + + CONTINUE + + + + + ); +}; + +const Overlay = styled(Box)({ + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "rgba(0, 0, 0, 0.6)", + display: "flex", + alignItems: "center", + justifyContent: "center", + zIndex: 1000, +}); + +const ModalContainer = styled(Box)({ + width: "420px", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "16px", + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, + boxShadow: "0px 20px 25px -5px rgba(0, 0, 0, 0.3), 0px 8px 10px -6px rgba(0, 0, 0, 0.3)", + overflow: "hidden", +}); + +const ModalHeader = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "20px 24px 12px 24px", +}); + +const HeaderLeft = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", +}); + +const IconWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", +}); + +const ModalTitle = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "18px", + fontWeight: 600, + lineHeight: "24px", + color: canonHeaderTokens.foreground.accent0, +}); + +const CloseButton = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "4px", + backgroundColor: "transparent", + border: "none", + cursor: "pointer", + borderRadius: "4px", + "&:hover": { + backgroundColor: canonHeaderTokens.background.layer1Variation, + }, +}); + +const ActionLabelRow = styled(Box)({ + padding: "0 24px 8px 24px", +}); + +const ActionLabelText = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const ModalDescription = styled("p")({ + margin: 0, + padding: "0 24px 20px 24px", + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent20, +}); + +const DurationInputSection = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "8px", + padding: "0 24px 24px 24px", +}); + +const DurationLabel = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 500, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent10, +}); + +const DurationError = styled(Box)({ + display: "flex", + alignItems: "center", +}); + +const ErrorText = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.status.red, +}); + +const ButtonRow = styled(Box)({ + display: "flex", + gap: "12px", + padding: "0 24px 24px 24px", +}); + +const CancelButton = styled("button")({ + flex: 1, + height: "44px", + padding: "12px 24px", + borderRadius: "100px", + backgroundColor: "transparent", + color: canonHeaderTokens.foreground.accent10, + fontSize: "12px", + fontWeight: 600, + lineHeight: "16px", + letterSpacing: "0.6px", + textTransform: "uppercase", + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, + cursor: "pointer", + transition: "background-color 0.2s ease", + "&:hover": { + backgroundColor: canonHeaderTokens.background.layer1Variation, + }, +}); + +const SubmitButton = styled("button")<{ disabled?: boolean }>(({ disabled }) => ({ + flex: 1, + height: "44px", + padding: "12px 24px", + borderRadius: "100px", + backgroundColor: disabled ? canonHeaderTokens.foreground.accent40 : canonHeaderTokens.brand.green, + color: "#ffffff", + fontSize: "12px", + fontWeight: 600, + lineHeight: "16px", + letterSpacing: "0.6px", + textTransform: "uppercase", + border: "none", + cursor: disabled ? "not-allowed" : "pointer", + opacity: disabled ? 0.6 : 1, + transition: "opacity 0.2s ease", + "&:hover": { + opacity: disabled ? 0.6 : 0.9, + }, +})); diff --git a/src/components/CanonListSection/RenameModal.tsx b/src/components/CanonListSection/RenameModal.tsx new file mode 100644 index 0000000..9050090 --- /dev/null +++ b/src/components/CanonListSection/RenameModal.tsx @@ -0,0 +1,318 @@ +import { useState, useEffect, useRef, useMemo } from "react"; +import { Box, styled, CircularProgress } from "@mui/material"; +import { XIcon, SquarePenIcon } from "~/components/icons"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; + +interface RenameModalProps { + isOpen: boolean; + onClose: () => void; + onSubmit: (newLabel: string) => void; + currentLabel: string; + isLoading?: boolean; +} + +export const RenameModal = ({ isOpen, onClose, onSubmit, currentLabel, isLoading = false }: RenameModalProps) => { + const modalRef = useRef(null); + const inputRef = useRef(null); + + // Input state - pre-populate with current label + const [newLabel, setNewLabel] = useState(currentLabel); + + // Validation + const { isValid, errorMessage } = useMemo(() => { + const trimmed = newLabel.trim(); + if (!trimmed) { + return { isValid: false, errorMessage: "Name cannot be empty" }; + } + return { isValid: true, errorMessage: null }; + }, [newLabel]); + + // Close modal when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (modalRef.current && !modalRef.current.contains(event.target as Node)) { + onClose(); + } + }; + + if (isOpen) { + setTimeout(() => { + document.addEventListener("mousedown", handleClickOutside); + }, 0); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isOpen, onClose]); + + // Close on escape key + useEffect(() => { + const handleEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") { + onClose(); + } + }; + + if (isOpen) { + document.addEventListener("keydown", handleEscape); + } + + return () => { + document.removeEventListener("keydown", handleEscape); + }; + }, [isOpen, onClose]); + + // Reset state and focus input when modal opens + useEffect(() => { + if (isOpen) { + setNewLabel(currentLabel); + // Focus input after a short delay to ensure modal is rendered + setTimeout(() => { + inputRef.current?.focus(); + inputRef.current?.select(); + }, 50); + } + }, [isOpen, currentLabel]); + + const handleSubmit = () => { + if (!isValid || isLoading) return; + onSubmit(newLabel.trim()); + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === "Enter") { + handleSubmit(); + } + }; + + if (!isOpen) return null; + + return ( + + + + + + + + Rename Action + + + + + + + + Enter a new name for this action. This will update the label in the Canon List. + + + + Name + setNewLabel(e.target.value)} + onKeyDown={handleKeyDown} + placeholder='Enter action name' + $hasError={!isValid && newLabel !== currentLabel} + disabled={isLoading} + /> + {!isValid && errorMessage && newLabel !== currentLabel && ( + + {errorMessage} + + )} + + + + + CANCEL + + + {isLoading ? : "SAVE"} + + + + + ); +}; + +const Overlay = styled(Box)({ + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "rgba(0, 0, 0, 0.6)", + display: "flex", + alignItems: "center", + justifyContent: "center", + zIndex: 1000, +}); + +const ModalContainer = styled(Box)({ + width: "420px", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "16px", + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, + boxShadow: "0px 20px 25px -5px rgba(0, 0, 0, 0.3), 0px 8px 10px -6px rgba(0, 0, 0, 0.3)", + overflow: "hidden", +}); + +const ModalHeader = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "20px 24px 12px 24px", +}); + +const HeaderLeft = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", +}); + +const IconWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", +}); + +const ModalTitle = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "18px", + fontWeight: 600, + lineHeight: "24px", + color: canonHeaderTokens.foreground.accent0, +}); + +const CloseButton = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "4px", + backgroundColor: "transparent", + border: "none", + cursor: "pointer", + borderRadius: "4px", + "&:hover": { + backgroundColor: canonHeaderTokens.background.layer1Variation, + }, +}); + +const ModalDescription = styled("p")({ + margin: 0, + padding: "0 24px 20px 24px", + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent20, +}); + +const InputSection = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "8px", + padding: "0 24px 24px 24px", +}); + +const InputLabel = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 500, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent10, +}); + +const NameInput = styled("input")<{ $hasError?: boolean }>(({ $hasError }) => ({ + width: "100%", + height: "40px", + padding: "0 12px", + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent0, + backgroundColor: canonHeaderTokens.background.layer0, + border: `1px solid ${$hasError ? canonHeaderTokens.status.red : canonHeaderTokens.foreground.accent40}`, + borderRadius: "6px", + outline: "none", + boxSizing: "border-box", + transition: "border-color 0.2s ease", + "&:focus": { + borderColor: $hasError ? canonHeaderTokens.status.red : canonHeaderTokens.foreground.accent20, + }, + "&::placeholder": { + color: canonHeaderTokens.foreground.accent30, + }, + "&:disabled": { + opacity: 0.6, + cursor: "not-allowed", + }, +})); + +const InputError = styled(Box)({ + display: "flex", + alignItems: "center", +}); + +const ErrorText = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.status.red, +}); + +const ButtonRow = styled(Box)({ + display: "flex", + gap: "12px", + padding: "0 24px 24px 24px", +}); + +const CancelButton = styled("button")<{ disabled?: boolean }>(({ disabled }) => ({ + flex: 1, + height: "44px", + padding: "12px 24px", + borderRadius: "100px", + backgroundColor: "transparent", + color: canonHeaderTokens.foreground.accent10, + fontSize: "12px", + fontWeight: 600, + lineHeight: "16px", + letterSpacing: "0.6px", + textTransform: "uppercase", + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, + cursor: disabled ? "not-allowed" : "pointer", + opacity: disabled ? 0.6 : 1, + transition: "background-color 0.2s ease", + "&:hover": { + backgroundColor: disabled ? "transparent" : canonHeaderTokens.background.layer1Variation, + }, +})); + +const SubmitButton = styled("button")<{ disabled?: boolean }>(({ disabled }) => ({ + flex: 1, + height: "44px", + padding: "12px 24px", + borderRadius: "100px", + backgroundColor: disabled ? canonHeaderTokens.foreground.accent40 : canonHeaderTokens.brand.green, + color: "#ffffff", + fontSize: "12px", + fontWeight: 600, + lineHeight: "16px", + letterSpacing: "0.6px", + textTransform: "uppercase", + border: "none", + cursor: disabled ? "not-allowed" : "pointer", + opacity: disabled ? 0.6 : 1, + transition: "opacity 0.2s ease", + display: "flex", + alignItems: "center", + justifyContent: "center", + "&:hover": { + opacity: disabled ? 0.6 : 0.9, + }, +})); diff --git a/src/components/CanonListSection/index.tsx b/src/components/CanonListSection/index.tsx new file mode 100644 index 0000000..6abd16c --- /dev/null +++ b/src/components/CanonListSection/index.tsx @@ -0,0 +1,836 @@ +import { useState, useEffect, useCallback, useMemo } from "react"; +import { Box, Typography, styled, CircularProgress } from "@mui/material"; +import { + SearchIcon, + ShieldCheckIcon, + HelpCircleIcon, + ChevronLeftIcon, + ChevronRightIcon, + EllipsisIcon, +} from "~/components/icons"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { useNavigateWithParams } from "~/hooks"; +import { useClientService } from "~/hooks/useServices"; +import { useStateContext } from "~/hooks/useStateContext"; +import { useTransactionExecutor } from "~/hooks/useTransactionExecutor"; +import { RegistryService, RegisteredEntity } from "~/services"; +import { SafeInfo } from "~/types"; +import { getFactoryDisplayName } from "~/utils/factoryDisplay"; +import { ActionItem } from "./ActionItem"; +import { PreApproveDurationModal } from "./PreApproveDurationModal"; +import { RenameModal } from "./RenameModal"; +import type { Address } from "viem"; + +// Extended entity type for grouped display +interface DisplayEntity extends RegisteredEntity { + children?: RegisteredEntity[]; +} + +interface CanonListSectionProps { + safeInfo: SafeInfo; + onQueueCountChange?: () => void; +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const CanonListSection = ({ safeInfo }: CanonListSectionProps) => { + const { guardAddress, chainId } = useStateContext(); + const clientService = useClientService(); + const navigateWithParams = useNavigateWithParams(); + const { executeRemoveFromRegistry, executeRecordToRegistry, isExecuting } = useTransactionExecutor(); + + const [entities, setEntities] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [totalCount, setTotalCount] = useState(0); + const [removingAddress, setRemovingAddress] = useState
(null); + const [searchQuery, setSearchQuery] = useState(""); + + // Hub expansion state - tracks which hubs are expanded + const [expandedHubs, setExpandedHubs] = useState>(new Set()); + + // Pre-approve modal state + const [preApproveModalOpen, setPreApproveModalOpen] = useState(false); + const [selectedEntityForPreApprove, setSelectedEntityForPreApprove] = useState(null); + + // Rename modal state + const [renameModalOpen, setRenameModalOpen] = useState(false); + const [selectedEntityForRename, setSelectedEntityForRename] = useState(null); + const [isRenaming, setIsRenaming] = useState(false); + + // Pagination state + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 25; + + const fetchEntities = useCallback(async () => { + if (!guardAddress) { + setLoading(false); + return; + } + + try { + setLoading(true); + setError(null); + + const registryService = new RegistryService(clientService); + const offset = (currentPage - 1) * itemsPerPage; + + const [fetchedEntities, total] = await Promise.all([ + registryService.getRegisteredEntities( + guardAddress, + guardAddress, // entrypoint address (same as guardAddress) + offset, + itemsPerPage, + ), + registryService.getTotalEntities(guardAddress), + ]); + + setEntities(fetchedEntities); + setTotalCount(total); + } catch (err) { + console.error("Failed to fetch registry entities:", err); + setError("Failed to load Canon List"); + } finally { + setLoading(false); + } + }, [guardAddress, clientService, currentPage]); + + useEffect(() => { + fetchEntities(); + }, [fetchEntities]); + + const totalPages = Math.ceil(totalCount / itemsPerPage); + + // Group entities: hubs with their children, standalone entities remain separate + // Children are entities whose parentHubAddress matches a hub's address + const groupedEntities = useMemo((): DisplayEntity[] => { + // First, separate hubs and children + const hubs = entities.filter((e) => e.isHub); + const children = entities.filter((e) => e.parentHubAddress && !e.isHub); + const standalone = entities.filter((e) => !e.isHub && !e.parentHubAddress); + + // Create a map of hub address to children + const hubChildrenMap = new Map(); + for (const child of children) { + if (child.parentHubAddress) { + const existing = hubChildrenMap.get(child.parentHubAddress) || []; + hubChildrenMap.set(child.parentHubAddress, [...existing, child]); + } + } + + // Build display entities: hubs with their children, then standalone + const result: DisplayEntity[] = []; + + for (const hub of hubs) { + const hubChildren = hubChildrenMap.get(hub.address) || []; + result.push({ + ...hub, + children: hubChildren, + childrenCount: hubChildren.length, + }); + } + + // Add standalone entities (not hubs, not children of hubs) + for (const entity of standalone) { + result.push(entity); + } + + return result; + }, [entities]); + + // Filter entities based on search query + const filteredEntities = useMemo((): DisplayEntity[] => { + if (!searchQuery) return groupedEntities; + const query = searchQuery.toLowerCase(); + + return groupedEntities.filter((entity) => { + const matchesLabel = entity.label?.toLowerCase().includes(query); + const matchesAddress = entity.address.toLowerCase().includes(query); + + // For hubs, also check if any children match + if (entity.isHub && entity.children) { + const childMatches = entity.children.some( + (child) => child.label?.toLowerCase().includes(query) || child.address.toLowerCase().includes(query), + ); + return matchesLabel || matchesAddress || childMatches; + } + + return matchesLabel || matchesAddress; + }); + }, [groupedEntities, searchQuery]); + + // Toggle hub expansion + const handleToggleHubExpansion = useCallback((hubAddress: Address) => { + setExpandedHubs((prev) => { + const newSet = new Set(prev); + if (newSet.has(hubAddress)) { + newSet.delete(hubAddress); + } else { + newSet.add(hubAddress); + } + return newSet; + }); + }, []); + + const handleQueue = useCallback( + (entity: RegisteredEntity) => { + // Navigate to queue action page + // Note: This flow is not refresh-safe since details are in state only + navigateWithParams("/queue-action", { + state: { + actionBuilderAddress: entity.address, + label: entity.label, + factoryType: getFactoryDisplayName(entity.factoryType), + }, + }); + }, + [navigateWithParams], + ); + + const handleAddToQueue = useCallback( + (entity: RegisteredEntity) => { + // Same as handleQueue + navigateWithParams("/queue-action", { + state: { + actionBuilderAddress: entity.address, + label: entity.label, + factoryType: getFactoryDisplayName(entity.factoryType), + }, + }); + }, + [navigateWithParams], + ); + + const handleRename = useCallback((entity: RegisteredEntity) => { + setSelectedEntityForRename(entity); + setRenameModalOpen(true); + }, []); + + const handleRenameSubmit = useCallback( + async (newLabel: string) => { + if (!selectedEntityForRename || !guardAddress) return; + + setIsRenaming(true); + try { + const result = await executeRecordToRegistry( + guardAddress as Address, + selectedEntityForRename.address, + newLabel, + ); + + if (result) { + console.log("Entity renamed successfully:", result); + // Optimistic update: update local state without refetching + setEntities((prev) => + prev.map((e) => (e.address === selectedEntityForRename.address ? { ...e, label: newLabel } : e)), + ); + // Close modal + setRenameModalOpen(false); + setSelectedEntityForRename(null); + } + } catch (err) { + console.error("Failed to rename entity:", err); + } finally { + setIsRenaming(false); + } + }, + [selectedEntityForRename, guardAddress, executeRecordToRegistry], + ); + + const handleRenameClose = useCallback(() => { + if (!isRenaming) { + setRenameModalOpen(false); + setSelectedEntityForRename(null); + } + }, [isRenaming]); + + const handleProposePreApproval = useCallback( + (entity: RegisteredEntity) => { + // If already pre-approved, skip modal and navigate directly with duration=0 + if (entity.isFastPath) { + navigateWithParams("/queue-action", { + state: { + actionBuilderAddress: entity.address, + label: entity.label, + factoryType: getFactoryDisplayName(entity.factoryType), + approvalDuration: 0n, + }, + }); + return; + } + // Otherwise, open the modal to select duration + setSelectedEntityForPreApprove(entity); + setPreApproveModalOpen(true); + }, + [navigateWithParams], + ); + + const handlePreApproveSubmit = useCallback( + (durationSeconds: bigint) => { + if (!selectedEntityForPreApprove) return; + + // Close modal + setPreApproveModalOpen(false); + + // Navigate to queue action page - pre-approve mode is determined by presence of approvalDuration + // Note: This flow is not refresh-safe since duration is in state only + navigateWithParams("/queue-action", { + state: { + actionBuilderAddress: selectedEntityForPreApprove.address, + label: selectedEntityForPreApprove.label, + factoryType: getFactoryDisplayName(selectedEntityForPreApprove.factoryType), + approvalDuration: durationSeconds, + }, + }); + + // Clear selected entity + setSelectedEntityForPreApprove(null); + }, + [selectedEntityForPreApprove, navigateWithParams], + ); + + const handlePreApproveClose = useCallback(() => { + setPreApproveModalOpen(false); + setSelectedEntityForPreApprove(null); + }, []); + + const handleRemove = useCallback( + async (entity: RegisteredEntity) => { + if (!guardAddress || isExecuting) return; + + setRemovingAddress(entity.address); + try { + const result = await executeRemoveFromRegistry(guardAddress as Address, [entity.address]); + + if (result) { + console.log("Entity removed successfully:", result); + // Optimistically remove from local state + setEntities((prev) => prev.filter((e) => e.address !== entity.address)); + } + } catch (err) { + console.error("Failed to remove entity:", err); + } + setRemovingAddress(null); + }, + [guardAddress, isExecuting, executeRemoveFromRegistry], + ); + + // Handle deploying a child from a hub + const handleDeployChild = useCallback( + (hubEntity: RegisteredEntity) => { + navigateWithParams(`/create/hub-child/${hubEntity.address}`, { + state: { + hubLabel: hubEntity.label, + isFastPath: hubEntity.isFastPath, + }, + }); + }, + [navigateWithParams], + ); + + return ( + + + {/* Header Section */} + + + + Canon list + + + + + {/* Info Banner */} + + + + + + + New transactions are saved to your Canon List, making it easy to manage and reuse them over time. + + + + + {/* Search Bar */} + + + + setSearchQuery(e.target.value)} + /> + + + + + {/* Content Section */} + + {filteredEntities.length > 0 && ( + + SAVED TRANSACTIONS + + )} + + {loading ? ( + + + Loading actions... + + ) : error ? ( + + {error} + + ) : entities.length === 0 ? ( + + No saved transactions yet + Create your first action and it will appear here in your Canon List. + + ) : filteredEntities.length === 0 ? ( + No items match your search + ) : ( + + {filteredEntities.map((entity) => ( + + handleToggleHubExpansion(entity.address) : undefined} + onQueue={entity.isHub ? undefined : () => handleQueue(entity)} + onAddToQueue={entity.isHub ? undefined : () => handleAddToQueue(entity)} + onRename={() => handleRename(entity)} + onProposePreApproval={() => handleProposePreApproval(entity)} + onDeployChild={entity.isHub ? () => handleDeployChild(entity) : undefined} + onRemove={() => handleRemove(entity)} + isRemoving={removingAddress === entity.address && isExecuting} + /> + {/* Render children if hub is expanded and has children */} + {entity.isHub && + entity.children && + entity.children.length > 0 && + expandedHubs.has(entity.address) && ( + + {entity.children.map((child, index) => ( + + {/* Tree connector visual */} + + + + + + + handleAddToQueue(child)} + onRename={() => handleRename(child)} + onProposePreApproval={undefined} // Children don't have pre-approval + onRemove={undefined} // Children can't be removed directly + isRemoving={false} + /> + + + ))} + + )} + + ))} + + )} + + {/* Pagination */} + {filteredEntities.length > 0 && ( + + + Showing {Math.min(itemsPerPage, entities.length)} out of {totalCount} + + + setCurrentPage((p) => Math.max(1, p - 1))} disabled={currentPage === 1}> + + + + {/* Page numbers */} + {[...Array(Math.min(3, totalPages))].map((_, i) => { + const pageNum = i + 1; + return ( + setCurrentPage(pageNum)}> + {pageNum} + + ); + })} + + {totalPages > 3 && ( + <> + + setCurrentPage(totalPages)}> + {totalPages} + + + )} + + setCurrentPage((p) => Math.min(totalPages, p + 1))} + disabled={currentPage === totalPages} + > + + + + + )} + + + + {/* Pre-Approve Duration Modal */} + + + {/* Rename Modal */} + + + ); +}; + +// Styled components +const Container = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "center", + flex: 1, + padding: "32px 120px", + width: "100%", + minHeight: "100%", +}); + +const ContentWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "20px", + width: "1024px", + maxWidth: "100%", +}); + +const HeaderSection = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", +}); + +const TitleRow = styled(Box)({ + display: "flex", + alignItems: "flex-start", + padding: "32px 8px 12px 8px", +}); + +const TitleGroup = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", + flex: 1, +}); + +const PageTitle = styled(Typography)({ + fontSize: "24px", + fontWeight: 600, + fontStyle: "italic", + lineHeight: "32px", + color: canonHeaderTokens.foreground.accent0, +}); + +const InfoBanner = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + padding: "20px", + overflow: "hidden", +}); + +const InfoContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "16px", +}); + +const IconWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "48px", + height: "48px", + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, + borderRadius: "100px", +}); + +const InfoText = styled(Typography)({ + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent10, +}); + +const SearchBar = styled(Box)({ + display: "flex", + alignItems: "center", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + height: "48px", +}); + +const SearchSection = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", + flex: 1, + padding: "16px", +}); + +const SearchInput = styled("input")({ + flex: 1, + background: "transparent", + border: "none", + outline: "none", + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent0, + "&::placeholder": { + color: canonHeaderTokens.foreground.accent30, + }, +}); + +const ListSection = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "6px", +}); + +const SectionHeader = styled(Box)({ + display: "flex", + alignItems: "center", + padding: "8px", +}); + +const SectionLabel = styled(Typography)({ + fontSize: "11px", + fontWeight: 600, + lineHeight: "12px", + color: canonHeaderTokens.foreground.accent30, +}); + +const ItemsList = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "6px", +}); + +const ChildrenContainer = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "6px", + marginTop: "6px", + paddingBottom: "12px", +}); + +// Wrapper for each child item with tree connector +const ChildWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + width: "100%", + position: "relative", // Required for z-index stacking of dropdown menus +}); + +// Tree connector container (left side visual) +const TreeConnector = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isLast", +})<{ $isLast: boolean }>({ + position: "relative", + display: "flex", + flexDirection: "column", + alignItems: "flex-end", + width: "24px", + height: "100%", + alignSelf: "stretch", + backgroundColor: canonHeaderTokens.foreground.accent50, +}); + +// Vertical line that runs down the left side +const TreeVerticalLine = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isLast", +})<{ $isLast: boolean }>(({ $isLast }) => ({ + position: "absolute", + left: "9px", + top: "42px", + width: "11px", + height: "50px", + borderLeft: `0.5px solid ${canonHeaderTokens.foreground.accent40}`, + opacity: $isLast ? 0 : 1, +})); + +// Horizontal branch connecting to the child +const TreeHorizontalBranch = styled(Box)({ + width: "15px", + height: "42px", + borderLeft: `0.5px solid ${canonHeaderTokens.foreground.accent40}`, + borderBottom: `0.5px solid ${canonHeaderTokens.foreground.accent40}`, +}); + +// Small dot at the connection point +const TreeDot = styled(Box)({ + position: "absolute", + left: "7px", + top: "40px", + width: "4px", + height: "4px", + backgroundColor: canonHeaderTokens.background.layer0, + border: `0.5px solid ${canonHeaderTokens.foreground.accent30}`, +}); + +// Wrapper for the actual child ActionItem +const ChildItemWrapper = styled(Box)({ + flex: 1, + minWidth: 0, +}); + +const LoadingState = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + gap: "12px", + padding: "48px", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", +}); + +const LoadingText = styled(Typography)({ + fontSize: "14px", + color: canonHeaderTokens.foreground.accent20, +}); + +const ErrorState = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "48px", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", +}); + +const ErrorText = styled(Typography)({ + fontSize: "14px", + color: canonHeaderTokens.status.red, +}); + +const EmptyState = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + gap: "8px", + padding: "64px", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", +}); + +const EmptyText = styled(Typography)({ + fontSize: "16px", + fontWeight: 600, + color: canonHeaderTokens.foreground.accent10, +}); + +const EmptySubtext = styled(Typography)({ + fontSize: "14px", + color: canonHeaderTokens.foreground.accent20, + textAlign: "center", +}); + +const SearchEmptyState = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + height: "200px", + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 400, + color: canonHeaderTokens.foreground.accent20, + fontStyle: "italic", +}); + +const PaginationRow = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "12px 0", +}); + +const PaginationInfo = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, + padding: "0 16px", +}); + +const PaginationControls = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", + padding: "0 16px", +}); + +const PaginationArrow = styled("button")<{ disabled?: boolean }>(({ disabled }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "14px", + height: "14px", + background: "none", + border: "none", + cursor: disabled ? "default" : "pointer", + opacity: disabled ? 0.4 : 1, + padding: 0, +})); + +const PageNumber = styled("button")<{ $active?: boolean }>(({ $active }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "28px", + height: "28px", + borderRadius: "8px", + backgroundColor: $active ? canonHeaderTokens.background.layer1 : "transparent", + border: "none", + cursor: "pointer", + fontSize: "13px", + fontWeight: 600, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, + "&:hover": { + backgroundColor: canonHeaderTokens.background.layer1, + }, +})); + +export default CanonListSection; diff --git a/src/components/ChangeGuardSection/index.tsx b/src/components/ChangeGuardSection/index.tsx new file mode 100644 index 0000000..32e5347 --- /dev/null +++ b/src/components/ChangeGuardSection/index.tsx @@ -0,0 +1,380 @@ +import { useState, useEffect, useCallback, useMemo } from "react"; +import { Box, styled, CircularProgress } from "@mui/material"; +import { Address, Hex, encodeFunctionData } from "viem"; +import { canonGuardAbi, safeAbi } from "~/abis/canonGuard"; +import { changeSafeGuardActionFactoryAbi } from "~/abis/canonGuard"; +import { SigningFlowStep } from "~/components/NewAction/steps/SigningFlowStep"; +import { getRpcUrlForChain, getChainConfig } from "~/config/chains"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { CHANGE_SAFE_GUARD_ACTION_FACTORY } from "~/constants/canonGuard"; +import { useStateContext, useNavigateWithParams } from "~/hooks"; +import { useTransactionExecutor } from "~/hooks/useTransactionExecutor"; +import { ClientService, QueueService, type QueueItem } from "~/services"; +import type { TransactionStep } from "~/services/transactionBuilderService"; + +type ChangeGuardMode = "attach" | "detach"; + +interface ChangeGuardSectionProps { + mode: ChangeGuardMode; + onQueueCountChange?: () => void; +} + +// Mode-specific labels +const MODE_CONFIG = { + attach: { + actionVerb: "Attach", + actionTitle: "Attach Canon Guard", + factoryType: "ATTACH GUARD", + loadingText: "Initializing Attach Guard Flow...", + deployTitle: "Deploy Attach Action", + deployDescription: "Deploy a ChangeSafeGuardAction contract that will attach the Canon Guard to your Safe", + queueTitle: "Queue Attach Action", + queueDescription: "Add the attach action to the Canon Guard queue", + signTitle: "Sign Attach Action", + signDescription: "Sign the Safe transaction to approve the attach action", + }, + detach: { + actionVerb: "Detach", + actionTitle: "Detach Canon Guard", + factoryType: "DETACH GUARD", + loadingText: "Initializing Detach Guard Flow...", + deployTitle: "Deploy Detach Action", + deployDescription: "Deploy a ChangeSafeGuardAction contract that will remove the Canon Guard from your Safe", + queueTitle: "Queue Detach Action", + queueDescription: "Add the detach action to the Canon Guard queue", + signTitle: "Sign Detach Action", + signDescription: "Sign the Safe transaction to approve the detach action", + }, +} as const; + +export const ChangeGuardSection = ({ mode, onQueueCountChange }: ChangeGuardSectionProps) => { + const navigateWithParams = useNavigateWithParams(); + const { guardAddress, safeAddress, chainId } = useStateContext(); + const config = MODE_CONFIG[mode]; + + const { executeDeployChangeSafeGuardAction, executeQueueTransaction, executeSignTransaction } = + useTransactionExecutor(); + + // Flow state + const [transactionSteps, setTransactionSteps] = useState([]); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isSigningComplete, setIsSigningComplete] = useState(false); + const [initialized, setInitialized] = useState(false); + + // Deployed action address (set after deploy step) + const [deployedActionAddress, setDeployedActionAddress] = useState
(null); + + // Nonce selection state + const [currentSafeNonce, setCurrentSafeNonce] = useState(0); + const [queueItems, setQueueItems] = useState([]); + const [nonceDataLoaded, setNonceDataLoaded] = useState(false); + + // Create client and queue services + const clientService = useMemo(() => { + const rpcUrl = getRpcUrlForChain(chainId as number); + const chain = getChainConfig(chainId as number).chain; + return new ClientService(rpcUrl, chain); + }, [chainId]); + + const queueService = useMemo(() => new QueueService(clientService), [clientService]); + + // Target address for the guard change: + // - attach: use the current guardAddress (from context) + // - detach: use address(0) + const targetGuardAddress = useMemo(() => { + if (mode === "attach") { + return guardAddress as Address; + } + return "0x0000000000000000000000000000000000000000" as Address; + }, [mode, guardAddress]); + + // Build the 3-step transaction flow + const buildChangeGuardSteps = useCallback((): TransactionStep[] => { + if (!guardAddress || !safeAddress) return []; + + const steps: TransactionStep[] = []; + + // Step 1: Deploy ChangeSafeGuardAction + const deployData = encodeFunctionData({ + abi: changeSafeGuardActionFactoryAbi, + functionName: "createChangeSafeGuardAction", + args: [targetGuardAddress], + }); + + steps.push({ + id: "deploy-change-guard-action", + title: config.deployTitle, + description: config.deployDescription, + status: "pending", + to: CHANGE_SAFE_GUARD_ACTION_FACTORY, + data: deployData, + }); + + // Step 2: Queue the transaction + const queueData = encodeFunctionData({ + abi: canonGuardAbi, + functionName: "queueTransaction", + args: ["0x0000000000000000000000000000000000000000" as Address], // Placeholder, will be replaced + }); + + steps.push({ + id: "queue-change-guard-action", + title: config.queueTitle, + description: config.queueDescription, + status: "waiting", + to: guardAddress as Address, + data: queueData, + }); + + // Step 3: Sign the transaction + const signData = encodeFunctionData({ + abi: safeAbi, + functionName: "approveHash", + args: ["0x0000000000000000000000000000000000000000000000000000000000000000" as Hex], + }); + + steps.push({ + id: "sign-change-guard-action", + title: config.signTitle, + description: config.signDescription, + status: "waiting", + to: safeAddress as Address, + data: signData, + }); + + return steps; + }, [guardAddress, safeAddress, targetGuardAddress, config]); + + // Initialize the flow + useEffect(() => { + if (!initialized && guardAddress && safeAddress) { + const steps = buildChangeGuardSteps(); + setTransactionSteps(steps); + setCurrentStepIndex(0); + setInitialized(true); + } + }, [initialized, guardAddress, safeAddress, buildChangeGuardSteps]); + + // Fetch nonce data when flow is initialized + useEffect(() => { + if (!initialized || !guardAddress || !safeAddress || nonceDataLoaded) return; + + const fetchNonceData = async () => { + try { + const [nonce, items] = await Promise.all([ + queueService.getCurrentSafeNonce(guardAddress as Address), + queueService.getQueueItems(guardAddress as Address, safeAddress as Address), + ]); + setCurrentSafeNonce(nonce); + setQueueItems(items); + setNonceDataLoaded(true); + } catch (error) { + console.error(`[ChangeGuardSection:${mode}] Failed to fetch nonce data:`, error); + setCurrentSafeNonce(0); + setQueueItems([]); + setNonceDataLoaded(true); + } + }; + + fetchNonceData(); + }, [initialized, guardAddress, safeAddress, queueService, nonceDataLoaded, mode]); + + // Handle step execution + const handleExecuteStep = useCallback( + async (nonce?: number) => { + if (currentStepIndex >= transactionSteps.length) return; + + const currentStep = transactionSteps[currentStepIndex]; + const stepIndex = currentStepIndex; + + // Update status to waiting + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "waiting" }; + return updated; + }); + + // Step 1: Deploy ChangeSafeGuardAction + if (currentStep.id === "deploy-change-guard-action") { + console.log(`[ChangeGuardSection:${mode}] Executing deploy step`); + + // Pass guardAddress for attach, undefined for detach (will use address(0)) + const deployAddress = mode === "attach" ? (guardAddress as Address) : undefined; + const result = await executeDeployChangeSafeGuardAction(deployAddress); + + if (result) { + console.log(`[ChangeGuardSection:${mode}] Deploy successful:`, result.deployedAddress); + setDeployedActionAddress(result.deployedAddress); + + // Update step to signed and move to next + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "signed", hash: result.txHash }; + if (stepIndex + 1 < updated.length) { + updated[stepIndex + 1] = { ...updated[stepIndex + 1], status: "pending" }; + } + return updated; + }); + setCurrentStepIndex(stepIndex + 1); + } else { + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + } + return; + } + + // Step 2: Queue the transaction + if (currentStep.id === "queue-change-guard-action") { + console.log(`[ChangeGuardSection:${mode}] Executing queue step`); + + if (!guardAddress || !deployedActionAddress) { + console.error(`[ChangeGuardSection:${mode}] Missing addresses for queue step`); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + return; + } + + const result = await executeQueueTransaction(guardAddress as Address, deployedActionAddress); + + if (result) { + console.log(`[ChangeGuardSection:${mode}] Queue successful:`, result.txHash); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "signed", hash: result.txHash }; + if (stepIndex + 1 < updated.length) { + updated[stepIndex + 1] = { ...updated[stepIndex + 1], status: "pending" }; + } + return updated; + }); + setCurrentStepIndex(stepIndex + 1); + } else { + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + } + return; + } + + // Step 3: Sign the transaction + if (currentStep.id === "sign-change-guard-action") { + console.log(`[ChangeGuardSection:${mode}] Executing sign step`); + + if (!safeAddress || !guardAddress || !deployedActionAddress) { + console.error(`[ChangeGuardSection:${mode}] Missing addresses for sign step`); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + return; + } + + const result = await executeSignTransaction( + safeAddress as Address, + guardAddress as Address, + deployedActionAddress, + nonce, + ); + + if (result) { + console.log(`[ChangeGuardSection:${mode}] Sign successful:`, result.safeTxHash); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "signed", hash: result.txHash }; + return updated; + }); + setIsSigningComplete(true); + onQueueCountChange?.(); + } else { + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + } + return; + } + }, + [ + currentStepIndex, + transactionSteps, + guardAddress, + safeAddress, + deployedActionAddress, + executeDeployChangeSafeGuardAction, + executeQueueTransaction, + executeSignTransaction, + onQueueCountChange, + mode, + ], + ); + + // Handle back navigation + const handleBack = useCallback(() => { + navigateWithParams("/settings"); + }, [navigateWithParams]); + + // Handle navigate to create (just go back to settings in this context) + const handleNavigateToCreate = useCallback(() => { + navigateWithParams("/settings"); + }, [navigateWithParams]); + + // Loading state + if (!initialized || transactionSteps.length === 0) { + return ( + + + {config.loadingText} + + ); + } + + return ( + + ); +}; + +// Keep the old export name for backwards compatibility +export const DetachGuardSection = (props: Omit) => ( + +); + +// Styled Components +const LoadingContainer = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + height: "400px", + gap: "16px", +}); + +const LoadingText = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 400, + color: canonHeaderTokens.foreground.accent20, +}); diff --git a/src/components/CreateSection.tsx b/src/components/CreateSection.tsx new file mode 100644 index 0000000..73f572a --- /dev/null +++ b/src/components/CreateSection.tsx @@ -0,0 +1,236 @@ +import { Box, Typography, styled } from "@mui/material"; +import { useLocation } from "react-router-dom"; +import { BoxIcon, VectorSquareIcon, FileJsonIcon, StarIcon, HelpCircleIcon } from "~/components/icons"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { useNavigateWithParams } from "~/hooks"; +import { NewActionSection } from "./NewAction"; + +// Action type definitions +type ActionType = "new-action" | "new-action-hub" | "import-json" | "canon-list"; + +interface ActionTypeCardProps { + icon: React.ReactNode; + title: string; + description: string; + onClick?: () => void; +} + +const ActionTypeCard = ({ icon, title, description, onClick }: ActionTypeCardProps) => { + return ( + + {icon} + + {title} + {description} + + + ); +}; + +// Main Create view component +const CreateMain = () => { + const navigateWithParams = useNavigateWithParams(); + + const handleActionClick = (actionType: ActionType) => { + if (actionType === "new-action") { + navigateWithParams("/create/action"); + } else if (actionType === "new-action-hub") { + navigateWithParams("/create/hub"); + } else if (actionType === "canon-list") { + navigateWithParams("/canon-list"); + } + // Other action types will be implemented later + }; + + return ( + + + {/* Page Title */} + + Create transaction + + + + + + {/* Build New Transaction Section */} +
+ BUILD NEW TRANSACTION + + } + title='New Action' + description='Build a single onchain transaction with your chosen target, data, and value...' + onClick={() => handleActionClick("new-action")} + /> + } + title='New Action from Hub' + description='Set up a policy hub that manages related actions. Hubs help organize and verify ...' + onClick={() => handleActionClick("new-action-hub")} + /> + } + title='Import JSON File' + description='Load existing actions or hubs from a JSON file to quickly reuse, review, or edit them.' + onClick={() => handleActionClick("import-json")} + /> + +
+ + {/* Reuse From Canon List Section */} +
+ REUSE FROM YOUR CANON LIST + + } + title='Canon List' + description='Reuse past transactions saved to your Canon List for faster execution.' + onClick={() => handleActionClick("canon-list")} + /> + +
+
+
+ ); +}; + +interface CreateSectionProps { + onQueueCountChange?: () => void; +} + +export const CreateSection = ({ onQueueCountChange }: CreateSectionProps) => { + const location = useLocation(); + + // Determine what to render based on current path + const path = location.pathname; + + // If at /create exactly, show the main create view + if (path === "/create") { + return ; + } + + // If at /create/action or deeper, show the NewActionSection + if (path.startsWith("/create/action")) { + return ; + } + + // If at /create/hub or deeper, show the NewActionSection (hub flow) + if (path.startsWith("/create/hub")) { + return ; + } + + // Default to main create view + return ; +}; + +// Styled components +const Container = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: "32px 120px 64px", + width: "100%", + boxSizing: "border-box", +}); + +const ContentWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + width: "100%", + maxWidth: "576px", +}); + +const TitleSection = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", + padding: "32px 8px 12px", +}); + +const PageTitle = styled(Typography)({ + fontSize: "24px", + fontWeight: 600, + fontStyle: "italic", + lineHeight: "32px", + color: canonHeaderTokens.foreground.accent0, +}); + +const HelpIconWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + cursor: "pointer", + "&:hover": { + opacity: 0.8, + }, +}); + +const Section = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "6px", + width: "100%", +}); + +const SectionLabel = styled(Typography)({ + fontSize: "11px", + fontWeight: 600, + lineHeight: "12px", + color: canonHeaderTokens.foreground.accent30, + padding: "8px", + textTransform: "uppercase", +}); + +const CardsContainer = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "6px", +}); + +// Card components +const CardContainer = styled(Box)({ + display: "flex", + alignItems: "stretch", + borderRadius: "8px", + overflow: "hidden", + cursor: "pointer", + transition: "opacity 0.2s ease", + "&:hover": { + opacity: 0.85, + }, +}); + +const IconArea = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "132px", + backgroundColor: canonHeaderTokens.background.layer1, +}); + +const ContentArea = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "8px", + flex: 1, + padding: "20px 24px", + backgroundColor: canonHeaderTokens.background.layer1, + borderLeft: `1px dashed ${canonHeaderTokens.background.layer0}`, +}); + +const CardTitle = styled(Typography)({ + fontSize: "14px", + fontWeight: 600, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent0, +}); + +const CardDescription = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent20, + maxWidth: "264px", +}); diff --git a/src/components/DeploymentModesPanel.tsx b/src/components/DeploymentModesPanel.tsx new file mode 100644 index 0000000..f37538c --- /dev/null +++ b/src/components/DeploymentModesPanel.tsx @@ -0,0 +1,192 @@ +import { Box, styled } from "@mui/material"; +import { XIcon } from "~/components/icons"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { useNavigateWithParams } from "~/hooks"; + +interface DeploymentModesPanelProps { + isOpen: boolean; + onClose: () => void; + isDetached: boolean; +} + +/** + * Deployment Modes Panel - displays information about attached vs. detached + * deployment modes for Canon Guard. Shows an "Attach Canon Guard" button + * when in detached mode. + */ +export const DeploymentModesPanel = ({ isOpen, onClose, isDetached }: DeploymentModesPanelProps) => { + const navigateWithParams = useNavigateWithParams(); + + const handleAttachGuard = () => { + navigateWithParams("/settings/attach"); + onClose(); + }; + + return ( + <> + + + + {/* Close button */} + + + + + {/* Inner wrapper with padding */} + + {/* Title */} + Deployment modes:{"\n"}Attached vs. Detached + + {/* Introduction */} + You can adopt Canon Guard in two modes. + + {/* Detached explanation */} + + Detached (no Safe guard): You do not call setGuard. Teams use Canon Guard to + queue/approve/execute, but the Safe does not enforce it. If Canon Guard has a bug, you can stop using it + with no impact on the Safe. This is useful for an initial trial while you validate procedures. + + + {/* Attached explanation */} + + Attached (Safe guard set): You call setGuard(CanonGuard). The Safe enforces "only + Canon Guard may execute". This closes bypasses and makes approvals uniformly onchain. Risk: a + misconfiguration (wrong guard address, incompatible Safe version, or broken guard) can block execution + until the guard is changed. + + + {/* Recommended rollout */} + + A recommended rollout would be starting detached for a few weeks, verify builders/hubs and team workflow, + then attach. Keep a rollback prepared (for example, an action from ChangeSafe GuardActionFactory) to reset + the guard if needed. + + + {/* Attach button - only shown when in detached mode */} + {isDetached && Attach Canon Guard} + + + + + ); +}; + +// Drawer Overlay +const DrawerOverlay = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isOpen", +})<{ $isOpen: boolean }>(({ $isOpen }) => ({ + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "rgba(0, 0, 0, 0.5)", + opacity: $isOpen ? 1 : 0, + visibility: $isOpen ? "visible" : "hidden", + transition: "opacity 0.3s ease, visibility 0.3s ease", + zIndex: 1000, +})); + +// Drawer Panel +const DrawerPanel = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isOpen", +})<{ $isOpen: boolean }>(({ $isOpen }) => ({ + position: "fixed", + top: "12px", + right: "12px", + bottom: "12px", + width: "496px", + backgroundColor: canonHeaderTokens.background.layer1, + transform: $isOpen ? "translateX(0)" : "translateX(calc(100% + 24px))", + transition: "transform 0.3s ease", + zIndex: 1001, + display: "flex", + flexDirection: "column", + boxShadow: "-4px 0 24px rgba(0, 0, 0, 0.3)", + borderRadius: "8px", + overflow: "hidden", +})); + +const PanelContent = styled(Box)({ + display: "flex", + flexDirection: "column", + flex: 1, + padding: "24px", + position: "relative", + overflowY: "auto", +}); + +const CloseButton = styled("button")({ + position: "absolute", + top: "16px", + right: "16px", + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "8px", + background: "none", + border: "none", + borderRadius: "100px", + cursor: "pointer", + transition: "background-color 0.2s ease", + "&:hover": { + backgroundColor: canonHeaderTokens.foreground.accent40 + "40", + }, +}); + +// Inner wrapper with padding containing all content +const InnerWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "16px", + padding: "16px", +}); + +const PanelTitle = styled("h2")({ + fontFamily: "Inter, sans-serif", + fontSize: "20px", + fontWeight: 600, + lineHeight: "28px", + color: canonHeaderTokens.foreground.accent0, + margin: 0, + whiteSpace: "pre-line", +}); + +const DescriptionText = styled("p")({ + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent20, + margin: 0, +}); + +const BoldText = styled("span")({ + fontWeight: 700, + color: canonHeaderTokens.foreground.accent10, +}); + +// Attach Button (Primary green style) +const AttachButton = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "100%", + height: "44px", + padding: "8px 20px", + marginTop: "16px", + borderRadius: "100px", + backgroundColor: canonHeaderTokens.brand.green, + border: "none", + cursor: "pointer", + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 600, + letterSpacing: "0.6px", + textTransform: "uppercase", + color: "#FFFFFF", + transition: "opacity 0.2s ease", + "&:hover": { + opacity: 0.9, + }, +}); diff --git a/src/components/DetachedGuardInput.tsx b/src/components/DetachedGuardInput.tsx new file mode 100644 index 0000000..5b6c762 --- /dev/null +++ b/src/components/DetachedGuardInput.tsx @@ -0,0 +1,277 @@ +import { useState, useEffect, useCallback } from "react"; +import { Box, styled, CircularProgress } from "@mui/material"; +import { Address, isAddress } from "viem"; +import { getRpcUrlForChain, getViemChain } from "~/config/chains"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { ClientService, CanonGuardValidationService } from "~/services"; +import { SafeInfo } from "~/types"; +import { HeaderLogo } from "./Header"; +import { CheckIcon } from "./icons"; +import { SafeProfileCard } from "./shared/SafeProfileCard"; +import { + PageContainer, + SetupHeader, + SetupContentArea, + SetupFormWrapper, + SetupSectionTitle, +} from "./shared/StyledComponents"; + +interface DetachedGuardInputProps { + safeInfo: SafeInfo; + onContinue: (guardAddress: Address) => void; + onBack: () => void; + onReset: () => void; +} + +type ValidationState = "idle" | "validating" | "valid" | "invalid"; + +export const DetachedGuardInput = ({ safeInfo, onContinue, onBack, onReset }: DetachedGuardInputProps) => { + const [guardAddress, setGuardAddress] = useState(""); + const [validationState, setValidationState] = useState("idle"); + const [validationError, setValidationError] = useState(null); + + // Debounced validation + const validateGuardAddress = useCallback( + async (address: string) => { + if (!address || !isAddress(address)) { + setValidationState("idle"); + setValidationError(null); + return; + } + + setValidationState("validating"); + setValidationError(null); + + try { + const rpcUrl = getRpcUrlForChain(safeInfo.chainId); + const chain = getViemChain(safeInfo.chainId); + const clientService = new ClientService(rpcUrl, chain); + const validationService = new CanonGuardValidationService(clientService.getClient()); + + const result = await validationService.validateCanonGuard(address as Address); + + if (result.isValid) { + setValidationState("valid"); + setValidationError(null); + } else { + setValidationState("invalid"); + setValidationError(result.error || "Invalid Canon Guard address"); + } + } catch { + setValidationState("invalid"); + setValidationError("Failed to validate address"); + } + }, + [safeInfo.chainId], + ); + + // Debounce the validation + useEffect(() => { + const timeoutId = setTimeout(() => { + validateGuardAddress(guardAddress); + }, 500); + + return () => clearTimeout(timeoutId); + }, [guardAddress, validateGuardAddress]); + + const handleContinue = () => { + if (validationState === "valid" && isAddress(guardAddress)) { + onContinue(guardAddress as Address); + } + }; + + const isValidInput = validationState === "valid"; + const showError = validationState === "invalid" && validationError; + + return ( + + + + + + + + Add New Safe Account + + {/* Safe Profile Card */} + + + {/* Input Card */} + + + + It looks like your Safe doesn't have a Canon Guard attached. Enter an existing Canon Guard address to + continue. + + + + + + Canon Guard Address + + setGuardAddress(e.target.value)} + /> + {validationState === "validating" && ( + + + + )} + {validationState === "valid" && ( + + + + )} + + {showError && {validationError}} + + + + + BACK + + CONTINUE + + + + + + + ); +}; + +// Input Card +const InputCard = styled(Box)({ + display: "flex", + flexDirection: "column", + borderRadius: "8px", + overflow: "hidden", + backgroundColor: canonHeaderTokens.background.layer1, +}); + +const InfoSection = styled(Box)({ + padding: "24px", +}); + +const InfoText = styled("p")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent10, + margin: 0, +}); + +const InputSection = styled(Box)({ + padding: "24px", + borderTop: `1px dashed ${canonHeaderTokens.foreground.accent50}`, +}); + +const InputGroup = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", +}); + +const InputLabel = styled("label")({ + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent0, +}); + +const InputWrapper = styled(Box, { + shouldForwardProp: (prop) => prop !== "$hasError" && prop !== "$isValid", +})<{ $hasError?: boolean; $isValid?: boolean }>(({ $hasError, $isValid }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "16px 20px", + borderRadius: "8px", + border: `1px solid ${$hasError ? "#ef4444" : $isValid ? "#149b3a" : canonHeaderTokens.foreground.accent40}`, + boxShadow: "0px 1px 3px 0px rgba(0,0,0,0.1), 0px 1px 2px -1px rgba(0,0,0,0.1)", +})); + +const StyledInput = styled("input")({ + flex: 1, + fontSize: "16px", + lineHeight: "24px", + color: canonHeaderTokens.foreground.accent0, + backgroundColor: "transparent", + border: "none", + outline: "none", + "&::placeholder": { + color: canonHeaderTokens.foreground.accent30, + }, +}); + +const ValidationIcon = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + marginLeft: "12px", +}); + +const ErrorText = styled("p")({ + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: "#ef4444", + margin: 0, +}); + +const ButtonSection = styled(Box)({ + display: "flex", + gap: "16px", + padding: "24px", + borderTop: `1px dashed ${canonHeaderTokens.foreground.accent50}`, +}); + +const BackButton = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "center", + flex: 1, + height: "36px", + padding: "8px 20px", + borderRadius: "100px", + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, + background: "transparent", + cursor: "pointer", + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 600, + letterSpacing: "0.6px", + textTransform: "uppercase", + color: canonHeaderTokens.foreground.accent10, + "&:hover": { + backgroundColor: `${canonHeaderTokens.foreground.accent40}20`, + }, +}); + +const ContinueButton = styled("button")<{ disabled?: boolean }>(({ disabled }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + flex: 1, + height: "36px", + padding: "8px 20px", + borderRadius: "100px", + border: "none", + backgroundColor: disabled ? canonHeaderTokens.foreground.accent40 : canonHeaderTokens.brand.green, + cursor: disabled ? "not-allowed" : "pointer", + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 600, + letterSpacing: "0.6px", + textTransform: "uppercase", + color: disabled ? canonHeaderTokens.foreground.accent20 : "#ffffff", + "&:hover": { + backgroundColor: disabled ? canonHeaderTokens.foreground.accent40 : "#129035", + }, +})); diff --git a/src/components/DetachedModeBanner.tsx b/src/components/DetachedModeBanner.tsx new file mode 100644 index 0000000..ea19561 --- /dev/null +++ b/src/components/DetachedModeBanner.tsx @@ -0,0 +1,22 @@ +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { WarningBanner, BannerLink } from "./shared/WarningBanner"; + +interface DetachedModeBannerProps { + isActive: boolean; + onLearnMore: () => void; +} + +/** + * Detached Mode Banner - displays a warning banner at the top of the app + * when Canon Guard is operating in detached mode (not attached to the Safe). + * This warns users that their Safe is not protected by Canon Guard. + */ +export const DetachedModeBanner = ({ isActive, onLearnMore }: DetachedModeBannerProps) => { + if (!isActive) return null; + + return ( + + Canon Guard is detached and not securing this Safe. Learn more + + ); +}; diff --git a/src/components/EmergencyModeBanner.tsx b/src/components/EmergencyModeBanner.tsx new file mode 100644 index 0000000..aa5d7fa --- /dev/null +++ b/src/components/EmergencyModeBanner.tsx @@ -0,0 +1,17 @@ +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { WarningBanner } from "./shared/WarningBanner"; + +interface EmergencyModeBannerProps { + isActive: boolean; +} + +/** + * Emergency Mode Banner - displays a red banner at the top of the app + * when emergency mode is active. Should be rendered above the Header + * in all screens. + */ +export const EmergencyModeBanner = ({ isActive }: EmergencyModeBannerProps) => { + if (!isActive) return null; + + return Emergency Mode Activated; +}; diff --git a/src/components/EmergencyModePanel.tsx b/src/components/EmergencyModePanel.tsx new file mode 100644 index 0000000..8dca3f0 --- /dev/null +++ b/src/components/EmergencyModePanel.tsx @@ -0,0 +1,402 @@ +import { useState } from "react"; +import { Box, styled, CircularProgress } from "@mui/material"; +import { useConfig } from "wagmi"; +import { writeContract, waitForTransactionReceipt } from "wagmi/actions"; +import { canonGuardAbi } from "~/abis/canonGuard"; +import { XIcon, ShieldAlertIcon } from "~/components/icons"; +import { CopyableText } from "~/components/shared/CopyButton"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { useCanonGuardConfig, useWallet, useNavigateWithParams } from "~/hooks"; +import { useStateContext } from "~/hooks/useStateContext"; +import type { Address } from "viem"; + +interface EmergencyModePanelProps { + isOpen: boolean; + onClose: () => void; +} + +export const EmergencyModePanel = ({ isOpen, onClose }: EmergencyModePanelProps) => { + const config = useConfig(); + const navigateWithParams = useNavigateWithParams(); + const { guardAddress } = useStateContext(); + const { address: walletAddress } = useWallet(); + const { emergencyMode, emergencyTrigger, emergencyCaller, refetch } = useCanonGuardConfig(); + + const [isActivating, setIsActivating] = useState(false); + + // Permission checks + const isTrigger = + walletAddress && emergencyTrigger ? walletAddress.toLowerCase() === emergencyTrigger.toLowerCase() : false; + const isCaller = + walletAddress && emergencyCaller ? walletAddress.toLowerCase() === emergencyCaller.toLowerCase() : false; + + // Determine the panel state + const isEmergencyOn = emergencyMode === true; + const canActivate = !isEmergencyOn && isTrigger; + const canDeactivate = isEmergencyOn && isCaller; + const hasNoPermissions = !isTrigger && !isCaller; + + const handleActivateEmergencyMode = async () => { + if (!guardAddress || isActivating) return; + + setIsActivating(true); + try { + // Submit the transaction + const hash = await writeContract(config, { + address: guardAddress as Address, + abi: canonGuardAbi, + functionName: "setEmergencyMode", + }); + + // Wait for the transaction to be confirmed + await waitForTransactionReceipt(config, { hash }); + + // Refetch shared config to update all components + await refetch(); + onClose(); + } catch (error) { + console.error("[EmergencyModePanel] Failed to activate emergency mode:", error); + } finally { + setIsActivating(false); + } + }; + + const handleDeactivateEmergencyMode = () => { + // Navigate to the Turn Off Emergency Mode signing flow + navigateWithParams("/create/action/turn-off-emergency"); + onClose(); + }; + + return ( + <> + + + + {/* Close button */} + + + + + {/* Inner wrapper with 16px padding - contains everything */} + + {/* Title */} + Emergency Mode + + {/* Description */} + + Owners can still queue and approve, but only the designated Emergency Caller can execute or cancel while + it's active. + + + {/* Content Section */} + + + + {/* Status Section */} + + + + + + Emergency Mode: {isEmergencyOn ? "ON" : "OFF"} + Limit who can execute transactions + + + + + + {/* Emergency Trigger */} + + Emergency Trigger + {emergencyTrigger ? ( + + {emergencyTrigger} + + ) : ( + - + )} + + + + + {/* Emergency Caller */} + + Emergency Caller + {emergencyCaller ? ( + + {emergencyCaller} + + ) : ( + - + )} + + + + + {/* Action Section - conditional based on permissions */} + {hasNoPermissions && !isEmergencyOn && ( + + You do not have permissions to manage emergency mode. + + )} + + {canActivate && ( + + {isActivating ? : "ACTIVATE EMERGENCY MODE"} + + )} + + {canDeactivate && ( + TURN OFF EMERGENCY MODE + )} + + {isEmergencyOn && !isCaller && ( + + Only the Emergency Caller can turn off emergency mode. + + )} + + + + + + ); +}; + +// Drawer Overlay +const DrawerOverlay = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isOpen", +})<{ $isOpen: boolean }>(({ $isOpen }) => ({ + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "rgba(0, 0, 0, 0.5)", + opacity: $isOpen ? 1 : 0, + visibility: $isOpen ? "visible" : "hidden", + transition: "opacity 0.3s ease, visibility 0.3s ease", + zIndex: 1000, +})); + +// Drawer Panel +const DrawerPanel = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isOpen", +})<{ $isOpen: boolean }>(({ $isOpen }) => ({ + position: "fixed", + top: "12px", + right: "12px", + bottom: "12px", + width: "496px", + backgroundColor: canonHeaderTokens.background.layer1, + transform: $isOpen ? "translateX(0)" : "translateX(calc(100% + 24px))", + transition: "transform 0.3s ease", + zIndex: 1001, + display: "flex", + flexDirection: "column", + boxShadow: "-4px 0 24px rgba(0, 0, 0, 0.3)", + borderRadius: "8px", + overflow: "hidden", +})); + +const PanelContent = styled(Box)({ + display: "flex", + flexDirection: "column", + flex: 1, + padding: "24px", + position: "relative", +}); + +const CloseButton = styled("button")({ + position: "absolute", + top: "16px", + right: "16px", + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "8px", + background: "none", + border: "none", + borderRadius: "100px", + cursor: "pointer", + transition: "background-color 0.2s ease", + "&:hover": { + backgroundColor: canonHeaderTokens.foreground.accent40 + "40", + }, +}); + +// Inner wrapper with 16px padding containing all content +const InnerWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "32px", + padding: "16px", +}); + +const PanelTitle = styled("h2")({ + fontFamily: "Inter, sans-serif", + fontSize: "20px", + fontWeight: 600, + lineHeight: "28px", + color: canonHeaderTokens.foreground.accent0, + margin: 0, +}); + +const PanelDescription = styled("p")({ + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent20, + margin: 0, + maxWidth: "295px", +}); + +const ContentSection = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "20px", +}); + +const Divider = styled(Box)({ + width: "100%", + height: "0.5px", + backgroundColor: canonHeaderTokens.foreground.accent40, +}); + +// Status Section +const StatusSection = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "16px", +}); + +const StatusIconWrapper = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isActive", +})<{ $isActive: boolean }>(({ $isActive }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "56px", + height: "56px", + borderRadius: "100px", + backgroundColor: $isActive ? "#DA2828" : "transparent", + border: $isActive ? "none" : "1px solid rgba(225, 171, 17, 0.1)", +})); + +const StatusInfo = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "6px", +}); + +const StatusTitle = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "16px", + fontWeight: 600, + lineHeight: "24px", + color: canonHeaderTokens.foreground.accent0, +}); + +const StatusSubtitle = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +// Address Section +const AddressSection = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "8px", +}); + +const AddressLabel = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent0, +}); + +const AddressText = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +// Warning Box +const WarningBox = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "20px", + height: "48px", + borderRadius: "8px", + backgroundColor: "rgba(225, 171, 17, 0.1)", +}); + +const WarningText = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: "#E1AB11", + textAlign: "center", +}); + +// Activate Button (Red solid) +const ActivateButton = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "100%", + height: "36px", + padding: "8px 20px", + borderRadius: "100px", + backgroundColor: "#DA2828", + border: "none", + cursor: "pointer", + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 600, + letterSpacing: "0.6px", + textTransform: "uppercase", + color: "#FFFFFF", + transition: "opacity 0.2s ease", + "&:hover:not(:disabled)": { + opacity: 0.9, + }, + "&:disabled": { + opacity: 0.7, + cursor: "not-allowed", + }, +}); + +// Deactivate Button (Red outlined) +const DeactivateButton = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "100%", + height: "36px", + padding: "8px 20px", + borderRadius: "100px", + backgroundColor: "rgba(218, 40, 40, 0.2)", + border: "1px solid rgba(218, 40, 40, 0.5)", + cursor: "pointer", + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 600, + letterSpacing: "0.6px", + textTransform: "uppercase", + color: "#DA2828", + transition: "opacity 0.2s ease", + "&:hover": { + opacity: 0.8, + }, +}); diff --git a/src/components/ErrorState.tsx b/src/components/ErrorState.tsx new file mode 100644 index 0000000..3dd9b40 --- /dev/null +++ b/src/components/ErrorState.tsx @@ -0,0 +1,97 @@ +import { Box, Typography, Button, styled } from "@mui/material"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { HeaderLogo } from "./Header"; +import { + PageContainer, + SetupHeader, + SetupContentArea, + SetupFormWrapper, + SetupSectionTitle, +} from "./shared/StyledComponents"; + +interface ErrorStateProps { + title: string; + message: string; + buttonText?: string; + onChangeSetup: () => void; +} + +export const ErrorState = ({ title, message, buttonText = "Go Back", onChangeSetup }: ErrorStateProps) => { + return ( + + + + + + + + Error + + + + {title} + {message} + + + + {buttonText} + + + + + + ); +}; + +// Error card +const ErrorCard = styled(Box)({ + display: "flex", + flexDirection: "column", + borderRadius: "8px", + overflow: "hidden", + backgroundColor: canonHeaderTokens.background.layer1, +}); + +const CardContent = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "16px", + padding: "24px", +}); + +const ErrorTitle = styled(Typography)({ + fontSize: "18px", + fontWeight: 600, + color: canonHeaderTokens.foreground.accent0, +}); + +const ErrorDescription = styled(Typography)({ + fontSize: "13px", + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent10, +}); + +// Button section +const ButtonSection = styled(Box)({ + display: "flex", + flexDirection: "column", + padding: "24px", + borderTop: `1px dashed ${canonHeaderTokens.background.layer0}`, +}); + +const GoBackButton = styled(Button)({ + width: "100%", + height: "36px", + backgroundColor: canonHeaderTokens.brand.green, + color: "#ffffff", + fontSize: "12px", + fontWeight: 600, + letterSpacing: "0.6px", + textTransform: "uppercase", + borderRadius: "100px", + border: "none", + cursor: "pointer", + "&:hover": { + backgroundColor: "#129035", + }, +}); diff --git a/src/components/GuardSetupWizard.tsx b/src/components/GuardSetupWizard.tsx new file mode 100644 index 0000000..844ca74 --- /dev/null +++ b/src/components/GuardSetupWizard.tsx @@ -0,0 +1,773 @@ +import { useState, useEffect, useCallback, useMemo } from "react"; +import { Box, styled, CircularProgress } from "@mui/material"; +import { Address, isAddress } from "viem"; +import { getRpcUrlForChain, getViemChain } from "~/config/chains"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { CANON_GUARD_FACTORY, MULTI_SEND_CALL_ONLY } from "~/constants/addresses"; +import { ClientService, CanonGuardValidationService } from "~/services"; +import { SafeInfo } from "~/types"; +import { DurationTimeUnit, DURATION_TIME_MULTIPLIERS } from "~/utils/timeUnits"; +import { HeaderLogo } from "./Header"; +import { CheckIcon, InfoIcon } from "./icons"; +import { CopyableText } from "./shared/CopyButton"; +import { DurationInput } from "./shared/DurationInput"; +import { SafeProfileCard } from "./shared/SafeProfileCard"; +import { + PageContainer, + SetupHeader, + SetupContentArea, + SetupFormWrapper, + SetupSectionTitle, +} from "./shared/StyledComponents"; + +interface GuardSetupWizardProps { + safeInfo: SafeInfo; + onBack: () => void; + onReset: () => void; + onComplete: (guardAddress: Address) => void; +} + +interface DurationValue { + amount: string; + unit: DurationTimeUnit; +} + +interface SetupParams { + shortTxExecutionDelay: DurationValue; + longTxExecutionDelay: DurationValue; + txExpiryDelay: DurationValue; + maxApprovalDuration: DurationValue; + emergencyTrigger: string; + emergencyCaller: string; +} + +const DEFAULT_VALUES: SetupParams = { + shortTxExecutionDelay: { amount: "1", unit: "hours" }, // 1 hour + longTxExecutionDelay: { amount: "7", unit: "days" }, // 7 days + txExpiryDelay: { amount: "7", unit: "days" }, // 7 days + maxApprovalDuration: { amount: "4", unit: "months" }, // ~4 months (adjust as needed) + emergencyTrigger: "", + emergencyCaller: "", +}; + +// Duration fields that use DurationInput +const DURATION_FIELDS: (keyof Pick< + SetupParams, + "shortTxExecutionDelay" | "longTxExecutionDelay" | "txExpiryDelay" | "maxApprovalDuration" +>)[] = ["shortTxExecutionDelay", "longTxExecutionDelay", "txExpiryDelay", "maxApprovalDuration"]; + +const PARAM_INFO = { + shortTxExecutionDelay: { + label: "Short Execution Delay", + description: "Time delay for pre-approved transactions", + }, + longTxExecutionDelay: { + label: "Long Execution Delay", + description: "Time delay for non-pre-approved transactions", + }, + txExpiryDelay: { + label: "Transaction Expiry", + description: "How long a transaction remains executable", + }, + maxApprovalDuration: { + label: "Max Approval Duration", + description: "Maximum time an action can be pre-approved", + }, + emergencyTrigger: { + label: "Emergency Trigger", + description: "Address that can activate emergency mode", + placeholder: "0x...", + }, + emergencyCaller: { + label: "Emergency Caller", + description: "Address that can execute during emergency mode", + placeholder: "0x...", + }, +}; + +// Helper to convert duration to seconds +const toSeconds = (duration: DurationValue): number => { + const amount = parseFloat(duration.amount) || 0; + return Math.floor(amount * DURATION_TIME_MULTIPLIERS[duration.unit]); +}; + +type ValidationState = "idle" | "validating" | "valid" | "invalid"; + +export const GuardSetupWizard = ({ safeInfo, onBack, onReset, onComplete }: GuardSetupWizardProps) => { + const [activeStep, setActiveStep] = useState(0); + const [params, setParams] = useState(DEFAULT_VALUES); + const [errors, setErrors] = useState>({}); + + // Guard address input for step 2 + const [deployedGuardAddress, setDeployedGuardAddress] = useState(""); + const [validationState, setValidationState] = useState("idle"); + const [validationError, setValidationError] = useState(null); + + // Computed seconds values for display and validation + const secondsValues = useMemo( + () => ({ + shortTxExecutionDelay: toSeconds(params.shortTxExecutionDelay).toString(), + longTxExecutionDelay: toSeconds(params.longTxExecutionDelay).toString(), + txExpiryDelay: toSeconds(params.txExpiryDelay).toString(), + maxApprovalDuration: toSeconds(params.maxApprovalDuration).toString(), + }), + [params.shortTxExecutionDelay, params.longTxExecutionDelay, params.txExpiryDelay, params.maxApprovalDuration], + ); + + const validateParams = (): boolean => { + const newErrors: Partial> = {}; + + // Validate duration fields + for (const field of DURATION_FIELDS) { + const duration = params[field]; + const amount = parseFloat(duration.amount); + if (!duration.amount || isNaN(amount) || amount <= 0) { + newErrors[field] = "Must be a positive number"; + } + } + + // Check short delay is not greater than long delay + if (toSeconds(params.shortTxExecutionDelay) > toSeconds(params.longTxExecutionDelay)) { + newErrors.shortTxExecutionDelay = "Cannot be greater than long delay"; + } + + if (!params.emergencyTrigger || !isAddress(params.emergencyTrigger)) { + newErrors.emergencyTrigger = "Must be a valid address"; + } + if (!params.emergencyCaller || !isAddress(params.emergencyCaller)) { + newErrors.emergencyCaller = "Must be a valid address"; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleNext = () => { + if (activeStep === 0 && !validateParams()) { + return; + } + setActiveStep(1); + }; + + const handleBack = () => { + if (activeStep === 0) { + onBack(); + } else { + setActiveStep(0); + } + }; + + const handleAddressChange = (field: "emergencyTrigger" | "emergencyCaller", value: string) => { + setParams((prev) => ({ ...prev, [field]: value })); + if (errors[field]) { + setErrors((prev) => ({ ...prev, [field]: undefined })); + } + }; + + const handleDurationChange = ( + field: keyof Pick< + SetupParams, + "shortTxExecutionDelay" | "longTxExecutionDelay" | "txExpiryDelay" | "maxApprovalDuration" + >, + amount: string, + unit?: DurationTimeUnit, + ) => { + setParams((prev) => ({ + ...prev, + [field]: { + amount, + unit: unit ?? prev[field].unit, + }, + })); + if (errors[field]) { + setErrors((prev) => ({ ...prev, [field]: undefined })); + } + }; + + // Validate deployed guard address + const validateGuardAddress = useCallback( + async (address: string) => { + if (!address || !isAddress(address)) { + setValidationState("idle"); + setValidationError(null); + return; + } + + setValidationState("validating"); + setValidationError(null); + + try { + const rpcUrl = getRpcUrlForChain(safeInfo.chainId); + const chain = getViemChain(safeInfo.chainId); + const clientService = new ClientService(rpcUrl, chain); + const validationService = new CanonGuardValidationService(clientService.getClient()); + + const result = await validationService.validateCanonGuard(address as Address); + + if (result.isValid) { + setValidationState("valid"); + setValidationError(null); + } else { + setValidationState("invalid"); + setValidationError(result.error || "Invalid Canon Guard address"); + } + } catch { + setValidationState("invalid"); + setValidationError("Failed to validate address"); + } + }, + [safeInfo.chainId], + ); + + useEffect(() => { + const timeoutId = setTimeout(() => { + validateGuardAddress(deployedGuardAddress); + }, 500); + + return () => clearTimeout(timeoutId); + }, [deployedGuardAddress, validateGuardAddress]); + + const handleContinue = () => { + if (validationState === "valid" && isAddress(deployedGuardAddress)) { + onComplete(deployedGuardAddress as Address); + } + }; + + const isValidGuard = validationState === "valid"; + const showError = validationState === "invalid" && validationError; + + return ( + + + + + + + + + Setup Canon Guard + + {/* Safe Profile Card */} + + + {/* Step Indicator */} + + 0} + onClick={() => activeStep > 0 && setActiveStep(0)} + > + 0}> + 1 + + Configure + + + + + 2 + + Deploy + + + + {/* Step Content */} + {activeStep === 0 ? ( + + + + Configure the parameters for your new Canon Guard deployment. + + + + {/* Duration Fields */} + {DURATION_FIELDS.map((field) => ( + + + {PARAM_INFO[field].label} + {PARAM_INFO[field].description} + + handleDurationChange(field, amount)} + onUnitChange={(unit) => handleDurationChange(field, params[field].amount, unit)} + hasError={!!errors[field]} + placeholder='Enter duration' + /> + {errors[field] && {errors[field]}} + + ))} + + {/* Address Fields */} + + + {PARAM_INFO.emergencyTrigger.label} + {PARAM_INFO.emergencyTrigger.description} + + + handleAddressChange("emergencyTrigger", e.target.value)} + /> + + {errors.emergencyTrigger && {errors.emergencyTrigger}} + + + + + {PARAM_INFO.emergencyCaller.label} + {PARAM_INFO.emergencyCaller.description} + + + handleAddressChange("emergencyCaller", e.target.value)} + /> + + {errors.emergencyCaller && {errors.emergencyCaller}} + + + + + Back + Continue + + + ) : ( + + + + + + + Use your Safe's Transaction Builder to deploy your Canon Guard. Please go to it and use the + parameters below as input. + + + + + Deployment Parameters + + + Target Contract + + {CANON_GUARD_FACTORY} + + + + + Contract Method Selector + createCanonGuard + + + + + Function Arguments + + + _safe + + {safeInfo.address} + + + + + _multiSendCallOnly + + {MULTI_SEND_CALL_ONLY} + + + + + _shortTxExecutionDelay + + {secondsValues.shortTxExecutionDelay} + + + + + _longTxExecutionDelay + + {secondsValues.longTxExecutionDelay} + + + + + _txExpiryDelay + + {secondsValues.txExpiryDelay} + + + + + _maxApprovalDuration + + {secondsValues.maxApprovalDuration} + + + + + _emergencyTrigger + + {params.emergencyTrigger} + + + + + _emergencyCaller + + {params.emergencyCaller} + + + + + + + + + + + After deploying, open your transaction in a block explorer. Navigate to the Logs tab and find the + CanonGuardCreated event — your new Canon Guard address is in Topic 1 (_canonGuard). + + + + Deployed Canon Guard Address + + setDeployedGuardAddress(e.target.value)} + /> + {validationState === "validating" && ( + + + + )} + {validationState === "valid" && ( + + + + )} + + {showError && {validationError}} + + + + + Back + + Continue + + + + )} + + + + + ); +}; + +// Styled Components +const ScrollArea = styled(Box)({ + flex: 1, + overflowY: "auto", +}); + +// Step Indicator +const StepIndicator = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", + padding: "16px 0", +}); + +const StepItem = styled(Box)<{ $active: boolean; $completed: boolean }>(({ $active, $completed }) => ({ + display: "flex", + alignItems: "center", + gap: "8px", + cursor: $completed ? "pointer" : "default", + opacity: $active || $completed ? 1 : 0.5, +})); + +const StepNumber = styled(Box)<{ $active: boolean; $completed: boolean }>(({ $active, $completed }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "24px", + height: "24px", + borderRadius: "50%", + backgroundColor: $active + ? canonHeaderTokens.brand.green + : $completed + ? canonHeaderTokens.brand.green + : canonHeaderTokens.background.layer1, + color: $active || $completed ? "#ffffff" : canonHeaderTokens.foreground.accent20, + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 600, +})); + +const StepLabel = styled("span")<{ $active: boolean }>(({ $active }) => ({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: $active ? 600 : 400, + color: $active ? canonHeaderTokens.foreground.accent0 : canonHeaderTokens.foreground.accent20, +})); + +const StepDivider = styled(Box)({ + flex: 1, + height: "1px", + backgroundColor: canonHeaderTokens.foreground.accent40, +}); + +// Cards +const ConfigCard = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "20px", + padding: "20px", + borderRadius: "8px", + backgroundColor: canonHeaderTokens.background.layer1, +}); + +const DeployCard = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "20px", + padding: "20px", + borderRadius: "8px", + backgroundColor: canonHeaderTokens.background.layer1, +}); + +const InfoSection = styled(Box)({ + display: "flex", + alignItems: "flex-start", + gap: "10px", + padding: "12px", + borderRadius: "6px", + backgroundColor: canonHeaderTokens.background.layer1Variation, +}); + +const InfoText = styled("p")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "18px", + color: canonHeaderTokens.foreground.accent10, + margin: 0, +}); + +const ParamsSection = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "16px", +}); + +const InputGroup = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "6px", +}); + +const InputLabelRow = styled(Box)({ + display: "flex", + justifyContent: "space-between", + alignItems: "baseline", +}); + +const InputLabel = styled("label")({ + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 500, + color: canonHeaderTokens.foreground.accent10, +}); + +const InputHint = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "11px", + fontWeight: 400, + color: canonHeaderTokens.foreground.accent30, +}); + +const InputWrapper = styled(Box)<{ $hasError?: boolean; $isValid?: boolean }>(({ $hasError, $isValid }) => ({ + display: "flex", + alignItems: "center", + height: "40px", + padding: "0 12px", + borderRadius: "6px", + backgroundColor: canonHeaderTokens.background.layer0, + border: `1px solid ${$hasError ? "#DA2828" : $isValid ? "#149b3a" : canonHeaderTokens.foreground.accent50}`, + "&:focus-within": { + borderColor: $hasError ? "#DA2828" : $isValid ? "#149b3a" : canonHeaderTokens.brand.green, + }, +})); + +const StyledInput = styled("input")({ + flex: 1, + height: "100%", + backgroundColor: "transparent", + border: "none", + outline: "none", + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + color: canonHeaderTokens.foreground.accent0, + "&::placeholder": { + color: canonHeaderTokens.foreground.accent30, + }, +}); + +const ValidationIcon = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + marginLeft: "8px", +}); + +const ErrorText = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "11px", + fontWeight: 400, + color: "#DA2828", +}); + +const InputSection = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + marginTop: "12px", + paddingTop: "20px", + borderTop: `1px solid ${canonHeaderTokens.foreground.accent50}`, +}); + +// Deploy Section +const DeploySection = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "8px", +}); + +const DeploySectionTitle = styled("h3")({ + fontFamily: "Inter, sans-serif", + fontSize: "11px", + fontWeight: 600, + letterSpacing: "0.5px", + textTransform: "uppercase", + color: canonHeaderTokens.foreground.accent30, + margin: "8px 0 4px 0", +}); + +const ParamRow = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "8px 0", +}); + +const ParamLabel = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 400, + color: canonHeaderTokens.foreground.accent20, +}); + +const ParamCode = styled("code")({ + fontFamily: "monospace", + fontSize: "12px", + fontWeight: 400, + color: canonHeaderTokens.foreground.accent0, +}); + +const ParamsDivider = styled(Box)({ + height: "1px", + backgroundColor: canonHeaderTokens.foreground.accent50, + margin: "8px 0", +}); + +// Buttons +const ButtonSection = styled(Box)({ + display: "flex", + justifyContent: "space-between", + paddingTop: "12px", +}); + +const BackButton = styled("button")({ + height: "36px", + padding: "0 20px", + borderRadius: "100px", + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, + backgroundColor: "transparent", + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 600, + letterSpacing: "0.6px", + textTransform: "uppercase", + color: canonHeaderTokens.foreground.accent10, + cursor: "pointer", + "&:hover": { + backgroundColor: `${canonHeaderTokens.foreground.accent40}20`, + }, +}); + +const ContinueButton = styled("button")<{ disabled?: boolean }>(({ disabled }) => ({ + height: "36px", + padding: "0 24px", + borderRadius: "100px", + border: "none", + backgroundColor: disabled ? canonHeaderTokens.foreground.accent40 : canonHeaderTokens.brand.green, + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 600, + letterSpacing: "0.6px", + textTransform: "uppercase", + color: "#ffffff", + cursor: disabled ? "not-allowed" : "pointer", + opacity: disabled ? 0.5 : 1, + "&:hover": { + backgroundColor: disabled ? canonHeaderTokens.foreground.accent40 : "#129035", + }, +})); diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx new file mode 100644 index 0000000..4ad5df7 --- /dev/null +++ b/src/components/Header/Header.tsx @@ -0,0 +1,147 @@ +import { Box, styled } from "@mui/material"; +import { useLocation } from "react-router-dom"; +import { Chain } from "viem/chains"; +import { WalletConnectIcon } from "~/components/icons/WalletConnectIcon"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { useNavigateWithParams } from "~/hooks"; +import { useStateContext } from "~/hooks/useStateContext"; +import { useWalletConnect } from "~/providers/WalletConnectProvider"; +import { HeaderLogo } from "./HeaderLogo"; +import { HeaderNav } from "./HeaderNav"; +import { HeaderSafeDropdown } from "./HeaderSafeDropdown"; +import { HeaderWalletDropdown } from "./HeaderWalletDropdown"; +import type { Address } from "viem"; + +interface HeaderProps { + safeAddress: string; + chain: Chain; + queueCount?: number; + onClearConfig: () => void; +} + +export const Header = ({ safeAddress, chain, queueCount = 0, onClearConfig }: HeaderProps) => { + const location = useLocation(); + const navigateWithParams = useNavigateWithParams(); + const { guardAddress } = useStateContext(); + const { isInitialized, openModal, sessions } = useWalletConnect(); + const isCreateActive = location.pathname.startsWith("/create"); + const hasActiveSessions = sessions.length > 0; + + return ( + + {/* Left section: Logo + Nav (flex: 1 to fill space) */} + + + + + + + + + + {/* WalletConnect button */} + {isInitialized && ( + + + + )} + + {/* Right section: CREATE | Safe | Wallet with dividers */} + navigateWithParams("/create")}> + CREATE + + + + + ); +}; + +// Main container - dark background shows through gaps as dividers +const HeaderContainer = styled(Box)({ + display: "flex", + alignItems: "center", + height: "72px", + backgroundColor: canonHeaderTokens.background.layer0, + width: "100%", + gap: "1px", +}); + +// Left section groups logo and nav together +const HeaderLeftSection = styled(Box)({ + display: "flex", + alignItems: "center", + height: "100%", + flex: 1, + backgroundColor: canonHeaderTokens.background.layer1, +}); + +const LogoSection = styled(Box)({ + display: "flex", + alignItems: "center", + height: "100%", +}); + +const NavSection = styled(Box)({ + display: "flex", + alignItems: "center", + height: "100%", +}); + +// WalletConnect button +const WalletConnectButton = styled("button")<{ $hasActiveSessions?: boolean }>(({ $hasActiveSessions }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + height: "100%", + padding: "0 24px", + background: canonHeaderTokens.background.layer1, + border: "none", + cursor: "pointer", + position: "relative", + "&:hover": { + opacity: 0.8, + }, + // Active indicator dot + "&::after": $hasActiveSessions + ? { + content: '""', + position: "absolute", + top: "50%", + right: "8px", + transform: "translateY(-50%)", + width: "6px", + height: "6px", + borderRadius: "50%", + backgroundColor: canonHeaderTokens.brand.green, + } + : {}, +})); + +// CREATE button with layer1 background for divider effect +const CreateButton = styled("button")<{ $isActive?: boolean }>(() => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + height: "100%", + padding: "0 32px", + background: canonHeaderTokens.background.layer1, + border: "none", + cursor: "pointer", + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 600, + letterSpacing: "0.6px", + textTransform: "uppercase", + color: canonHeaderTokens.brand.green, + "&:hover": { + opacity: 0.8, + }, +})); diff --git a/src/components/Header/HeaderLogo.tsx b/src/components/Header/HeaderLogo.tsx new file mode 100644 index 0000000..fbaee19 --- /dev/null +++ b/src/components/Header/HeaderLogo.tsx @@ -0,0 +1,41 @@ +import { styled } from "@mui/material"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; + +interface HeaderLogoProps { + onClick?: () => void; +} + +export const HeaderLogo = ({ onClick }: HeaderLogoProps) => { + return ( + + CANON + GUARD + + ); +}; + +const LogoButton = styled("button")({ + display: "flex", + alignItems: "center", + gap: "4px", + padding: "0 28px", + fontSize: "20px", + color: canonHeaderTokens.foreground.accent0, + background: "transparent", + border: "none", + cursor: "pointer", + height: "100%", + "&:hover": { + opacity: 0.8, + }, +}); + +const LogoBold = styled("span")({ + fontWeight: 700, + fontStyle: "italic", +}); + +const LogoRegular = styled("span")({ + fontWeight: 400, + fontStyle: "italic", +}); diff --git a/src/components/Header/HeaderNav.tsx b/src/components/Header/HeaderNav.tsx new file mode 100644 index 0000000..7bc8d45 --- /dev/null +++ b/src/components/Header/HeaderNav.tsx @@ -0,0 +1,64 @@ +import { Box, styled } from "@mui/material"; +import { useLocation } from "react-router-dom"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { useNavigateWithParams } from "~/hooks"; + +interface HeaderNavProps { + queueCount?: number; +} + +export const HeaderNav = ({ queueCount = 0 }: HeaderNavProps) => { + const location = useLocation(); + const navigateWithParams = useNavigateWithParams(); + + // Determine active route based on current path + const isQueueActive = location.pathname === "/queue" || location.pathname === "/"; + const isCanonListActive = location.pathname === "/canon-list"; + + return ( + + navigateWithParams("/queue")}> + QUEUE + {queueCount > 0 && {queueCount}} + + navigateWithParams("/canon-list")}> + CANON LIST + + + ); +}; + +const NavContainer = styled(Box)({ + display: "flex", + alignItems: "center", + height: "100%", +}); + +const NavItem = styled("button")<{ $isActive?: boolean }>(({ $isActive }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + height: "100%", + padding: "0 20px", + background: "transparent", + border: "none", + borderBottom: $isActive ? `1px solid ${canonHeaderTokens.brand.green}` : "1px solid transparent", + cursor: "pointer", + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 600, + letterSpacing: "0.6px", + textTransform: "uppercase", + color: canonHeaderTokens.foreground.accent0, + position: "relative", + "&:hover": { + opacity: 0.8, + }, +})); + +const QueueBadge = styled("span")({ + marginLeft: "6px", + fontSize: "10px", + fontWeight: 600, + color: canonHeaderTokens.brand.green, +}); diff --git a/src/components/Header/HeaderSafeDropdown.tsx b/src/components/Header/HeaderSafeDropdown.tsx new file mode 100644 index 0000000..f5590d1 --- /dev/null +++ b/src/components/Header/HeaderSafeDropdown.tsx @@ -0,0 +1,359 @@ +import { useState, useEffect } from "react"; +import { KeyboardArrowDown } from "@mui/icons-material"; +import { Box, styled } from "@mui/material"; +import { Chain } from "viem/chains"; +import { EmergencyModePanel } from "~/components/EmergencyModePanel"; +import { ChevronRightIcon, GripIcon, ShieldCheckIcon } from "~/components/icons"; +import { CopyableText } from "~/components/shared/CopyButton"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { useNavigateWithParams, useCanonGuardConfig } from "~/hooks"; +import { truncateAddress } from "~/utils"; +import type { Address } from "viem"; + +interface HeaderSafeDropdownProps { + safeAddress: string; + chain: Chain; + guardAddress: Address | null; + onClearConfig: () => void; +} + +export const HeaderSafeDropdown = ({ safeAddress, chain }: HeaderSafeDropdownProps) => { + const navigateWithParams = useNavigateWithParams(); + const { emergencyMode } = useCanonGuardConfig(); + const [menuOpen, setMenuOpen] = useState(false); + const [emergencyPanelOpen, setEmergencyPanelOpen] = useState(false); + + // Close on escape key + useEffect(() => { + const handleEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setMenuOpen(false); + } + }; + + if (menuOpen) { + document.addEventListener("keydown", handleEscape); + } + + return () => { + document.removeEventListener("keydown", handleEscape); + }; + }, [menuOpen]); + + const handleButtonClick = () => { + setMenuOpen(!menuOpen); + }; + + const handleCloseMenu = () => { + setMenuOpen(false); + }; + + const handleSettingsClick = () => { + navigateWithParams("/settings"); + handleCloseMenu(); + }; + + const handleEmergencyModeClick = () => { + handleCloseMenu(); + setEmergencyPanelOpen(true); + }; + + const handleManageSafesClick = () => { + // TODO: Implement manage safe accounts + handleCloseMenu(); + }; + + return ( + + + + + Safe + {truncateAddress(safeAddress)} + + + {chain.name} + + + + {/* Backdrop to close menu */} + {menuOpen && } + + {/* Custom dropdown menu */} + + {/* Safe Profile Section */} + + + + + + + + Safe + + {truncateAddress(safeAddress)} + + + + {chain.name} + + + + + + + + {/* Settings */} + + + + Settings + + + + + + {/* Emergency Mode */} + + + + Emergency Mode + + + + {emergencyMode === true ? "ON" : "OFF"} + + + + + + {/* Manage Safe Accounts */} + + + + Manage Safe Accounts (3) + + + + + {/* Emergency Mode Panel */} + setEmergencyPanelOpen(false)} /> + + ); +}; + +// Wrapper ensures proper gap/divider behavior in flex parent +const DropdownWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + height: "100%", + background: canonHeaderTokens.background.layer1, + position: "relative", +}); + +const DropdownButton = styled("button")({ + display: "flex", + alignItems: "center", + height: "100%", + padding: "0 12px 0 20px", + background: "transparent", + border: "none", + cursor: "pointer", + "&:hover": { + opacity: 0.9, + }, +}); + +const DropdownContent = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "6px", + alignItems: "flex-start", + justifyContent: "center", +}); + +const DropdownRow = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "6px", +}); + +const DropdownLabel = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + color: canonHeaderTokens.foreground.accent10, +}); + +const DropdownValue = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + color: canonHeaderTokens.foreground.accent10, +}); + +const ChevronIcon = styled(KeyboardArrowDown)({ + fontSize: "14px", + color: canonHeaderTokens.foreground.accent10, +}); + +const NetworkLabel = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + color: canonHeaderTokens.foreground.accent20, +}); + +// Menu backdrop +const MenuBackdrop = styled("div")({ + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + zIndex: 999, +}); + +// Safe menu dropdown +const SafeMenu = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isOpen", +})<{ $isOpen: boolean }>(({ $isOpen }) => ({ + position: "absolute", + top: "78px", + right: 0, + width: "280px", + borderRadius: "12px", + border: `0.5px solid ${canonHeaderTokens.foreground.accent40}`, + backgroundColor: canonHeaderTokens.background.layer1, + boxShadow: "0px 20px 25px -5px rgba(0,0,0,0.1), 0px 8px 10px -6px rgba(0,0,0,0.1)", + overflow: "hidden", + zIndex: 1000, + opacity: $isOpen ? 1 : 0, + visibility: $isOpen ? "visible" : "hidden", + transform: $isOpen ? "translateY(0)" : "translateY(-8px)", + transition: "opacity 0.2s ease, transform 0.2s ease, visibility 0.2s ease", +})); + +// Safe Profile Section +const SafeProfileSection = styled(Box)({ + padding: "16px 20px", +}); + +const SafeProfileContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "16px", +}); + +const SafeIconWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "48px", + height: "48px", + padding: "16px", + borderRadius: "12px", + backgroundColor: canonHeaderTokens.background.layer1Variation, + border: `0.5px solid ${canonHeaderTokens.background.layer1Variation}`, +}); + +const SafeProfileInfo = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "6px", +}); + +const SafeAddressRow = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "6px", +}); + +const SafeLabel = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent0, +}); + +const AddressText = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent0, +}); + +const ChainRow = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const ChainName = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const MenuDivider = styled("div")({ + width: "100%", + height: "0.5px", + backgroundColor: canonHeaderTokens.foreground.accent40, +}); + +// Menu Item +const MenuItem = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + width: "100%", + padding: "16px", + background: "transparent", + border: "none", + cursor: "pointer", + "&:hover": { + backgroundColor: canonHeaderTokens.background.layer0, + }, +}); + +const MenuItemLeft = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "16px", +}); + +const MenuItemLabel = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent10, +}); + +// Emergency Mode Status +const EmergencyStatusIndicator = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const StatusDot = styled("div", { + shouldForwardProp: (prop) => prop !== "$isActive", +})<{ $isActive: boolean }>(({ $isActive }) => ({ + width: "7px", + height: "7px", + borderRadius: "50%", + backgroundColor: $isActive ? canonHeaderTokens.status.red : canonHeaderTokens.foreground.accent20, +})); + +const StatusText = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); diff --git a/src/components/Header/HeaderWalletDropdown.tsx b/src/components/Header/HeaderWalletDropdown.tsx new file mode 100644 index 0000000..a12eeba --- /dev/null +++ b/src/components/Header/HeaderWalletDropdown.tsx @@ -0,0 +1,276 @@ +import { useState } from "react"; +import { KeyboardArrowDown } from "@mui/icons-material"; +import { Box, styled, CircularProgress } from "@mui/material"; +import { LogOutIcon, WalletIcon } from "~/components/icons"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { useWallet } from "~/hooks"; +import { truncateAddress } from "~/utils"; + +export const HeaderWalletDropdown = () => { + const { address, isConnected, isConnecting, connect, disconnect } = useWallet(); + const [menuOpen, setMenuOpen] = useState(false); + + const handleButtonClick = () => { + if (isConnected) { + setMenuOpen(!menuOpen); + } else { + connect(); + } + }; + + const handleDisconnect = () => { + disconnect(); + setMenuOpen(false); + }; + + const handleCloseMenu = () => { + setMenuOpen(false); + }; + + // Disconnected state - show CONNECT button + if (!isConnected) { + return ( + + + {isConnecting ? ( + + ) : ( + "CONNECT" + )} + + + ); + } + + // Connected state - show address with dropdown + return ( + + + + + {truncateAddress(address || "")} + + + + Wallet + + + + + + {/* Backdrop to close menu */} + {menuOpen && } + + {/* Custom dropdown menu */} + + {/* Info section */} + + You can disconnect and switch to a different wallet. + + + + + + {/* Divider */} + + + {/* Disconnect button */} + + + Disconnect Wallet + + + + ); +}; + +// Connected state styles - wrapper for proper gap behavior +const WalletWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + height: "100%", + background: canonHeaderTokens.background.layer1, +}); + +const DropdownButton = styled("button")({ + display: "flex", + alignItems: "center", + height: "100%", + padding: "0 12px 0 20px", + background: "transparent", + border: "none", + cursor: "pointer", + "&:hover": { + opacity: 0.9, + }, +}); + +const DropdownContent = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "6px", + alignItems: "flex-start", + justifyContent: "center", + minWidth: "100px", +}); + +const DropdownRow = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + width: "100%", + gap: "6px", +}); + +const DropdownValue = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + color: canonHeaderTokens.foreground.accent10, +}); + +const ChevronIcon = styled(KeyboardArrowDown)({ + fontSize: "14px", + color: canonHeaderTokens.foreground.accent10, +}); + +const StatusRow = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "6px", +}); + +const StatusLabel = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + color: canonHeaderTokens.foreground.accent20, +}); + +const StatusDot = styled("div")({ + width: "6px", + height: "6px", + borderRadius: "50%", + backgroundColor: canonHeaderTokens.brand.green, +}); + +// Disconnected state styles - CONNECT button +const ConnectButtonWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + height: "100%", + padding: "0 16px 0 20px", + background: canonHeaderTokens.background.layer1, +}); + +const ConnectButton = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "center", + height: "36px", + padding: "8px 20px", + minWidth: "140px", + borderRadius: "100px", + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, + background: "transparent", + cursor: "pointer", + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 600, + letterSpacing: "0.6px", + textTransform: "uppercase", + color: canonHeaderTokens.foreground.accent10, + transition: "all 0.2s ease", + "&:hover": { + backgroundColor: `${canonHeaderTokens.foreground.accent40}20`, + }, + "&:disabled": { + cursor: "wait", + opacity: 0.7, + }, +}); + +// Menu backdrop +const MenuBackdrop = styled("div")({ + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + zIndex: 999, +}); + +// Wallet menu dropdown +const WalletMenu = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isOpen", +})<{ $isOpen: boolean }>(({ $isOpen }) => ({ + position: "absolute", + top: "78px", + right: "12px", + width: "280px", + borderRadius: "12px", + border: `0.5px solid ${canonHeaderTokens.foreground.accent40}`, + backgroundColor: canonHeaderTokens.background.layer1, + boxShadow: "0px 20px 25px -5px rgba(0,0,0,0.1), 0px 8px 10px -6px rgba(0,0,0,0.1)", + overflow: "hidden", + zIndex: 1000, + opacity: $isOpen ? 1 : 0, + visibility: $isOpen ? "visible" : "hidden", + transform: $isOpen ? "translateY(0)" : "translateY(-8px)", + transition: "opacity 0.2s ease, transform 0.2s ease, visibility 0.2s ease", +})); + +const MenuInfoSection = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "16px 20px", +}); + +const MenuInfoText = styled("p")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent20, + width: "177px", + margin: 0, +}); + +const MenuIconWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "8px", + borderRadius: "8px", + backgroundColor: canonHeaderTokens.background.layer2, +}); + +const MenuDivider = styled("div")({ + width: "100%", + height: "0.5px", + backgroundColor: canonHeaderTokens.foreground.accent40, +}); + +const MenuDisconnectButton = styled("button")({ + display: "flex", + alignItems: "center", + gap: "16px", + width: "100%", + padding: "16px", + background: "transparent", + border: "none", + cursor: "pointer", + "&:hover": { + backgroundColor: canonHeaderTokens.background.layer0, + }, +}); + +const MenuDisconnectText = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent10, +}); diff --git a/src/components/Header/index.ts b/src/components/Header/index.ts new file mode 100644 index 0000000..db930cc --- /dev/null +++ b/src/components/Header/index.ts @@ -0,0 +1,5 @@ +export { Header } from "./Header"; +export { HeaderLogo } from "./HeaderLogo"; +export { HeaderNav } from "./HeaderNav"; +export { HeaderSafeDropdown } from "./HeaderSafeDropdown"; +export { HeaderWalletDropdown } from "./HeaderWalletDropdown"; diff --git a/src/components/NewAction/NewActionSection.tsx b/src/components/NewAction/NewActionSection.tsx new file mode 100644 index 0000000..1c0d2bc --- /dev/null +++ b/src/components/NewAction/NewActionSection.tsx @@ -0,0 +1,1564 @@ +import { useState, useCallback, useEffect, useMemo, useRef } from "react"; +import { Box, Typography, styled } from "@mui/material"; +import { useLocation, useParams } from "react-router-dom"; +import { Address, Hex, encodeFunctionData } from "viem"; +import { canonGuardAbi, safeAbi } from "~/abis/canonGuard"; +import { getRpcUrlForChain, getViemChain } from "~/config/chains"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { useNavigateWithParams, useTransactionExecutor } from "~/hooks"; +import { useStateContext } from "~/hooks/useStateContext"; +import type { ParsedWalletConnectTransaction } from "~/providers/WalletConnectProvider"; +import { ClientService } from "~/services/clientService"; +import { QueueService, type QueueItem } from "~/services/queueService"; +import { RegistryService } from "~/services/registryService"; +import { + buildTransactionSteps, + buildArbitraryActionSteps, + buildClaimAllowanceSteps, + buildCappedTransferHubSteps, + buildDeployHubChildSteps, + type TransactionStep, +} from "~/services/transactionBuilderService"; +import { CappedTokenTransfersHubInfo } from "~/types/canon-guard"; +import { ActionFactoryType } from "~/types/canon-guard"; +import { FACTORY_DISPLAY_NAMES, HUB_DISPLAY_NAMES } from "~/utils/factoryDisplay"; +import { + SelectFactoryStep, + TransferFormStep, + ArbitraryActionFormStep, + ClaimAllowanceFormStep, + ReviewDeployStep, + SigningFlowStep, + SelectHubTypeStep, + CappedTransferHubFormStep, + HubReviewStep, + DeployHubChildFormStep, + DeployHubChildReviewStep, +} from "./steps"; +import type { + TransferFormData, + ArbitraryActionFormData, + ClaimAllowanceFormData, + FactoryType, + HubType, + CappedTransferHubFormData, + HubChildFormData, +} from "./steps"; + +// Pre-deployed UnsetEmergencyModeAction contract +const UNSET_EMERGENCY_MODE_ACTION: Address = "0x68e54338e31C7A8B7c46a2BB8Fd73f3a0606A506"; + +const INITIAL_TRANSFER_FORM_DATA: TransferFormData = { + title: "", + transfers: [{ tokenAddress: "", recipientAddress: "", amount: "" }], +}; + +const INITIAL_ARBITRARY_ACTION_FORM_DATA: ArbitraryActionFormData = { + title: "", + actions: [{ target: "", signature: "", data: "", value: "" }], +}; + +const INITIAL_CLAIM_ALLOWANCE_FORM_DATA: ClaimAllowanceFormData = { + title: "", + token: "", + tokenOwner: "", + tokenRecipient: "", +}; + +const INITIAL_CAPPED_TRANSFER_HUB_FORM_DATA: CappedTransferHubFormData = { + title: "", + recipientAddress: "", + epochLength: "", + epochUnit: "months", + tokens: [{ address: "", amount: "" }], +}; + +const INITIAL_HUB_CHILD_FORM_DATA: HubChildFormData = { + title: "", + token: "", + amount: "", +}; + +interface NewActionSectionProps { + onQueueCountChange?: () => void; +} + +export const NewActionSection = ({ onQueueCountChange }: NewActionSectionProps) => { + const location = useLocation(); + const { hubAddress: hubAddressParam } = useParams<{ hubAddress: string }>(); + const navigateWithParams = useNavigateWithParams(); + const { safeAddress, guardAddress, chainId } = useStateContext(); + + // Transaction executor hook for real blockchain transactions + const { + executeDeployTransfer, + executeDeployArbitraryAction, + executeDeployClaimAllowance, + executeDeployHubChild, + executeDeployCappedTransferHub, + executeRecordToRegistry, + executeQueueTransaction, + executeSignTransaction, + executeDeployPreApproval, + reset: resetExecutor, + } = useTransactionExecutor(); + + // Local state for factory selection and form data + const [selectedFactory, setSelectedFactory] = useState(null); + const [transferFormData, setTransferFormData] = useState(INITIAL_TRANSFER_FORM_DATA); + const [arbitraryActionFormData, setArbitraryActionFormData] = useState( + INITIAL_ARBITRARY_ACTION_FORM_DATA, + ); + const [claimAllowanceFormData, setClaimAllowanceFormData] = useState( + INITIAL_CLAIM_ALLOWANCE_FORM_DATA, + ); + const [isReviewMode, setIsReviewMode] = useState(false); + + // Hub-specific state + const [selectedHub, setSelectedHub] = useState(null); + const [cappedTransferHubFormData, setCappedTransferHubFormData] = useState( + INITIAL_CAPPED_TRANSFER_HUB_FORM_DATA, + ); + const [isHubReviewMode, setIsHubReviewMode] = useState(false); + + // Hub child deployment state + const [hubChildFormData, setHubChildFormData] = useState(INITIAL_HUB_CHILD_FORM_DATA); + const [isHubChildReviewMode, setIsHubChildReviewMode] = useState(false); + const [hubInfo, setHubInfo] = useState(null); + const [isLoadingHubInfo, setIsLoadingHubInfo] = useState(false); + const [hubLabel, setHubLabel] = useState(""); + const [hubIsFastPath, setHubIsFastPath] = useState(false); + + // Signing flow state + const [isSigningMode, setIsSigningMode] = useState(false); + const [transactionSteps, setTransactionSteps] = useState([]); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [reviewCheckboxState, setReviewCheckboxState] = useState({ + proposeTransaction: false, + proposePreApproval: false, + approvalDurationSeconds: undefined as bigint | undefined, + }); + + // Store the deployed action builder address for subsequent steps + const [deployedActionAddress, setDeployedActionAddress] = useState
(null); + + // Store the deployed pre-approval action address for queue/sign steps + const [preApprovalAddress, setPreApprovalAddress] = useState
(null); + + // Track if turn-off-emergency flow has been initialized + const [emergencyFlowInitialized, setEmergencyFlowInitialized] = useState(false); + + // Track if WalletConnect transaction has been processed + const walletConnectProcessed = useRef(false); + + // Nonce selection state + const [currentSafeNonce, setCurrentSafeNonce] = useState(0); + const [queueItems, setQueueItems] = useState([]); + const [nonceDataLoaded, setNonceDataLoaded] = useState(false); + + // Create service instances + const clientService = useMemo(() => { + const rpcUrl = getRpcUrlForChain(chainId as number); + const chain = getViemChain(chainId as number); + return new ClientService(rpcUrl, chain); + }, [chainId]); + const queueService = useMemo(() => new QueueService(clientService), [clientService]); + + const path = location.pathname; + + // Reset all state + const resetFlow = () => { + setSelectedFactory(null); + setTransferFormData(INITIAL_TRANSFER_FORM_DATA); + setArbitraryActionFormData(INITIAL_ARBITRARY_ACTION_FORM_DATA); + setClaimAllowanceFormData(INITIAL_CLAIM_ALLOWANCE_FORM_DATA); + setIsReviewMode(false); + setIsSigningMode(false); + setTransactionSteps([]); + setCurrentStepIndex(0); + setReviewCheckboxState({ + proposeTransaction: false, + proposePreApproval: false, + approvalDurationSeconds: undefined, + }); + setDeployedActionAddress(null); + setPreApprovalAddress(null); + setEmergencyFlowInitialized(false); + setNonceDataLoaded(false); + setCurrentSafeNonce(0); + setQueueItems([]); + // Reset hub state + setSelectedHub(null); + setCappedTransferHubFormData(INITIAL_CAPPED_TRANSFER_HUB_FORM_DATA); + setIsHubReviewMode(false); + // Reset hub child state + setHubChildFormData(INITIAL_HUB_CHILD_FORM_DATA); + setIsHubChildReviewMode(false); + setHubInfo(null); + setIsLoadingHubInfo(false); + setHubLabel(""); + setHubIsFastPath(false); + resetExecutor(); + }; + + // Fetch nonce data when entering signing mode + useEffect(() => { + const fetchNonceData = async () => { + if (!isSigningMode || !guardAddress || !safeAddress || nonceDataLoaded) { + return; + } + + try { + console.log("[NewActionSection] Fetching nonce data for signing flow"); + + // Fetch current nonce and queue items in parallel + const [nonce, items] = await Promise.all([ + queueService.getCurrentSafeNonce(guardAddress as Address), + queueService.getQueueItems(guardAddress as Address, safeAddress as Address), + ]); + + console.log("[NewActionSection] Nonce data loaded:", { nonce, queueItemsCount: items.length }); + setCurrentSafeNonce(nonce); + setQueueItems(items); + setNonceDataLoaded(true); + } catch (error) { + console.error("[NewActionSection] Failed to fetch nonce data:", error); + // Set defaults on error to avoid blocking + setCurrentSafeNonce(0); + setQueueItems([]); + setNonceDataLoaded(true); + } + }; + + fetchNonceData(); + }, [isSigningMode, guardAddress, safeAddress, queueService, nonceDataLoaded]); + + // Build steps for Turn Off Emergency Mode (2 steps: Queue + Sign) + const buildTurnOffEmergencySteps = useCallback((): TransactionStep[] => { + const steps: TransactionStep[] = []; + + // Step 1: Queue the UnsetEmergencyModeAction + const queueData = encodeFunctionData({ + abi: canonGuardAbi, + functionName: "queueTransaction", + args: [UNSET_EMERGENCY_MODE_ACTION], + }); + + steps.push({ + id: "queue-emergency-off", + title: "Queue Transaction", + description: "Queue the Turn Off Emergency Mode action in Canon Guard", + status: "pending", + to: guardAddress as Address, + data: queueData, + }); + + // Step 2: Sign Transaction + const signData = encodeFunctionData({ + abi: safeAbi, + functionName: "approveHash", + args: ["0x0000000000000000000000000000000000000000000000000000000000000000" as Hex], + }); + + steps.push({ + id: "sign-emergency-off", + title: "Sign Transaction", + description: "Approve the transaction hash in the Safe", + status: "pending", + to: safeAddress as Address, + data: signData, + }); + + return steps; + }, [guardAddress, safeAddress]); + + // Fetch hub info when on hub-child path + useEffect(() => { + const isHubChildPath = path.startsWith("/create/hub-child/"); + if (isHubChildPath && hubAddressParam && !hubInfo && !isLoadingHubInfo) { + const fetchHubInfo = async () => { + setIsLoadingHubInfo(true); + try { + const registryService = new RegistryService(clientService); + const info = await registryService.getHubConfiguration(hubAddressParam as Address); + setHubInfo(info); + // Get hub label and fast path status from location state if available + const state = location.state as { hubLabel?: string; isFastPath?: boolean } | null; + if (state?.hubLabel) { + setHubLabel(state.hubLabel); + } + if (state?.isFastPath !== undefined) { + setHubIsFastPath(state.isFastPath); + } + } catch (error) { + console.error("[NewActionSection] Failed to fetch hub info:", error); + } finally { + setIsLoadingHubInfo(false); + } + }; + fetchHubInfo(); + } + }, [path, hubAddressParam, hubInfo, isLoadingHubInfo, clientService, location.state]); + + // Initialize turn-off-emergency flow when navigating directly to the path + useEffect(() => { + const isTurnOffPath = + path === "/create/action/turn-off-emergency" || path.startsWith("/create/action/turn-off-emergency"); + const shouldInit = isTurnOffPath && !emergencyFlowInitialized && guardAddress && safeAddress; + + if (shouldInit) { + const steps = buildTurnOffEmergencySteps(); + setTransactionSteps(steps); + setCurrentStepIndex(0); + setIsSigningMode(true); + setEmergencyFlowInitialized(true); + } + // Reset initialization flag when leaving the path + if (!isTurnOffPath && emergencyFlowInitialized) { + setEmergencyFlowInitialized(false); + } + }, [path, emergencyFlowInitialized, guardAddress, safeAddress, buildTurnOffEmergencySteps]); + + // Handle WalletConnect transaction - pre-fill form and go directly to signing + useEffect(() => { + const state = location.state as { walletConnectTx?: ParsedWalletConnectTransaction } | null; + const wcTx = state?.walletConnectTx; + + // Only process once and only on arbitrary-action path + if (!wcTx || walletConnectProcessed.current || path !== "/create/action/arbitrary-action") { + return; + } + + // Only process if we have the required addresses + if (!safeAddress || !guardAddress) { + return; + } + + walletConnectProcessed.current = true; + + // Parse value - WalletConnect sends hex values like "0x0" + let parsedValue = ""; + if (wcTx.value && wcTx.value !== "0" && wcTx.value !== "0x0") { + try { + // Convert hex to decimal string + parsedValue = BigInt(wcTx.value).toString(); + } catch { + parsedValue = wcTx.value; + } + } + + // Pre-fill the form data - use full calldata directly + const prefilledData: ArbitraryActionFormData = { + title: `WalletConnect: ${wcTx.dappName || "Unknown App"}`, + actions: [ + { + target: wcTx.target, + signature: "", // Signature is now optional + data: wcTx.data.startsWith("0x") ? wcTx.data : `0x${wcTx.data}`, // Full calldata with selector + value: parsedValue, + }, + ], + }; + + setArbitraryActionFormData(prefilledData); + setSelectedFactory("arbitrary-action"); + + // Always go directly to signing flow + // Build steps and start signing flow - skip registry for WalletConnect transactions + const { steps } = buildArbitraryActionSteps({ + formData: prefilledData, + safeAddress: safeAddress as Address, + guardAddress: guardAddress as Address, + proposeTransaction: true, + proposePreApproval: false, + approvalDurationSeconds: undefined, + skipRegistry: true, // Don't save WalletConnect transactions to Canon List + }); + + console.log("[NewActionSection] WalletConnect tx - going directly to signing flow"); + + setReviewCheckboxState({ + proposeTransaction: true, + proposePreApproval: false, + approvalDurationSeconds: undefined, + }); + setTransactionSteps(steps); + setCurrentStepIndex(0); + setIsSigningMode(true); + + // Clear the location state to prevent re-processing on navigation + window.history.replaceState({}, document.title); + }, [location.state, path, safeAddress, guardAddress]); + + // Handle factory selection + const handleSelectFactory = (factory: FactoryType) => { + setSelectedFactory(factory); + if (factory === "transfer") { + navigateWithParams("/create/action/transfer"); + } else if (factory === "arbitrary-action") { + navigateWithParams("/create/action/arbitrary-action"); + } else if (factory === "claim-allowance") { + navigateWithParams("/create/action/claim-allowance"); + } + }; + + // Navigate back to factory selection + const handleBackToFactory = () => { + setSelectedFactory(null); + setIsReviewMode(false); + navigateWithParams("/create/action"); + }; + + // Navigate back to transfer form from review + const handleBackToForm = () => { + setIsReviewMode(false); + }; + + // Go to review step (Transfer) + const handleTransferFormContinue = () => { + setSelectedFactory("transfer"); + setIsReviewMode(true); + }; + + // Go to review step (Arbitrary Action) + const handleArbitraryActionFormContinue = () => { + setSelectedFactory("arbitrary-action"); + setIsReviewMode(true); + }; + + // Go to review step (Claim Allowance) + const handleClaimAllowanceFormContinue = () => { + setSelectedFactory("claim-allowance"); + setIsReviewMode(true); + }; + + // Navigate back to main create page + const handleNavigateToCreate = () => { + resetFlow(); + navigateWithParams("/create"); + }; + + // Change factory + const handleChangeFactory = () => { + setIsReviewMode(false); + navigateWithParams("/create/action"); + }; + + // Handle initiate action (deployment) - starts signing flow (Transfer) + const handleInitiate = ( + proposeTransaction: boolean, + proposePreApproval: boolean, + approvalDurationSeconds?: bigint, + ) => { + if (!safeAddress || !guardAddress) { + console.error("Missing safeAddress or guardAddress"); + return; + } + + // Build transaction steps based on checkbox selections + const { steps } = buildTransactionSteps({ + formData: transferFormData, + safeAddress: safeAddress as Address, + guardAddress: guardAddress as Address, + proposeTransaction, + proposePreApproval, + approvalDurationSeconds, + }); + + // Store checkbox state for reference (including duration) + setReviewCheckboxState({ proposeTransaction, proposePreApproval, approvalDurationSeconds }); + + // Set up signing flow + setTransactionSteps(steps); + setCurrentStepIndex(0); + setIsSigningMode(true); + }; + + // Handle initiate Arbitrary Action - starts signing flow + const handleInitiateArbitraryAction = ( + proposeTransaction: boolean, + proposePreApproval: boolean, + approvalDurationSeconds?: bigint, + ) => { + if (!safeAddress || !guardAddress) { + console.error("Missing safeAddress or guardAddress"); + return; + } + + const { steps } = buildArbitraryActionSteps({ + formData: arbitraryActionFormData, + safeAddress: safeAddress as Address, + guardAddress: guardAddress as Address, + proposeTransaction, + proposePreApproval, + approvalDurationSeconds, + }); + + setReviewCheckboxState({ proposeTransaction, proposePreApproval, approvalDurationSeconds }); + setTransactionSteps(steps); + setCurrentStepIndex(0); + setIsSigningMode(true); + }; + + // Handle initiate Claim Allowance - starts signing flow + const handleInitiateClaimAllowance = ( + proposeTransaction: boolean, + proposePreApproval: boolean, + approvalDurationSeconds?: bigint, + ) => { + if (!safeAddress || !guardAddress) { + console.error("Missing safeAddress or guardAddress"); + return; + } + + const { steps } = buildClaimAllowanceSteps({ + formData: claimAllowanceFormData, + safeAddress: safeAddress as Address, + guardAddress: guardAddress as Address, + proposeTransaction, + proposePreApproval, + approvalDurationSeconds, + }); + + setReviewCheckboxState({ proposeTransaction, proposePreApproval, approvalDurationSeconds }); + setTransactionSteps(steps); + setCurrentStepIndex(0); + setIsSigningMode(true); + }; + + // Handle edit from review + const handleEdit = () => { + setIsReviewMode(false); + }; + + // ===================================================== + // Hub-specific handlers + // ===================================================== + + // Handle hub selection + const handleSelectHub = (hub: HubType) => { + setSelectedHub(hub); + if (hub === "capped-transfer-hub") { + navigateWithParams("/create/hub/capped-transfer"); + } + }; + + // Navigate back to hub selection + const handleBackToHubSelection = () => { + setSelectedHub(null); + setIsHubReviewMode(false); + navigateWithParams("/create/hub"); + }; + + // Navigate back to hub form from review + const handleBackToHubForm = () => { + setIsHubReviewMode(false); + }; + + // Go to review step (Capped Transfer Hub) + const handleCappedTransferHubFormContinue = () => { + setSelectedHub("capped-transfer-hub"); + setIsHubReviewMode(true); + }; + + // Change hub type + const handleChangeHub = () => { + setIsHubReviewMode(false); + navigateWithParams("/create/hub"); + }; + + // Handle edit from hub review + const handleHubEdit = () => { + setIsHubReviewMode(false); + }; + + // Handle initiate Capped Transfer Hub - starts signing flow + const handleInitiateCappedTransferHub = (proposePreApproval: boolean, approvalDurationSeconds?: bigint) => { + if (!safeAddress || !guardAddress) { + console.error("Missing safeAddress or guardAddress"); + return; + } + + const { steps } = buildCappedTransferHubSteps({ + formData: cappedTransferHubFormData, + safeAddress: safeAddress as Address, + guardAddress: guardAddress as Address, + proposePreApproval, + approvalDurationSeconds, + }); + + setReviewCheckboxState({ proposeTransaction: false, proposePreApproval, approvalDurationSeconds }); + setTransactionSteps(steps); + setCurrentStepIndex(0); + setIsSigningMode(true); + }; + + // ===================================================== + // Hub Child handlers + // ===================================================== + + // Navigate back to Canon List from hub child form + const handleBackFromHubChild = () => { + navigateWithParams("/canon-list"); + }; + + // Navigate back to hub child form from review + const handleBackToHubChildForm = () => { + setIsHubChildReviewMode(false); + }; + + // Go to review step (Hub Child) + const handleHubChildFormContinue = () => { + setIsHubChildReviewMode(true); + }; + + // Handle edit from hub child review + const handleHubChildEdit = () => { + setIsHubChildReviewMode(false); + }; + + // Handle initiate Hub Child deployment - starts signing flow + const handleInitiateHubChild = (proposeTransaction: boolean) => { + if (!safeAddress || !guardAddress || !hubAddressParam) { + console.error("Missing safeAddress, guardAddress, or hubAddress"); + return; + } + + const { steps } = buildDeployHubChildSteps({ + formData: hubChildFormData, + hubAddress: hubAddressParam as Address, + safeAddress: safeAddress as Address, + guardAddress: guardAddress as Address, + proposeTransaction, + }); + + setReviewCheckboxState({ proposeTransaction, proposePreApproval: false, approvalDurationSeconds: undefined }); + setTransactionSteps(steps); + setCurrentStepIndex(0); + setIsSigningMode(true); + }; + + // Handle back from signing flow + const handleBackFromSigning = () => { + setIsSigningMode(false); + setTransactionSteps([]); + setCurrentStepIndex(0); + setDeployedActionAddress(null); + setPreApprovalAddress(null); + resetExecutor(); + }; + + /** + * Execute the current transaction step + * For the Deploy step, this calls the real contract + * For subsequent steps, this uses a mock (to be implemented later) + * + * @param nonce - Optional nonce to use for signing (passed from NonceSelector) + */ + const handleExecuteStep = useCallback( + async (nonce?: number) => { + if (currentStepIndex >= transactionSteps.length) return; + + // Capture the step index at call time to avoid stale closure issues + const stepIndex = currentStepIndex; + const currentStep = transactionSteps[stepIndex]; + + // Set current step to "waiting" (user will see wallet popup) + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "waiting" }; + return updated; + }); + + // Handle the Deploy Contract step for Transfer (first step) + if (currentStep.id === "deploy-action") { + console.log("[handleExecuteStep] Executing deploy-action step"); + const result = await executeDeployTransfer(transferFormData); + console.log("[handleExecuteStep] Deploy result:", result); + + if (result) { + // Success - store deployed address and mark step as signed + setDeployedActionAddress(result.deployedAddress); + console.log("[handleExecuteStep] Action deployed at:", result.deployedAddress); + console.log("[handleExecuteStep] Transaction hash:", result.txHash); + console.log("[handleExecuteStep] Updating step", stepIndex, "to signed and advancing to", stepIndex + 1); + + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "signed" }; + // Set next step to pending + if (stepIndex + 1 < updated.length) { + updated[stepIndex + 1] = { ...updated[stepIndex + 1], status: "pending" }; + } + console.log( + "[handleExecuteStep] Updated steps:", + updated.map((s) => ({ id: s.id, status: s.status })), + ); + return updated; + }); + setCurrentStepIndex(stepIndex + 1); + console.log("[handleExecuteStep] setCurrentStepIndex called with:", stepIndex + 1); + } else { + // Failed - revert to error status + console.log("[handleExecuteStep] Deploy failed, setting error status"); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + } + return; + } + + // Handle the Deploy Contract step for Arbitrary Action + if (currentStep.id === "deploy-arbitrary-action") { + console.log("[handleExecuteStep] Executing deploy-arbitrary-action step"); + const result = await executeDeployArbitraryAction(arbitraryActionFormData); + console.log("[handleExecuteStep] Deploy ArbitraryAction result:", result); + + if (result) { + setDeployedActionAddress(result.deployedAddress); + console.log("[handleExecuteStep] ArbitraryAction deployed at:", result.deployedAddress); + + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "signed" }; + if (stepIndex + 1 < updated.length) { + updated[stepIndex + 1] = { ...updated[stepIndex + 1], status: "pending" }; + } + return updated; + }); + setCurrentStepIndex(stepIndex + 1); + } else { + console.log("[handleExecuteStep] Deploy ArbitraryAction failed, setting error status"); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + } + return; + } + + // Handle the Deploy Contract step for Claim Allowance + if (currentStep.id === "deploy-claim-allowance") { + console.log("[handleExecuteStep] Executing deploy-claim-allowance step"); + const result = await executeDeployClaimAllowance(claimAllowanceFormData); + console.log("[handleExecuteStep] Deploy ClaimAllowance result:", result); + + if (result) { + setDeployedActionAddress(result.deployedAddress); + console.log("[handleExecuteStep] ClaimAllowance deployed at:", result.deployedAddress); + + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "signed" }; + if (stepIndex + 1 < updated.length) { + updated[stepIndex + 1] = { ...updated[stepIndex + 1], status: "pending" }; + } + return updated; + }); + setCurrentStepIndex(stepIndex + 1); + } else { + console.log("[handleExecuteStep] Deploy ClaimAllowance failed, setting error status"); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + } + return; + } + + // Handle the Deploy Hub step for Capped Token Transfers Hub + if (currentStep.id === "deploy-capped-transfer-hub") { + console.log("[handleExecuteStep] Executing deploy-capped-transfer-hub step"); + const result = await executeDeployCappedTransferHub(cappedTransferHubFormData, safeAddress as Address); + console.log("[handleExecuteStep] Deploy CappedTransferHub result:", result); + + if (result) { + setDeployedActionAddress(result.deployedAddress); + console.log("[handleExecuteStep] CappedTransferHub deployed at:", result.deployedAddress); + + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "signed" }; + if (stepIndex + 1 < updated.length) { + updated[stepIndex + 1] = { ...updated[stepIndex + 1], status: "pending" }; + } + return updated; + }); + setCurrentStepIndex(stepIndex + 1); + } else { + console.log("[handleExecuteStep] Deploy CappedTransferHub failed, setting error status"); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + } + return; + } + + // Handle the Deploy Hub Child step + if (currentStep.id === "deploy-hub-child") { + console.log("[handleExecuteStep] Executing deploy-hub-child step"); + if (!hubAddressParam) { + console.error("[handleExecuteStep] Missing hubAddress for hub child deploy"); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + return; + } + + const result = await executeDeployHubChild(hubAddressParam as Address, hubChildFormData); + console.log("[handleExecuteStep] Deploy Hub Child result:", result); + + if (result) { + setDeployedActionAddress(result.deployedAddress); + console.log("[handleExecuteStep] Hub Child deployed at:", result.deployedAddress); + + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "signed" }; + if (stepIndex + 1 < updated.length) { + updated[stepIndex + 1] = { ...updated[stepIndex + 1], status: "pending" }; + } + return updated; + }); + setCurrentStepIndex(stepIndex + 1); + } else { + console.log("[handleExecuteStep] Deploy Hub Child failed, setting error status"); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + } + return; + } + + // Handle the Save to Registry step (second step) + if (currentStep.id === "record-registry") { + console.log("[handleExecuteStep] Executing record-registry step"); + + if (!guardAddress || !deployedActionAddress) { + console.error("[handleExecuteStep] Missing guardAddress or deployedActionAddress for registry step"); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + return; + } + + // Get label from the appropriate form data based on selected factory or hub + let label: string; + if (path.startsWith("/create/hub-child/")) { + label = hubChildFormData.title || "Untitled Transfer"; + } else if (selectedHub === "capped-transfer-hub") { + label = cappedTransferHubFormData.title || "Untitled Hub"; + } else if (selectedFactory === "arbitrary-action") { + label = arbitraryActionFormData.title || "Untitled Arbitrary Action"; + } else if (selectedFactory === "claim-allowance") { + label = claimAllowanceFormData.title || "Untitled Claim Allowance"; + } else { + label = transferFormData.title || "Untitled Transfer"; + } + const result = await executeRecordToRegistry(guardAddress as Address, deployedActionAddress, label); + console.log("[handleExecuteStep] Registry result:", result); + + if (result) { + console.log("[handleExecuteStep] Registered to registry with hash:", result.txHash); + console.log("[handleExecuteStep] Updating step", stepIndex, "to signed and advancing to", stepIndex + 1); + + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "signed" }; + if (stepIndex + 1 < updated.length) { + updated[stepIndex + 1] = { ...updated[stepIndex + 1], status: "pending" }; + } + console.log( + "[handleExecuteStep] Updated steps:", + updated.map((s) => ({ id: s.id, status: s.status })), + ); + return updated; + }); + setCurrentStepIndex(stepIndex + 1); + console.log("[handleExecuteStep] setCurrentStepIndex called with:", stepIndex + 1); + } else { + console.log("[handleExecuteStep] Registry record failed, setting error status"); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + } + return; + } + + // Handle the Queue Transaction step (for the main action) + if (currentStep.id === "queue-action") { + console.log("[handleExecuteStep] Executing queue-action step"); + + if (!guardAddress || !deployedActionAddress) { + console.error("[handleExecuteStep] Missing guardAddress or deployedActionAddress for queue step"); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + return; + } + + const result = await executeQueueTransaction(guardAddress as Address, deployedActionAddress); + console.log("[handleExecuteStep] Queue result:", result); + + if (result) { + console.log("[handleExecuteStep] Queued with hash:", result.txHash); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "signed" }; + if (stepIndex + 1 < updated.length) { + updated[stepIndex + 1] = { ...updated[stepIndex + 1], status: "pending" }; + } + return updated; + }); + setCurrentStepIndex(stepIndex + 1); + } else { + console.log("[handleExecuteStep] Queue failed, setting error status"); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + } + return; + } + + // Handle the Sign Transaction step (for the main action) + if (currentStep.id === "sign-safe-tx") { + console.log("[handleExecuteStep] Executing sign-safe-tx step"); + + if (!safeAddress || !guardAddress || !deployedActionAddress) { + console.error("[handleExecuteStep] Missing addresses for sign step"); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + return; + } + + const result = await executeSignTransaction( + safeAddress as Address, + guardAddress as Address, + deployedActionAddress, + nonce, + ); + console.log("[handleExecuteStep] Sign result:", result, "nonce:", nonce); + + if (result) { + console.log("[handleExecuteStep] Signed with safeTxHash:", result.safeTxHash); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "signed" }; + if (stepIndex + 1 < updated.length) { + updated[stepIndex + 1] = { ...updated[stepIndex + 1], status: "pending" }; + } + return updated; + }); + setCurrentStepIndex(stepIndex + 1); + // Notify that queue count may have changed + onQueueCountChange?.(); + } else { + console.log("[handleExecuteStep] Sign failed, setting error status"); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + } + return; + } + + // Handle the Deploy Pre-Approval step + if (currentStep.id === "deploy-preapprove") { + console.log("[handleExecuteStep] Executing deploy-preapprove step"); + + if (!deployedActionAddress || !reviewCheckboxState.approvalDurationSeconds) { + console.error( + "[handleExecuteStep] Missing deployedActionAddress or approvalDuration for pre-approval deploy", + ); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + return; + } + + const result = await executeDeployPreApproval( + deployedActionAddress, + reviewCheckboxState.approvalDurationSeconds, + ); + console.log("[handleExecuteStep] Deploy pre-approval result:", result); + + if (result) { + // Store the pre-approval address for subsequent queue/sign steps + setPreApprovalAddress(result.preApprovalAddress); + console.log("[handleExecuteStep] Pre-approval deployed at:", result.preApprovalAddress); + + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "signed" }; + if (stepIndex + 1 < updated.length) { + updated[stepIndex + 1] = { ...updated[stepIndex + 1], status: "pending" }; + } + return updated; + }); + setCurrentStepIndex(stepIndex + 1); + } else { + console.log("[handleExecuteStep] Deploy pre-approval failed, setting error status"); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + } + return; + } + + // Handle the Queue Pre-Approval step + if (currentStep.id === "queue-preapprove") { + console.log("[handleExecuteStep] Executing queue-preapprove step"); + + if (!guardAddress || !preApprovalAddress) { + console.error("[handleExecuteStep] Missing guardAddress or preApprovalAddress for pre-approval queue"); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + return; + } + + const result = await executeQueueTransaction(guardAddress as Address, preApprovalAddress); + console.log("[handleExecuteStep] Queue pre-approval result:", result); + + if (result) { + console.log("[handleExecuteStep] Pre-approval queued with hash:", result.txHash); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "signed" }; + if (stepIndex + 1 < updated.length) { + updated[stepIndex + 1] = { ...updated[stepIndex + 1], status: "pending" }; + } + return updated; + }); + setCurrentStepIndex(stepIndex + 1); + } else { + console.log("[handleExecuteStep] Queue pre-approval failed, setting error status"); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + } + return; + } + + // Handle the Sign Pre-Approval step + if (currentStep.id === "sign-preapprove") { + console.log("[handleExecuteStep] Executing sign-preapprove step"); + + if (!safeAddress || !guardAddress || !preApprovalAddress) { + console.error("[handleExecuteStep] Missing addresses for pre-approval sign step"); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + return; + } + + const result = await executeSignTransaction( + safeAddress as Address, + guardAddress as Address, + preApprovalAddress, + nonce, + ); + console.log("[handleExecuteStep] Sign pre-approval result:", result, "nonce:", nonce); + + if (result) { + console.log("[handleExecuteStep] Pre-approval signed with safeTxHash:", result.safeTxHash); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "signed" }; + if (stepIndex + 1 < updated.length) { + updated[stepIndex + 1] = { ...updated[stepIndex + 1], status: "pending" }; + } + return updated; + }); + setCurrentStepIndex(stepIndex + 1); + // Notify that queue count may have changed + onQueueCountChange?.(); + } else { + console.log("[handleExecuteStep] Sign pre-approval failed, setting error status"); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + } + return; + } + + // Handle the Queue Emergency Off step (Turn Off Emergency Mode flow) + if (currentStep.id === "queue-emergency-off") { + console.log("[handleExecuteStep] Executing queue-emergency-off step"); + + if (!guardAddress) { + console.error("[handleExecuteStep] Missing guardAddress for queue step"); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + return; + } + + const result = await executeQueueTransaction(guardAddress as Address, UNSET_EMERGENCY_MODE_ACTION); + console.log("[handleExecuteStep] Queue emergency off result:", result); + + if (result) { + console.log("[handleExecuteStep] Queued emergency off with hash:", result.txHash); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "signed" }; + if (stepIndex + 1 < updated.length) { + updated[stepIndex + 1] = { ...updated[stepIndex + 1], status: "pending" }; + } + return updated; + }); + setCurrentStepIndex(stepIndex + 1); + } else { + console.log("[handleExecuteStep] Queue emergency off failed, setting error status"); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + } + return; + } + + // Handle the Sign Emergency Off step (Turn Off Emergency Mode flow) + if (currentStep.id === "sign-emergency-off") { + console.log("[handleExecuteStep] Executing sign-emergency-off step"); + + if (!safeAddress || !guardAddress) { + console.error("[handleExecuteStep] Missing addresses for sign step"); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + return; + } + + const result = await executeSignTransaction( + safeAddress as Address, + guardAddress as Address, + UNSET_EMERGENCY_MODE_ACTION, + nonce, + ); + console.log("[handleExecuteStep] Sign emergency off result:", result, "nonce:", nonce); + + if (result) { + console.log("[handleExecuteStep] Emergency off signed with safeTxHash:", result.safeTxHash); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "signed" }; + if (stepIndex + 1 < updated.length) { + updated[stepIndex + 1] = { ...updated[stepIndex + 1], status: "pending" }; + } + return updated; + }); + setCurrentStepIndex(stepIndex + 1); + // Notify that queue count may have changed + onQueueCountChange?.(); + } else { + console.log("[handleExecuteStep] Sign emergency off failed, setting error status"); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + } + return; + } + + // Fallback for unknown step IDs (shouldn't happen in normal usage) + console.error("[handleExecuteStep] Unknown step ID:", currentStep.id); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + }, + [ + currentStepIndex, + transactionSteps, + selectedFactory, + selectedHub, + transferFormData, + arbitraryActionFormData, + claimAllowanceFormData, + cappedTransferHubFormData, + hubChildFormData, + hubAddressParam, + executeDeployTransfer, + executeDeployArbitraryAction, + executeDeployClaimAllowance, + executeDeployHubChild, + executeDeployCappedTransferHub, + executeRecordToRegistry, + executeQueueTransaction, + executeSignTransaction, + executeDeployPreApproval, + guardAddress, + safeAddress, + deployedActionAddress, + preApprovalAddress, + reviewCheckboxState.approvalDurationSeconds, + onQueueCountChange, + ], + ); + + // Check if signing is complete + const isSigningComplete = transactionSteps.length > 0 && transactionSteps.every((s) => s.status === "signed"); + + // /create/action -> Select Factory + if (path === "/create/action") { + return ; + } + + // /create/action/transfer -> Transfer Form, Review, or Signing + if (path === "/create/action/transfer") { + // Show signing flow if in signing mode + if (isSigningMode) { + return ( + + ); + } + + // Show review step + if (isReviewMode) { + return ( + + ); + } + + // Show transfer form + return ( + + ); + } + + // /create/action/arbitrary-action -> Arbitrary Action Form, Review, or Signing + if (path === "/create/action/arbitrary-action") { + // Show signing flow if in signing mode + if (isSigningMode) { + return ( + + ); + } + + // Show review step + if (isReviewMode) { + return ( + + ); + } + + // Show arbitrary action form + return ( + + ); + } + + // /create/action/claim-allowance -> Claim Allowance Form, Review, or Signing + if (path === "/create/action/claim-allowance") { + // Show signing flow if in signing mode + if (isSigningMode) { + return ( + + ); + } + + // Show review step + if (isReviewMode) { + return ( + + ); + } + + // Show claim allowance form + return ( + + ); + } + + // /create/action/turn-off-emergency -> Signing Flow only (no form) + const isTurnOffEmergencyPath = + path === "/create/action/turn-off-emergency" || path.startsWith("/create/action/turn-off-emergency"); + + if (isTurnOffEmergencyPath) { + // Wait for initialization to complete (useEffect will set up the steps) + if (!emergencyFlowInitialized || transactionSteps.length === 0) { + // Show loading state while initializing + return ( + + Initializing Turn Off Emergency Mode... + + ); + } + + return ( + + ); + } + + // ===================================================== + // Hub Routes + // ===================================================== + + // /create/hub -> Select Hub Type + if (path === "/create/hub") { + return ; + } + + // /create/hub/capped-transfer -> Capped Transfer Hub Form, Review, or Signing + if (path === "/create/hub/capped-transfer") { + // Show signing flow if in signing mode + if (isSigningMode) { + return ( + + ); + } + + // Show review step + if (isHubReviewMode) { + return ( + + ); + } + + // Show capped transfer hub form + return ( + + ); + } + + // /create/hub-child/:hubAddress -> Deploy Hub Child Form, Review, or Signing + if (path.startsWith("/create/hub-child/") && hubAddressParam) { + // Show signing flow if in signing mode + if (isSigningMode) { + return ( + + ); + } + + // Show review step + if (isHubChildReviewMode) { + return ( + + ); + } + + // Show hub child form + return ( + + ); + } + + // Default: show factory selection + return ; +}; + +// Styled components for loading state +const LoadingContainer = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + padding: "64px", + width: "100%", +}); + +const LoadingText = styled(Typography)({ + fontSize: "14px", + fontWeight: 400, + color: canonHeaderTokens.foreground.accent20, +}); diff --git a/src/components/NewAction/index.ts b/src/components/NewAction/index.ts new file mode 100644 index 0000000..f54d865 --- /dev/null +++ b/src/components/NewAction/index.ts @@ -0,0 +1,3 @@ +export { NewActionSection } from "./NewActionSection"; +export { SelectFactoryStep, TransferFormStep, ReviewDeployStep } from "./steps"; +// Types are now exported from ~/contexts diff --git a/src/components/NewAction/shared/Breadcrumb.tsx b/src/components/NewAction/shared/Breadcrumb.tsx new file mode 100644 index 0000000..63ca2db --- /dev/null +++ b/src/components/NewAction/shared/Breadcrumb.tsx @@ -0,0 +1,73 @@ +import { Box, Typography, styled } from "@mui/material"; +import { ChevronRightIcon, HelpCircleIcon } from "~/components/icons"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; + +interface BreadcrumbProps { + onNavigateToCreate: () => void; + currentPage: string; + /** If true, shows only the current page without "Create >" prefix */ + standalone?: boolean; + /** Custom text for the first link (defaults to "Create") */ + firstLinkText?: string; +} + +export const Breadcrumb = ({ + onNavigateToCreate, + currentPage, + standalone = false, + firstLinkText = "Create", +}: BreadcrumbProps) => { + return ( + + {!standalone && ( + <> + {firstLinkText} + + + )} + {currentPage} + + + + + ); +}; + +const BreadcrumbContainer = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", + padding: "32px 8px 12px 8px", + width: "100%", +}); + +const BreadcrumbLink = styled(Typography)({ + fontSize: "24px", + fontWeight: 600, + fontStyle: "italic", + lineHeight: "32px", + color: canonHeaderTokens.foreground.accent20, + cursor: "pointer", + transition: "color 0.2s ease", + "&:hover": { + color: canonHeaderTokens.foreground.accent10, + }, +}); + +const CurrentPage = styled(Typography)({ + fontSize: "24px", + fontWeight: 600, + fontStyle: "italic", + lineHeight: "32px", + color: canonHeaderTokens.foreground.accent0, +}); + +const HelpIconWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + cursor: "pointer", + "&:hover": { + opacity: 0.8, + }, +}); diff --git a/src/components/NewAction/shared/FormCard.tsx b/src/components/NewAction/shared/FormCard.tsx new file mode 100644 index 0000000..2472997 --- /dev/null +++ b/src/components/NewAction/shared/FormCard.tsx @@ -0,0 +1,131 @@ +import type { ReactNode } from "react"; +import { Box, Typography, styled } from "@mui/material"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; + +interface FormCardProps { + children: ReactNode; +} + +export const FormCard = ({ children }: FormCardProps) => { + return {children}; +}; + +interface FormSectionProps { + label: string; + children: ReactNode; +} + +export const FormSection = ({ label, children }: FormSectionProps) => { + return ( + + + {label} + + {children} + + ); +}; + +interface ButtonRowProps { + children: ReactNode; +} + +export const ButtonRow = ({ children }: ButtonRowProps) => { + return {children}; +}; + +interface ActionButtonProps { + variant: "primary" | "secondary"; + onClick: () => void; + children: ReactNode; + disabled?: boolean; +} + +export const ActionButton = ({ variant, onClick, children, disabled = false }: ActionButtonProps) => { + return variant === "primary" ? ( + + {children} + + ) : ( + + {children} + + ); +}; + +const CardContainer = styled(Box)({ + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + overflow: "hidden", + width: "100%", +}); + +const SectionWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "6px", + width: "100%", +}); + +const SectionLabelWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + padding: "8px", + width: "100%", +}); + +const SectionLabel = styled(Typography)({ + fontSize: "11px", + fontWeight: 600, + lineHeight: "12px", + color: canonHeaderTokens.foreground.accent30, + textTransform: "uppercase", +}); + +const ButtonRowContainer = styled(Box)({ + display: "flex", + gap: "16px", + width: "100%", + padding: "24px", + borderTop: `1px dashed ${canonHeaderTokens.background.layer0}`, +}); + +const buttonBaseStyles = { + display: "flex", + alignItems: "center", + justifyContent: "center", + flex: 1, + height: "36px", + padding: "8px 20px", + borderRadius: "100px", + fontSize: "12px", + fontWeight: 600, + lineHeight: "16px", + letterSpacing: "0.6px", + textTransform: "uppercase" as const, + cursor: "pointer", + transition: "opacity 0.2s ease", + "&:hover:not(:disabled)": { + opacity: 0.9, + }, + "&:disabled": { + opacity: 0.5, + cursor: "not-allowed", + }, +}; + +const PrimaryButton = styled("button")({ + ...buttonBaseStyles, + backgroundColor: canonHeaderTokens.brand.green, + color: "#ffffff", + border: "none", +}); + +const SecondaryButton = styled("button")({ + ...buttonBaseStyles, + backgroundColor: "transparent", + color: canonHeaderTokens.foreground.accent10, + border: `1px solid ${canonHeaderTokens.foreground.accent40 || "#37373e"}`, +}); diff --git a/src/components/NewAction/shared/FormInput.tsx b/src/components/NewAction/shared/FormInput.tsx new file mode 100644 index 0000000..f146fea --- /dev/null +++ b/src/components/NewAction/shared/FormInput.tsx @@ -0,0 +1,186 @@ +import type { ReactNode } from "react"; +import { Box, Typography, styled } from "@mui/material"; +import { ChevronDownIcon } from "~/components/icons"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; + +interface FormInputProps { + label: string; + placeholder?: string; + value: string; + onChange: (value: string) => void; + optional?: boolean; + type?: "text" | "number" | "select"; + selectOptions?: { label: string; value: string }[]; + badge?: ReactNode; + disabled?: boolean; + error?: string; + warning?: string; +} + +export const FormInput = ({ + label, + placeholder = "", + value, + onChange, + optional = false, + type = "text", + selectOptions = [], + badge, + disabled = false, + error, + warning, +}: FormInputProps) => { + const hasError = Boolean(error); + const hasWarning = Boolean(warning) && !hasError; + + return ( + + + + {optional && (Optional)} + {badge} + + + {type === "select" ? ( + + onChange(e.target.value)} disabled={disabled}> + + {selectOptions.map((option) => ( + + ))} + + + + + + ) : ( + onChange(e.target.value)} + type={type} + disabled={disabled} + /> + )} + + {hasError && {error}} + {hasWarning && {warning}} + + ); +}; + +const InputWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + width: "100%", +}); + +const LabelRow = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const Label = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent0, +}); + +const OptionalLabel = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent30, +}); + +const InputContainer = styled(Box, { + shouldForwardProp: (prop) => prop !== "hasError" && prop !== "hasWarning", +})<{ hasError?: boolean; hasWarning?: boolean }>(({ hasError, hasWarning }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "16px 20px", + borderRadius: "8px", + border: `1px solid ${ + hasError + ? canonHeaderTokens.status.red + : hasWarning + ? canonHeaderTokens.amber.border + : canonHeaderTokens.foreground.accent40 || "#37373e" + }`, + boxShadow: "0px 1px 3px 0px rgba(0,0,0,0.1), 0px 1px 2px -1px rgba(0,0,0,0.1)", + transition: "border-color 0.2s ease", +})); + +const StyledInput = styled("input")({ + fontSize: "16px", + fontWeight: 400, + lineHeight: "24px", + color: canonHeaderTokens.foreground.accent0, + width: "100%", + padding: 0, + border: "none", + outline: "none", + backgroundColor: "transparent", + "&::placeholder": { + color: canonHeaderTokens.foreground.accent20, + opacity: 1, + }, + "&:disabled": { + cursor: "not-allowed", + opacity: 0.5, + }, +}); + +const SelectWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + width: "100%", +}); + +const StyledSelect = styled("select")({ + flex: 1, + fontSize: "16px", + fontWeight: 400, + lineHeight: "24px", + color: canonHeaderTokens.foreground.accent0, + backgroundColor: "transparent", + border: "none", + outline: "none", + appearance: "none", + cursor: "pointer", + "& option": { + backgroundColor: canonHeaderTokens.background.layer1, + color: canonHeaderTokens.foreground.accent0, + }, +}); + +const ChevronWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + pointerEvents: "none", +}); + +const ErrorMessage = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.status.red, + marginTop: "-4px", +}); + +const WarningMessage = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.amber.text, + marginTop: "-4px", +}); diff --git a/src/components/NewAction/shared/MultiItemForm.tsx b/src/components/NewAction/shared/MultiItemForm.tsx new file mode 100644 index 0000000..5a88f1d --- /dev/null +++ b/src/components/NewAction/shared/MultiItemForm.tsx @@ -0,0 +1,179 @@ +import type { ReactNode } from "react"; +import { Box, Typography, styled } from "@mui/material"; +import { Info as InfoIcon, Plus as PlusIcon, X as XIcon } from "lucide-react"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; + +/** + * Shared styled components for multi-item forms (Transfers, Arbitrary Actions, Hub Tokens). + * These components provide a consistent UI pattern for adding/removing multiple items. + */ + +// Main card container for multi-item sections +export const ItemsCard = styled(Box)({ + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + overflow: "hidden", +}); + +// Wrapper for a single item - handles hover state for remove button +export const ItemSection = styled(Box)({ + display: "flex", + flexDirection: "column", + "&:hover .remove-button": { + opacity: 1, + }, +}); + +// Divider header between items with label and remove button +interface ItemDividerHeaderProps { + label: string; + showRemove?: boolean; + onRemove?: () => void; +} + +export const ItemDividerHeader = ({ label, showRemove = false, onRemove }: ItemDividerHeaderProps) => { + return ( + + {label} + {showRemove && onRemove && ( + + REMOVE + + + )} + + ); +}; + +const DividerHeaderContainer = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "12px", + backgroundColor: canonHeaderTokens.background.layer1Variation, + borderTop: `0.5px solid ${canonHeaderTokens.foreground.accent50}`, +}); + +const ItemLabel = styled(Typography)({ + fontSize: "11px", + fontWeight: 600, + lineHeight: "12px", + color: canonHeaderTokens.foreground.accent30, + textTransform: "uppercase", +}); + +const RemoveButton = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "6px", + cursor: "pointer", + opacity: 0, + transition: "opacity 0.2s ease", + "&:hover": { + opacity: 0.7, + }, +}); + +const RemoveText = styled(Typography)({ + fontSize: "11px", + fontWeight: 400, + lineHeight: "12px", + color: canonHeaderTokens.foreground.accent20, + textTransform: "uppercase", +}); + +// Container for form fields within an item +export const ItemFieldsSection = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "24px", + padding: "24px", + backgroundColor: canonHeaderTokens.background.layer1, +}); + +// Info row with add button at the bottom +interface AddItemRowProps { + infoText: string; + addButtonText: string; + onAdd: () => void; +} + +export const AddItemRow = ({ infoText, addButtonText, onAdd }: AddItemRowProps) => { + return ( + + + + {infoText} + + + {addButtonText} + + + + ); +}; + +const InfoRowContainer = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "16px 20px", + backgroundColor: canonHeaderTokens.background.layer1Variation, + borderTop: `0.5px solid ${canonHeaderTokens.foreground.accent50}`, +}); + +const InfoContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const InfoText = styled(Typography)({ + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent20, +}); + +const AddButton = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", + cursor: "pointer", + transition: "opacity 0.2s ease", + "&:hover": { + opacity: 0.7, + }, +}); + +const AddButtonText = styled(Typography)({ + fontSize: "12px", + fontWeight: 600, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent10, + textTransform: "uppercase", + letterSpacing: "0.6px", +}); + +// Action buttons row with dashed border at bottom +interface ActionButtonRowProps { + children: ReactNode; +} + +export const ActionButtonRow = ({ children }: ActionButtonRowProps) => { + return {children}; +}; + +const ActionButtonRowContainer = styled(Box)({ + padding: "24px", + backgroundColor: canonHeaderTokens.background.layer1, + borderTop: `1px dashed ${canonHeaderTokens.foreground.accent50}`, +}); + +export const ButtonsContainer = styled(Box)({ + display: "flex", + gap: "16px", + width: "100%", +}); diff --git a/src/components/NewAction/shared/ParametersDisplay.tsx b/src/components/NewAction/shared/ParametersDisplay.tsx new file mode 100644 index 0000000..29aa842 --- /dev/null +++ b/src/components/NewAction/shared/ParametersDisplay.tsx @@ -0,0 +1,170 @@ +import { Box, Typography, styled } from "@mui/material"; +import { CopyableText } from "~/components/shared/CopyButton"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import type { TransferFormData, ArbitraryActionFormData, CappedTransferHubFormData } from "../steps"; + +// Type guard to detect hub form data +export const isHubFormData = ( + data: TransferFormData | ArbitraryActionFormData | CappedTransferHubFormData, +): data is CappedTransferHubFormData => { + return "tokens" in data && Array.isArray(data.tokens) && "epochLength" in data; +}; + +// Type guard to detect transfer form data +export const isTransferFormData = ( + data: TransferFormData | ArbitraryActionFormData | CappedTransferHubFormData, +): data is TransferFormData => { + return "transfers" in data && Array.isArray(data.transfers); +}; + +// Type guard to detect arbitrary action form data +export const isArbitraryActionFormData = ( + data: TransferFormData | ArbitraryActionFormData | CappedTransferHubFormData, +): data is ArbitraryActionFormData => { + return "actions" in data && Array.isArray(data.actions); +}; + +interface ParametersDisplayProps { + formData: TransferFormData | ArbitraryActionFormData | CappedTransferHubFormData; + onCopy?: (value: string) => void; +} + +// Helper component for copyable parameter values +const CopyableValue = ({ value, fallback = "-" }: { value: string | undefined; fallback?: string }) => { + if (!value) { + return {fallback}; + } + return ( + + {value} + + ); +}; + +export const ParametersDisplay = ({ formData }: ParametersDisplayProps) => { + if (isHubFormData(formData)) { + // Hub parameters + return ( + + + Recipient + + + + Epoch + + + {formData.tokens.map((token, index) => ( + + + Token {index + 1} Address + + + + Token {index + 1} Amount + + + + ))} + + ); + } + + if (isArbitraryActionFormData(formData)) { + // Arbitrary Action parameters (multiple actions) + return ( + + {formData.actions.map((action, index) => ( + + {formData.actions.length > 1 && ACTION {index + 1}} + + Target Address + + + + Signature + + + + Data + + + + Value (wei) + + + + ))} + + ); + } + + // Transfer parameters (multiple transfers) + return ( + + {formData.transfers.map((transfer, index) => ( + + {formData.transfers.length > 1 && TRANSFER {index + 1}} + + Token Address + + + + Recipient Address + + + + Amount + + + + ))} + + ); +}; + +// Styled components +const ParametersContent = styled(Box)({ + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, +}); + +const SectionHeader = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isFirst", +})<{ $isFirst?: boolean }>(({ $isFirst }) => ({ + padding: "12px 16px 12px 36px", + backgroundColor: canonHeaderTokens.background.layer1Variation, + borderTop: $isFirst ? "none" : `0.5px solid ${canonHeaderTokens.foreground.accent50}`, + fontSize: "11px", + fontWeight: 600, + lineHeight: "12px", + color: canonHeaderTokens.foreground.accent30, + textTransform: "uppercase", +})); + +const ParameterRow = styled(Box, { + shouldForwardProp: (prop) => prop !== "$noBorder", +})<{ $noBorder?: boolean }>(({ $noBorder }) => ({ + display: "flex", + flexDirection: "column", + gap: "8px", + padding: "16px 16px 16px 36px", + borderTop: $noBorder ? "none" : `0.5px solid ${canonHeaderTokens.foreground.accent50}`, +})); + +const ParameterLabel = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const ParameterValue = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent0, + fontFamily: "monospace", + wordBreak: "break-all", +}); diff --git a/src/components/NewAction/shared/index.ts b/src/components/NewAction/shared/index.ts new file mode 100644 index 0000000..1dfb7fd --- /dev/null +++ b/src/components/NewAction/shared/index.ts @@ -0,0 +1,13 @@ +export { Breadcrumb } from "./Breadcrumb"; +export { FormInput } from "./FormInput"; +export { FormCard, FormSection, ButtonRow, ActionButton } from "./FormCard"; +export { ParametersDisplay, isHubFormData } from "./ParametersDisplay"; +export { + ItemsCard, + ItemSection, + ItemDividerHeader, + ItemFieldsSection, + AddItemRow, + ActionButtonRow, + ButtonsContainer, +} from "./MultiItemForm"; diff --git a/src/components/NewAction/steps/ArbitraryActionFormStep.tsx b/src/components/NewAction/steps/ArbitraryActionFormStep.tsx new file mode 100644 index 0000000..a46fae7 --- /dev/null +++ b/src/components/NewAction/steps/ArbitraryActionFormStep.tsx @@ -0,0 +1,368 @@ +import { useMemo } from "react"; +import { Box, Typography, styled } from "@mui/material"; +import { keccak256, toBytes, slice } from "viem"; +import { BoxIcon, AsteriskIcon } from "~/components/icons"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { ActionFactoryType } from "~/types/canon-guard"; +import { FACTORY_DISPLAY_NAMES } from "~/utils/factoryDisplay"; +import { isValidAddress, isValidHexData, isValidValue } from "~/utils/validation"; +import { + Breadcrumb, + FormSection, + FormInput, + ActionButton, + ItemsCard, + ItemSection, + ItemDividerHeader, + ItemFieldsSection, + AddItemRow, + ActionButtonRow, + ButtonsContainer, +} from "../shared"; +import type { ArbitraryActionFormData, ArbitraryActionItem } from "./index"; + +// Compute function selector from signature (first 4 bytes of keccak256) +const computeSelector = (signature: string): string | null => { + if (!signature.trim()) return null; + try { + const hash = keccak256(toBytes(signature)); + return slice(hash, 0, 4); // Returns "0x" + 8 hex chars + } catch { + return null; + } +}; + +// Extract selector from calldata (first 4 bytes) +const extractSelectorFromCalldata = (calldata: string): string | null => { + const trimmed = calldata.trim().toLowerCase(); + if (!trimmed.startsWith("0x") || trimmed.length < 10) return null; + return trimmed.slice(0, 10); +}; + +interface ArbitraryActionFormStepProps { + formData: ArbitraryActionFormData; + onFormDataChange: (data: ArbitraryActionFormData) => void; + onContinue: () => void; + onBack: () => void; + onNavigateToCreate: () => void; + onChangeFactory: () => void; +} + +export const ArbitraryActionFormStep = ({ + formData, + onFormDataChange, + onContinue, + onBack, + onNavigateToCreate, + onChangeFactory, +}: ArbitraryActionFormStepProps) => { + // Update title field + const updateTitle = (value: string) => { + onFormDataChange({ ...formData, title: value }); + }; + + // Update a specific action item + const updateAction = (index: number, field: keyof ArbitraryActionItem, value: string) => { + const newActions = [...formData.actions]; + newActions[index] = { ...newActions[index], [field]: value }; + onFormDataChange({ ...formData, actions: newActions }); + }; + + // Add a new action item + const addAction = () => { + onFormDataChange({ + ...formData, + actions: [...formData.actions, { target: "", signature: "", data: "", value: "" }], + }); + }; + + // Remove an action item + const removeAction = (index: number) => { + const newActions = formData.actions.filter((_, i) => i !== index); + onFormDataChange({ ...formData, actions: newActions }); + }; + + // Validation errors for each action + const errors = useMemo(() => { + return formData.actions.map((action) => { + const dataTrimmed = action.data.trim(); + const signatureTrimmed = action.signature.trim(); + + // Calldata validation - must have selector (at least 10 chars: 0x + 8 hex) + let dataError: string | undefined; + if (dataTrimmed) { + if (!dataTrimmed.startsWith("0x")) { + dataError = "Must be a valid hex string starting with 0x"; + } else if (dataTrimmed.length < 10) { + dataError = "Calldata must include the function selector (at least 10 characters)"; + } else if (!isValidHexData(dataTrimmed)) { + dataError = "Must be a valid hex string"; + } + } + + // Signature validation - optional, but if provided must match calldata selector + let signatureError: string | undefined; + if (signatureTrimmed) { + // Basic format check: must have parentheses + if (!signatureTrimmed.includes("(") || !signatureTrimmed.includes(")")) { + signatureError = "Invalid signature format (e.g. transfer(address,uint256))"; + } else if (dataTrimmed.length >= 10) { + // Validate that signature matches calldata selector + const computedSelector = computeSelector(signatureTrimmed); + const calldataSelector = extractSelectorFromCalldata(dataTrimmed); + + if (computedSelector && calldataSelector && computedSelector.toLowerCase() !== calldataSelector) { + signatureError = "Signature does not match the calldata selector"; + } + } + } + + return { + target: action.target.trim() && !isValidAddress(action.target) ? "Invalid address format" : undefined, + data: dataError, + signature: signatureError, + value: action.value.trim() && !isValidValue(action.value) ? "Must be a valid number" : undefined, + }; + }); + }, [formData.actions]); + + const hasErrors = errors.some((e) => e.target || e.signature || e.data || e.value); + + // Validation: title required, at least one action with target and calldata (with selector) + const isValid = + formData.title.trim() !== "" && + formData.actions.length > 0 && + formData.actions.every((action) => { + const dataTrimmed = action.data.trim(); + return ( + action.target.trim() !== "" && + dataTrimmed.startsWith("0x") && + dataTrimmed.length >= 10 && + isValidHexData(dataTrimmed) + ); + }) && + !hasErrors; + + return ( + + + + + {/* Set Action Details Section */} + + {/* Factory Selector */} + + + CANON FACTORY + + {FACTORY_DISPLAY_NAMES[ActionFactoryType.ARBITRARY_ACTIONS]} + + CHANGE + + + {/* Transaction Title Card */} + + + + + Public + + + + + You can encrypt transaction titles to keep them private. Learn more + + + + + + + {/* Set Action Parameters Section */} + + + {/* Action Items */} + {formData.actions.map((action, index) => ( + + {/* Divider header with action number and remove button */} + 1} + onRemove={() => removeAction(index)} + /> + + {/* Action Fields - Calldata first, then signature (optional) */} + + updateAction(index, "target", value)} + error={errors[index]?.target} + /> + updateAction(index, "data", value)} + error={errors[index]?.data} + /> + updateAction(index, "signature", value)} + error={errors[index]?.signature} + /> + updateAction(index, "value", value)} + error={errors[index]?.value} + /> + + + ))} + + {/* Info Row with ADD ACTION button */} + + + {/* Action Buttons Row */} + + + + BACK + + + CONTINUE + + + + + + + + ); +}; + +const Container = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: "32px 120px 64px", + width: "100%", + boxSizing: "border-box", +}); + +const ContentWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + width: "100%", + maxWidth: "576px", +}); + +const FactorySelector = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "16px", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + cursor: "pointer", + transition: "opacity 0.2s ease", + "&:hover": { + opacity: 0.85, + }, +}); + +const LeftContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const FactoryLabel = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const FactoryValue = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent10, + textTransform: "uppercase", +}); + +const ChangeButton = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const TransactionTitleCard = styled(Box)({ + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + overflow: "hidden", + position: "relative", +}); + +const CardContent = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + padding: "24px", +}); + +const FormInputWrapper = styled(Box)({ + position: "relative", +}); + +const PublicBadge = styled(Box)({ + position: "absolute", + top: "42px", + right: "14px", + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "6px 12px", + borderRadius: "100px", + border: `1px solid ${canonHeaderTokens.amber.border}`, + fontSize: "11px", + fontWeight: 400, + lineHeight: "12px", + color: canonHeaderTokens.amber.text, +}); + +const EncryptionNote = styled(Box)({ + display: "flex", + alignItems: "flex-start", + gap: "6px", +}); + +const NoteText = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent10, +}); + +const LearnMoreLink = styled("span")({ + textDecoration: "underline", + cursor: "pointer", +}); diff --git a/src/components/NewAction/steps/CappedTransferHubFormStep.tsx b/src/components/NewAction/steps/CappedTransferHubFormStep.tsx new file mode 100644 index 0000000..1d79052 --- /dev/null +++ b/src/components/NewAction/steps/CappedTransferHubFormStep.tsx @@ -0,0 +1,507 @@ +import { useMemo } from "react"; +import { Box, Typography, styled } from "@mui/material"; +import { isAddress } from "viem"; +import { Layers2Icon, AsteriskIcon, PlusIcon, TrashIcon, InfoIcon } from "~/components/icons"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { ActionFactoryType } from "~/types/canon-guard"; +import { HUB_DISPLAY_NAMES } from "~/utils/factoryDisplay"; +import { Breadcrumb, FormSection, FormInput, ActionButton } from "../shared"; +import type { CappedTransferHubFormData, TimeUnit } from "./index"; + +// Validation helpers +const isValidAddress = (value: string): boolean => { + if (!value.trim()) return true; // Empty is not invalid (just incomplete) + return isAddress(value); +}; + +const isValidAmount = (value: string): boolean => { + if (!value.trim()) return true; // Empty is not invalid (just incomplete) + const num = parseFloat(value); + return !isNaN(num) && num > 0 && /^[0-9]*\.?[0-9]*$/.test(value); +}; + +const isValidEpochLength = (value: string): boolean => { + if (!value.trim()) return true; + const num = parseFloat(value); + return !isNaN(num) && num > 0 && /^[0-9]*\.?[0-9]*$/.test(value); +}; + +const TIME_UNIT_OPTIONS = [ + { label: "Minutes", value: "minutes" }, + { label: "Hours", value: "hours" }, + { label: "Days", value: "days" }, + { label: "Weeks", value: "weeks" }, + { label: "Months", value: "months" }, +]; + +interface CappedTransferHubFormStepProps { + formData: CappedTransferHubFormData; + onFormDataChange: (data: CappedTransferHubFormData) => void; + onContinue: () => void; + onBack: () => void; + onNavigateToCreate: () => void; + onChangeHub: () => void; +} + +export const CappedTransferHubFormStep = ({ + formData, + onFormDataChange, + onContinue, + onBack, + onNavigateToCreate, + onChangeHub, +}: CappedTransferHubFormStepProps) => { + const updateField = (field: K, value: CappedTransferHubFormData[K]) => { + onFormDataChange({ ...formData, [field]: value }); + }; + + const updateToken = (index: number, field: "address" | "amount", value: string) => { + const newTokens = [...formData.tokens]; + newTokens[index] = { ...newTokens[index], [field]: value }; + updateField("tokens", newTokens); + }; + + const addToken = () => { + updateField("tokens", [...formData.tokens, { address: "", amount: "" }]); + }; + + const removeToken = (index: number) => { + if (formData.tokens.length > 1) { + const newTokens = formData.tokens.filter((_, i) => i !== index); + updateField("tokens", newTokens); + } + }; + + // Validation errors + const errors = useMemo(() => { + const tokenErrors = formData.tokens.map((token) => ({ + address: token.address.trim() && !isValidAddress(token.address) ? "Invalid address format" : undefined, + amount: token.amount.trim() && !isValidAmount(token.amount) ? "Must be a positive number" : undefined, + })); + + return { + recipientAddress: + formData.recipientAddress.trim() && !isValidAddress(formData.recipientAddress) + ? "Invalid address format" + : undefined, + epochLength: + formData.epochLength.trim() && !isValidEpochLength(formData.epochLength) + ? "Must be a positive number" + : undefined, + tokens: tokenErrors, + }; + }, [formData.recipientAddress, formData.epochLength, formData.tokens]); + + const hasErrors = useMemo(() => { + if (errors.recipientAddress || errors.epochLength) return true; + return errors.tokens.some((t) => t.address || t.amount); + }, [errors]); + + const isValid = useMemo(() => { + // All required fields must be filled + if (!formData.title.trim()) return false; + if (!formData.recipientAddress.trim()) return false; + if (!formData.epochLength.trim()) return false; + + // All tokens must have address and amount + for (const token of formData.tokens) { + if (!token.address.trim() || !token.amount.trim()) return false; + } + + return !hasErrors; + }, [formData, hasErrors]); + + return ( + + + + + {/* Set Hub Details Section */} + + {/* Hub Selector */} + + + CANON FACTORY + + HUB: {HUB_DISPLAY_NAMES[ActionFactoryType.CAPPED_TOKEN_TRANSFERS]} + + CHANGE + + + {/* Transaction Title Card */} + + + + updateField("title", value)} + /> + Public + + + + + You can encrypt transaction titles to keep them private. Learn more + + + + + + + {/* Set Action Parameters Section - ONE unified card */} + + + {/* Recipient and Epoch Fields */} + + updateField("recipientAddress", value)} + error={errors.recipientAddress} + /> + + + updateField("epochLength", value)} + error={errors.epochLength} + type='number' + /> + + + updateField("epochUnit", value as TimeUnit)} + type='select' + selectOptions={TIME_UNIT_OPTIONS} + /> + + + + + {/* Token Sections */} + {formData.tokens.map((token, index) => ( + + {/* Token Divider Header */} + + TOKEN {index + 1} + {formData.tokens.length > 1 && ( + removeToken(index)}> + + REMOVE + + )} + + + {/* Token Fields */} + + updateToken(index, "address", value)} + error={errors.tokens[index]?.address} + /> + updateToken(index, "amount", value)} + error={errors.tokens[index]?.amount} + /> + + + ))} + + {/* Info Row with ADD TOKEN button */} + + + + You can transfer multiple tokens at once. + + + ADD TOKEN + + + + + {/* Action Buttons Row */} + + + + BACK + + + CONTINUE + + + + + + + + ); +}; + +const Container = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: "32px 120px 64px", + width: "100%", + boxSizing: "border-box", +}); + +const ContentWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + width: "100%", + maxWidth: "576px", +}); + +const HubSelector = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "16px", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + cursor: "pointer", + transition: "opacity 0.2s ease", + "&:hover": { + opacity: 0.85, + }, +}); + +const LeftContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const HubLabel = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const HubValue = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent10, + textTransform: "uppercase", +}); + +const ChangeButton = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const TransactionTitleCard = styled(Box)({ + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + overflow: "hidden", + position: "relative", +}); + +const CardContent = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + padding: "24px", +}); + +const FormInputWrapper = styled(Box)({ + position: "relative", +}); + +const PublicBadge = styled(Box)({ + position: "absolute", + top: "42px", + right: "14px", + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "6px 12px", + borderRadius: "100px", + border: `1px solid ${canonHeaderTokens.amber.border}`, + fontSize: "11px", + fontWeight: 400, + lineHeight: "12px", + color: canonHeaderTokens.amber.text, +}); + +const EncryptionNote = styled(Box)({ + display: "flex", + alignItems: "flex-start", + gap: "6px", +}); + +const NoteText = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent10, +}); + +const LearnMoreLink = styled("span")({ + textDecoration: "underline", + cursor: "pointer", +}); + +// Unified card for SET ACTION PARAMETERS section +const UnifiedParametersCard = styled(Box)({ + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + overflow: "hidden", +}); + +// Section containing Recipient Address and Epoch fields +const ParametersSection = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "24px", + padding: "24px", + backgroundColor: canonHeaderTokens.background.layer1, +}); + +const EpochRow = styled(Box)({ + display: "flex", + gap: "16px", + alignItems: "flex-end", +}); + +const EpochInputWrapper = styled(Box)({ + flex: 1, +}); + +const UnitSelectWrapper = styled(Box)({ + flex: 1, +}); + +// Token section wrapper - handles hover state for remove button +const TokenSection = styled(Box)({ + display: "flex", + flexDirection: "column", + "&:hover .remove-button": { + opacity: 1, + }, +}); + +// Token divider header with layer1-variation background +const TokenDividerHeader = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "12px", + backgroundColor: canonHeaderTokens.background.layer1Variation, + borderTop: `0.5px solid ${canonHeaderTokens.foreground.accent50}`, +}); + +const TokenLabel = styled(Typography)({ + fontSize: "11px", + fontWeight: 600, + lineHeight: "12px", + color: canonHeaderTokens.foreground.accent30, + textTransform: "uppercase", +}); + +// Remove button - hidden by default, shown on hover +const RemoveButton = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "6px", + cursor: "pointer", + opacity: 0, + transition: "opacity 0.2s ease", + "&:hover": { + opacity: 0.7, + }, +}); + +const RemoveText = styled(Typography)({ + fontSize: "11px", + fontWeight: 400, + lineHeight: "12px", + color: canonHeaderTokens.foreground.accent20, + textTransform: "uppercase", +}); + +// Token fields section +const TokenFieldsSection = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "24px", + padding: "24px", + backgroundColor: canonHeaderTokens.background.layer1, +}); + +// Info row with ADD TOKEN button +const InfoRow = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "16px 20px", + backgroundColor: canonHeaderTokens.background.layer1Variation, + borderTop: `0.5px solid ${canonHeaderTokens.foreground.accent50}`, +}); + +const InfoContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const InfoText = styled(Typography)({ + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent20, +}); + +const AddTokenButton = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", + cursor: "pointer", + transition: "opacity 0.2s ease", + "&:hover": { + opacity: 0.7, + }, +}); + +const AddTokenText = styled(Typography)({ + fontSize: "12px", + fontWeight: 600, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent10, + textTransform: "uppercase", + letterSpacing: "0.6px", +}); + +// Action buttons row with dashed border +const ActionButtonRow = styled(Box)({ + padding: "24px", + backgroundColor: canonHeaderTokens.background.layer1, + borderTop: `1px dashed ${canonHeaderTokens.foreground.accent50}`, +}); + +const ButtonsContainer = styled(Box)({ + display: "flex", + gap: "16px", + width: "100%", +}); diff --git a/src/components/NewAction/steps/ClaimAllowanceFormStep.tsx b/src/components/NewAction/steps/ClaimAllowanceFormStep.tsx new file mode 100644 index 0000000..c4543a1 --- /dev/null +++ b/src/components/NewAction/steps/ClaimAllowanceFormStep.tsx @@ -0,0 +1,260 @@ +import { useMemo } from "react"; +import { Box, Typography, styled } from "@mui/material"; +import { isAddress } from "viem"; +import { BoxIcon, AsteriskIcon } from "~/components/icons"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { ActionFactoryType } from "~/types/canon-guard"; +import { FACTORY_DISPLAY_NAMES } from "~/utils/factoryDisplay"; +import { Breadcrumb, FormSection, FormInput, ActionButton, ButtonRow } from "../shared"; +import type { ClaimAllowanceFormData } from "./index"; + +// Validation helpers +const isValidAddress = (value: string): boolean => { + if (!value.trim()) return true; // Empty is not invalid (just incomplete) + return isAddress(value); +}; + +interface ClaimAllowanceFormStepProps { + formData: ClaimAllowanceFormData; + onFormDataChange: (data: ClaimAllowanceFormData) => void; + onContinue: () => void; + onBack: () => void; + onNavigateToCreate: () => void; + onChangeFactory: () => void; +} + +export const ClaimAllowanceFormStep = ({ + formData, + onFormDataChange, + onContinue, + onBack, + onNavigateToCreate, + onChangeFactory, +}: ClaimAllowanceFormStepProps) => { + const updateField = (field: keyof ClaimAllowanceFormData, value: string) => { + onFormDataChange({ ...formData, [field]: value }); + }; + + // Validation errors + const errors = useMemo( + () => ({ + token: formData.token.trim() && !isValidAddress(formData.token) ? "Invalid address format" : undefined, + tokenOwner: + formData.tokenOwner.trim() && !isValidAddress(formData.tokenOwner) ? "Invalid address format" : undefined, + tokenRecipient: + formData.tokenRecipient.trim() && !isValidAddress(formData.tokenRecipient) + ? "Invalid address format" + : undefined, + }), + [formData.token, formData.tokenOwner, formData.tokenRecipient], + ); + + const hasErrors = Boolean(errors.token || errors.tokenOwner || errors.tokenRecipient); + + const isValid = + formData.title.trim() !== "" && + formData.token.trim() !== "" && + formData.tokenOwner.trim() !== "" && + formData.tokenRecipient.trim() !== "" && + !hasErrors; + + return ( + + + + + {/* Set Action Details Section */} + + {/* Factory Selector */} + + + CANON FACTORY + + {FACTORY_DISPLAY_NAMES[ActionFactoryType.ALLOWANCE_CLAIMOR]} + + CHANGE + + + {/* Transaction Title Card */} + + + + updateField("title", value)} + /> + Public + + + + + You can encrypt transaction titles to keep them private. Learn more + + + + + + + {/* Set Action Parameters Section */} + + + + updateField("token", value)} + error={errors.token} + /> + updateField("tokenOwner", value)} + error={errors.tokenOwner} + /> + updateField("tokenRecipient", value)} + error={errors.tokenRecipient} + /> + + + + BACK + + + CONTINUE + + + + + + + ); +}; + +const Container = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: "32px 120px 64px", + width: "100%", + boxSizing: "border-box", +}); + +const ContentWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + width: "100%", + maxWidth: "576px", +}); + +const FactorySelector = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "16px", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + cursor: "pointer", + transition: "opacity 0.2s ease", + "&:hover": { + opacity: 0.85, + }, +}); + +const LeftContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const FactoryLabel = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const FactoryValue = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent10, + textTransform: "uppercase", +}); + +const ChangeButton = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const TransactionTitleCard = styled(Box)({ + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + overflow: "hidden", + position: "relative", +}); + +const CardContent = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + padding: "24px", +}); + +const FormInputWrapper = styled(Box)({ + position: "relative", +}); + +const PublicBadge = styled(Box)({ + position: "absolute", + top: "42px", + right: "14px", + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "6px 12px", + borderRadius: "100px", + border: `1px solid ${canonHeaderTokens.amber.border}`, + fontSize: "11px", + fontWeight: 400, + lineHeight: "12px", + color: canonHeaderTokens.amber.text, +}); + +const EncryptionNote = styled(Box)({ + display: "flex", + alignItems: "flex-start", + gap: "6px", +}); + +const NoteText = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent10, +}); + +const LearnMoreLink = styled("span")({ + textDecoration: "underline", + cursor: "pointer", +}); + +const ParametersCard = styled(Box)({ + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + overflow: "hidden", +}); diff --git a/src/components/NewAction/steps/DeployHubChildFormStep.tsx b/src/components/NewAction/steps/DeployHubChildFormStep.tsx new file mode 100644 index 0000000..b62700b --- /dev/null +++ b/src/components/NewAction/steps/DeployHubChildFormStep.tsx @@ -0,0 +1,470 @@ +import { useState, useEffect, useMemo } from "react"; +import { Box, Typography, styled, CircularProgress } from "@mui/material"; +import { Address, formatUnits } from "viem"; +import { VectorSquareIcon, AsteriskIcon, ChevronDownIcon } from "~/components/icons"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { ActionFactoryType, CappedTokenTransfersHubInfo, HubTokenConfig } from "~/types/canon-guard"; +import { HUB_DISPLAY_NAMES } from "~/utils/factoryDisplay"; +import { Breadcrumb, FormSection, FormInput, ActionButton, ButtonRow } from "../shared"; + +export interface HubChildFormData { + title: string; + token: Address | ""; + amount: string; +} + +interface DeployHubChildFormStepProps { + hubAddress: Address; + hubLabel: string; + hubInfo: CappedTokenTransfersHubInfo | null; + isLoadingHubInfo: boolean; + formData: HubChildFormData; + onFormDataChange: (data: HubChildFormData) => void; + onContinue: () => void; + onBack: () => void; + onNavigateToCreate: () => void; +} + +export const DeployHubChildFormStep = (props: DeployHubChildFormStepProps) => { + const { hubLabel, hubInfo, isLoadingHubInfo, formData, onFormDataChange, onContinue, onBack, onNavigateToCreate } = + props; + const [tokenDropdownOpen, setTokenDropdownOpen] = useState(false); + + const updateField = (field: keyof HubChildFormData, value: string) => { + onFormDataChange({ ...formData, [field]: value }); + }; + + // Get the selected token's config + const selectedTokenConfig = useMemo((): HubTokenConfig | null => { + if (!formData.token || !hubInfo) return null; + return hubInfo.tokens.find((t) => t.address.toLowerCase() === formData.token.toLowerCase()) || null; + }, [formData.token, hubInfo]); + + // Format cap left for display using the token's actual decimals + const capLeftDisplay = useMemo(() => { + if (!selectedTokenConfig) return null; + return formatUnits(selectedTokenConfig.capLeft, selectedTokenConfig.decimals); + }, [selectedTokenConfig]); + + // Validation + const isValidAmount = (value: string): boolean => { + if (!value.trim()) return true; + const num = parseFloat(value); + return !isNaN(num) && num >= 0 && /^[0-9]*\.?[0-9]*$/.test(value); + }; + + const errors = useMemo( + () => ({ + amount: formData.amount.trim() && !isValidAmount(formData.amount) ? "Must be a valid number" : undefined, + }), + [formData.amount], + ); + + const hasErrors = Boolean(errors.amount); + + const isValid = formData.title.trim() !== "" && formData.token !== "" && formData.amount.trim() !== "" && !hasErrors; + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = () => { + setTokenDropdownOpen(false); + }; + + if (tokenDropdownOpen) { + document.addEventListener("click", handleClickOutside); + } + + return () => { + document.removeEventListener("click", handleClickOutside); + }; + }, [tokenDropdownOpen]); + + const handleTokenSelect = (tokenAddress: Address) => { + onFormDataChange({ ...formData, token: tokenAddress }); + setTokenDropdownOpen(false); + }; + + return ( + + + + + {/* Hub Info Section */} + + + + HUB + + {HUB_DISPLAY_NAMES[ActionFactoryType.CAPPED_TOKEN_TRANSFERS]} + + {hubLabel} + + + + {/* Set Action Details Section */} + + {/* Transaction Title Card */} + + + + updateField("title", value)} + /> + Public + + + + + You can encrypt transaction titles to keep them private. Learn more + + + + + + + {/* Set Action Parameters Section */} + + + + {isLoadingHubInfo ? ( + + + Loading hub configuration... + + ) : !hubInfo ? ( + + Failed to load hub configuration + + ) : ( + <> + {/* Token Dropdown */} + + Token + e.stopPropagation()}> + setTokenDropdownOpen(!tokenDropdownOpen)}> + + {formData.token || "Select a token"} + + + + {tokenDropdownOpen && ( + + {hubInfo.tokens.map((tokenConfig) => ( + handleTokenSelect(tokenConfig.address)} + $isSelected={tokenConfig.address.toLowerCase() === formData.token.toLowerCase()} + > + {tokenConfig.address} + Cap: {formatUnits(tokenConfig.cap, tokenConfig.decimals)} + + ))} + + )} + + + + {/* Recipient (Read-only) */} + + Recipient + {hubInfo.recipient} + Set by hub configuration + + + {/* Amount */} + updateField("amount", value)} + error={errors.amount} + /> + + {/* Cap Remaining Info */} + {selectedTokenConfig && ( + Remaining for this epoch: {capLeftDisplay} + )} + + )} + + + + BACK + + + CONTINUE + + + + + + + ); +}; + +const Container = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: "32px 120px 64px", + width: "100%", + boxSizing: "border-box", +}); + +const ContentWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + width: "100%", + maxWidth: "576px", +}); + +const HubSelector = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "16px", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", +}); + +const LeftContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const HubLabel = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const HubValue = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent10, + textTransform: "uppercase", +}); + +const HubName = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent10, +}); + +const TransactionTitleCard = styled(Box)({ + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + overflow: "hidden", + position: "relative", +}); + +const CardContent = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + padding: "24px", +}); + +const FormInputWrapper = styled(Box)({ + position: "relative", +}); + +const PublicBadge = styled(Box)({ + position: "absolute", + top: "42px", + right: "14px", + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "6px 12px", + borderRadius: "100px", + border: `1px solid ${canonHeaderTokens.amber.border}`, + fontSize: "11px", + fontWeight: 400, + lineHeight: "12px", + color: canonHeaderTokens.amber.text, +}); + +const EncryptionNote = styled(Box)({ + display: "flex", + alignItems: "flex-start", + gap: "6px", +}); + +const NoteText = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent10, +}); + +const LearnMoreLink = styled("span")({ + textDecoration: "underline", + cursor: "pointer", +}); + +const ParametersCard = styled(Box)({ + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + overflow: "hidden", +}); + +const InputWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "8px", +}); + +const InputLabel = styled(Typography)({ + fontSize: "13px", + fontWeight: 500, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent10, +}); + +const TokenDropdownContainer = styled(Box)({ + position: "relative", +}); + +const TokenDropdownButton = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + width: "100%", + padding: "12px 16px", + backgroundColor: canonHeaderTokens.background.layer0, + border: `1px solid ${canonHeaderTokens.foreground.accent50}`, + borderRadius: "8px", + cursor: "pointer", + transition: "border-color 0.2s ease", + "&:hover": { + borderColor: canonHeaderTokens.foreground.accent40, + }, + "&:focus": { + outline: "none", + borderColor: canonHeaderTokens.foreground.accent20, + }, +}); + +const TokenDropdownText = styled(Typography, { + shouldForwardProp: (prop) => prop !== "$hasValue", +})<{ $hasValue: boolean }>(({ $hasValue }) => ({ + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: $hasValue ? canonHeaderTokens.foreground.accent0 : canonHeaderTokens.foreground.accent30, + fontFamily: "monospace", +})); + +const TokenDropdownMenu = styled(Box)({ + position: "absolute", + top: "calc(100% + 4px)", + left: 0, + right: 0, + backgroundColor: canonHeaderTokens.background.layer1, + border: `1px solid ${canonHeaderTokens.foreground.accent50}`, + borderRadius: "8px", + overflow: "hidden", + zIndex: 100, + maxHeight: "200px", + overflowY: "auto", +}); + +const TokenDropdownItem = styled("button", { + shouldForwardProp: (prop) => prop !== "$isSelected", +})<{ $isSelected: boolean }>(({ $isSelected }) => ({ + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + gap: "4px", + width: "100%", + padding: "12px 16px", + backgroundColor: $isSelected ? canonHeaderTokens.background.layer1Variation : "transparent", + border: "none", + cursor: "pointer", + transition: "background-color 0.15s ease", + "&:hover": { + backgroundColor: canonHeaderTokens.background.layer1Variation, + }, +})); + +const TokenAddress = styled(Typography)({ + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent0, + fontFamily: "monospace", +}); + +const TokenCapInfo = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const ReadOnlyInput = styled(Box)({ + padding: "12px 16px", + backgroundColor: canonHeaderTokens.background.layer0, + border: `1px solid ${canonHeaderTokens.foreground.accent50}`, + borderRadius: "8px", + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent20, + fontFamily: "monospace", + wordBreak: "break-all", +}); + +const InputHint = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent30, + fontStyle: "italic", +}); + +const CapRemainingInfo = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, + fontStyle: "italic", + marginTop: "-4px", +}); + +const LoadingState = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", + padding: "24px", +}); + +const LoadingText = styled(Typography)({ + fontSize: "14px", + color: canonHeaderTokens.foreground.accent20, +}); + +const ErrorState = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "24px", +}); + +const ErrorText = styled(Typography)({ + fontSize: "14px", + color: canonHeaderTokens.status.red, +}); diff --git a/src/components/NewAction/steps/DeployHubChildReviewStep.tsx b/src/components/NewAction/steps/DeployHubChildReviewStep.tsx new file mode 100644 index 0000000..df5c564 --- /dev/null +++ b/src/components/NewAction/steps/DeployHubChildReviewStep.tsx @@ -0,0 +1,433 @@ +import { useState } from "react"; +import { Box, Typography, styled } from "@mui/material"; +import { Address } from "viem"; +import { + VectorSquareIcon, + PlusIcon, + MinusIcon, + CheckIcon, + InfoIcon, + ZapIcon, + ZapOffIcon, + LockIcon, +} from "~/components/icons"; +import { CopyableText } from "~/components/shared/CopyButton"; +import { StyledTooltip } from "~/components/shared/StyledComponents"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { ActionFactoryType, CappedTokenTransfersHubInfo } from "~/types/canon-guard"; +import { HUB_DISPLAY_NAMES } from "~/utils/factoryDisplay"; +import { Breadcrumb, FormSection, ActionButton, ButtonRow } from "../shared"; +import type { HubChildFormData } from "./DeployHubChildFormStep"; + +// Tooltip content +const TOOLTIP_DEPLOY_SAVE = "Deploy and save for future use. Deploying and saving doesn't require multisig."; +const TOOLTIP_PROPOSE_TRANSACTION_SLOW = + "Request signatures from Safe signers. This transaction will follow the slow path with a 7-day delay."; +const TOOLTIP_PROPOSE_TRANSACTION_FAST = + "Request signatures from Safe signers. This transaction will follow the fast-path with a 1 hour delay."; + +interface DeployHubChildReviewStepProps { + hubAddress: Address; + hubLabel: string; + hubInfo: CappedTokenTransfersHubInfo | null; + isFastPath: boolean; + formData: HubChildFormData; + onBack: () => void; + onInitiate: (proposeTransaction: boolean) => void; + onNavigateToCreate: () => void; + onEdit: () => void; +} + +export const DeployHubChildReviewStep = (props: DeployHubChildReviewStepProps) => { + const { hubInfo, isFastPath, formData, onBack, onInitiate, onNavigateToCreate, onEdit } = props; + const [parametersExpanded, setParametersExpanded] = useState(false); + const [proposeTransaction, setProposeTransaction] = useState(true); + + // Handle initiate + const handleInitiate = () => { + onInitiate(proposeTransaction); + }; + + // Get recipient from hub info + const recipient = hubInfo?.recipient || "Unknown"; + + return ( + + + + + {/* Preview Action Section */} + + + + {formData.title || "Untitled Transaction"} + + + FROM HUB + + HUB: {HUB_DISPLAY_NAMES[ActionFactoryType.CAPPED_TOKEN_TRANSFERS]} + + EDIT + + + + {/* Parameters Expandable Section */} + setParametersExpanded(!parametersExpanded)}> + + {parametersExpanded ? ( + + ) : ( + + )} + PARAMETERS + + + + {parametersExpanded && ( + + + Token + {formData.token ? ( + + {formData.token} + + ) : ( + - + )} + + + Amount + {formData.amount ? ( + + {formData.amount} + + ) : ( + - + )} + + + Recipient + + {recipient} + + + + )} + + + + {/* Setup Action Deploy Section */} + + + {/* Deploy & Save Transaction - Locked */} + + + + + + + + + + + Deploy & Save Transaction + + + + + + + + + {/* Propose Transaction */} + + + setProposeTransaction(!proposeTransaction)}> + {proposeTransaction && } + + Propose Transaction + + + + {isFastPath ? ( + + ) : ( + + )} + {isFastPath ? "FAST-PATH" : "SLOW-PATH"} + + + + + + + + + + + + BACK + + + INITIATE + + + + + + + ); +}; + +const Container = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: "32px 120px 64px", + width: "100%", + boxSizing: "border-box", +}); + +const ContentWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + width: "100%", + maxWidth: "576px", +}); + +const ActionPreviewCard = styled(Box)({ + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + overflow: "hidden", +}); + +const PreviewHeader = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "20px", + padding: "16px", +}); + +const ActionTitle = styled(Typography)({ + fontSize: "18px", + fontWeight: 600, + lineHeight: "28px", + color: canonHeaderTokens.foreground.accent0, +}); + +const PreviewRow = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + width: "100%", +}); + +const FactoryInfo = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const FactoryLabel = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const FactoryValue = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, + textTransform: "uppercase", +}); + +const EditButton = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, + cursor: "pointer", + "&:hover": { + color: canonHeaderTokens.foreground.accent10, + }, +}); + +const ParametersToggle = styled(Box)({ + display: "flex", + flexDirection: "column", + height: "36px", + backgroundColor: canonHeaderTokens.background.layer1Variation, + borderTop: `0.5px solid ${canonHeaderTokens.foreground.accent50}`, + cursor: "pointer", + "&:hover": { + opacity: 0.9, + }, +}); + +const ToggleContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", + padding: "12px", +}); + +const ToggleLabel = styled(Typography)({ + fontSize: "11px", + fontWeight: 600, + lineHeight: "12px", + color: canonHeaderTokens.foreground.accent30, + textTransform: "uppercase", +}); + +const ParametersContent = styled(Box)({ + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, +}); + +const ParameterRow = styled(Box, { + shouldForwardProp: (prop) => prop !== "$noBorder", +})<{ $noBorder?: boolean }>(({ $noBorder }) => ({ + display: "flex", + flexDirection: "column", + gap: "8px", + padding: "16px 16px 16px 36px", + borderTop: $noBorder ? "none" : `0.5px solid ${canonHeaderTokens.foreground.accent50}`, +})); + +const ParameterLabel = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const ParameterValue = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent0, + fontFamily: "monospace", + wordBreak: "break-all", +}); + +const DeployOptionsCard = styled(Box)({ + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + overflow: "hidden", +}); + +const CheckboxRow = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "16px", + borderBottom: `1px dashed ${canonHeaderTokens.foreground.accent50}`, +}); + +const CheckboxLeft = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "20px", +}); + +const LockedCheckbox = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "28px", + height: "28px", + borderRadius: "8px", + backgroundColor: canonHeaderTokens.brand.green, + position: "relative", + "&:hover": { + "& .check-icon": { + opacity: 0, + }, + "& .lock-icon": { + opacity: 1, + }, + }, +}); + +const CheckIconWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + transition: "opacity 0.15s ease", +}); + +const LockIconWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + position: "absolute", + opacity: 0, + transition: "opacity 0.15s ease", +}); + +const Checkbox = styled(Box)<{ checked: boolean }>(({ checked }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "28px", + height: "28px", + borderRadius: "8px", + backgroundColor: checked ? canonHeaderTokens.brand.green : "transparent", + border: checked ? "none" : `1px solid ${canonHeaderTokens.foreground.accent40}`, + cursor: "pointer", + transition: "all 0.2s ease", + "&:hover": { + opacity: 0.9, + }, +})); + +const CheckboxLabel = styled(Typography)({ + fontSize: "14px", + fontWeight: 600, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent0, +}); + +const RightContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const PathTag = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isFastPath", +})<{ $isFastPath: boolean }>({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const PathLabel = styled(Typography, { + shouldForwardProp: (prop) => prop !== "$isFastPath", +})<{ $isFastPath: boolean }>(({ $isFastPath }) => ({ + fontSize: "11px", + fontWeight: 400, + lineHeight: "12px", + color: $isFastPath ? canonHeaderTokens.brand.green : canonHeaderTokens.status.red, +})); + +const InfoIconWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", +}); diff --git a/src/components/NewAction/steps/HubReviewStep.tsx b/src/components/NewAction/steps/HubReviewStep.tsx new file mode 100644 index 0000000..adedfaf --- /dev/null +++ b/src/components/NewAction/steps/HubReviewStep.tsx @@ -0,0 +1,584 @@ +import { useState, useEffect, useMemo } from "react"; +import { Box, Typography, styled } from "@mui/material"; +import { Address } from "viem"; +import { useConfig } from "wagmi"; +import { readContract } from "wagmi/actions"; +import { canonGuardAbi } from "~/abis/canonGuard"; +import { Layers2Icon, PlusIcon, MinusIcon, CheckIcon, InfoIcon, ZapOffIcon, LockIcon } from "~/components/icons"; +import { CopyableText } from "~/components/shared/CopyButton"; +import { DurationInput } from "~/components/shared/DurationInput"; +import { StyledTooltip } from "~/components/shared/StyledComponents"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { humanizeDuration } from "~/hooks/useCanonGuardConfig"; +import { ActionFactoryType } from "~/types/canon-guard"; +import { HUB_DISPLAY_NAMES } from "~/utils/factoryDisplay"; +import { DURATION_TIME_MULTIPLIERS, type DurationTimeUnit } from "~/utils/timeUnits"; +import { Breadcrumb, FormSection, ActionButton, ButtonRow } from "../shared"; +import type { CappedTransferHubFormData } from "./index"; + +// Tooltip content +const TOOLTIP_DEPLOY_SAVE = + "Deploy and save for future use. Once deployed, you can propose or pre-approve it later. Deploying and saving doesn't require multisig."; +const TOOLTIP_PROPOSE_PREAPPROVAL = + "Request signatures from Safe signers. This transaction will follow the fast-path with a 1 hour delay."; + +interface HubReviewStepProps { + formData: CappedTransferHubFormData; + guardAddress: Address; + chainId: number; + onBack: () => void; + onInitiate: (proposePreApproval: boolean, approvalDurationSeconds?: bigint) => void; + onNavigateToCreate: () => void; + onEdit: () => void; +} + +export const HubReviewStep = ({ + formData, + guardAddress, + chainId, + onBack, + onInitiate, + onNavigateToCreate, + onEdit, +}: HubReviewStepProps) => { + const config = useConfig(); + + const [parametersExpanded, setParametersExpanded] = useState(false); + const [expandedTokens, setExpandedTokens] = useState>({}); + const [proposePreApproval, setProposePreApproval] = useState(false); + + // Pre-approval duration state + const [durationAmount, setDurationAmount] = useState("1"); + const [durationUnit, setDurationUnit] = useState("hours"); + const [maxApprovalDuration, setMaxApprovalDuration] = useState(null); + + // Fetch MAX_APPROVAL_DURATION from Canon Guard contract + useEffect(() => { + const fetchMaxDuration = async () => { + if (!guardAddress || !chainId) return; + + try { + const maxDuration = await readContract(config, { + address: guardAddress, + abi: canonGuardAbi, + functionName: "MAX_APPROVAL_DURATION", + chainId: chainId, + }); + setMaxApprovalDuration(maxDuration as bigint); + } catch (error) { + console.error("[HubReviewStep] Failed to fetch MAX_APPROVAL_DURATION:", error); + } + }; + + fetchMaxDuration(); + }, [config, guardAddress, chainId]); + + // Calculate total duration in seconds and validate + const { totalDurationSeconds, isValid, errorMessage } = useMemo(() => { + const amount = parseFloat(durationAmount) || 0; + if (amount <= 0) { + return { totalDurationSeconds: 0n, isValid: false, errorMessage: "Duration must be greater than 0" }; + } + + const multiplier = DURATION_TIME_MULTIPLIERS[durationUnit]; + const totalSeconds = BigInt(Math.floor(amount * multiplier)); + + if (maxApprovalDuration !== null && totalSeconds > maxApprovalDuration) { + const maxHumanized = humanizeDuration(maxApprovalDuration); + return { + totalDurationSeconds: totalSeconds, + isValid: false, + errorMessage: `Exceeds maximum duration of ${maxHumanized}`, + }; + } + + return { totalDurationSeconds: totalSeconds, isValid: true, errorMessage: null }; + }, [durationAmount, durationUnit, maxApprovalDuration]); + + // Handle initiate with duration + const handleInitiate = () => { + if (proposePreApproval && !isValid) return; + onInitiate(proposePreApproval, proposePreApproval ? totalDurationSeconds : undefined); + }; + + const toggleToken = (index: number) => { + setExpandedTokens((prev) => ({ ...prev, [index]: !prev[index] })); + }; + + // Format epoch display + const epochDisplay = `${formData.epochLength} ${formData.epochUnit}`; + + return ( + + + + + {/* Preview Hub Section */} + + + + {formData.title || "Untitled Hub"} + + + HUB FACTORY + + HUB: {HUB_DISPLAY_NAMES[ActionFactoryType.CAPPED_TOKEN_TRANSFERS]} + + EDIT + + + + {/* Parameters Expandable Section */} + setParametersExpanded(!parametersExpanded)}> + + {parametersExpanded ? ( + + ) : ( + + )} + PARAMETERS + + + + {parametersExpanded && ( + + + Recipient + {formData.recipientAddress ? ( + + {formData.recipientAddress} + + ) : ( + - + )} + + + Epoch + + {epochDisplay} + + + + )} + + {/* Token Expandable Sections */} + {formData.tokens.map((token, index) => ( + + toggleToken(index)}> + + {expandedTokens[index] ? ( + + ) : ( + + )} + TOKEN {index + 1} + + + + {expandedTokens[index] && ( + + + Address + {token.address ? ( + + {token.address} + + ) : ( + - + )} + + + Amount + {token.amount ? ( + + {token.amount} + + ) : ( + - + )} + + + )} + + ))} + + + + {/* Setup Hub Deploy Section */} + + + {/* Deploy & Save Transaction - Locked */} + + + + + + + + + + + Deploy & Save Hub + + + + + + + + + {/* Propose Pre-Approval */} + + + setProposePreApproval(!proposePreApproval)}> + {proposePreApproval && } + + Propose Pre-Approval + + + + + SLOW-PATH + + + + + + + + + + {/* Duration Input - shown when Pre-Approval is checked */} + {proposePreApproval && ( + + Duration + + {!isValid && errorMessage && ( + + {errorMessage} + + )} + + )} + + + + BACK + + + INITIATE + + + + + + + ); +}; + +const Container = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: "32px 120px 64px", + width: "100%", + boxSizing: "border-box", +}); + +const ContentWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + width: "100%", + maxWidth: "576px", +}); + +const HubPreviewCard = styled(Box)({ + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + overflow: "hidden", +}); + +const PreviewHeader = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "20px", + padding: "16px", +}); + +const HubTitle = styled(Typography)({ + fontSize: "18px", + fontWeight: 600, + lineHeight: "28px", + color: canonHeaderTokens.foreground.accent0, +}); + +const PreviewRow = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + width: "100%", +}); + +const HubInfo = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const HubLabel = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const HubValue = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, + textTransform: "uppercase", +}); + +const EditButton = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, + cursor: "pointer", + "&:hover": { + color: canonHeaderTokens.foreground.accent10, + }, +}); + +const ParametersToggle = styled(Box)({ + display: "flex", + flexDirection: "column", + height: "36px", + backgroundColor: canonHeaderTokens.background.layer1Variation, + borderTop: `0.5px solid ${canonHeaderTokens.foreground.accent50}`, + cursor: "pointer", + "&:hover": { + opacity: 0.9, + }, +}); + +const TokenToggle = styled(Box)({ + display: "flex", + flexDirection: "column", + height: "36px", + backgroundColor: canonHeaderTokens.background.layer1Variation, + borderTop: `0.5px solid ${canonHeaderTokens.foreground.accent50}`, + cursor: "pointer", + "&:hover": { + opacity: 0.9, + }, +}); + +const ToggleContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", + padding: "12px", +}); + +const ToggleLabel = styled(Typography)({ + fontSize: "11px", + fontWeight: 600, + lineHeight: "12px", + color: canonHeaderTokens.foreground.accent30, + textTransform: "uppercase", +}); + +const ParametersContent = styled(Box)({ + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, +}); + +const ParameterRow = styled(Box, { + shouldForwardProp: (prop) => prop !== "$noBorder", +})<{ $noBorder?: boolean }>(({ $noBorder }) => ({ + display: "flex", + flexDirection: "column", + gap: "8px", + padding: "16px 16px 16px 36px", + borderTop: $noBorder ? "none" : `0.5px solid ${canonHeaderTokens.foreground.accent50}`, +})); + +const ParameterLabel = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const ParameterValue = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent0, + fontFamily: "monospace", + wordBreak: "break-all", +}); + +const DeployOptionsCard = styled(Box)({ + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + overflow: "hidden", +}); + +const CheckboxRow = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "16px", + borderBottom: `1px dashed ${canonHeaderTokens.foreground.accent50}`, +}); + +const CheckboxLeft = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "20px", +}); + +const LockedCheckbox = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "28px", + height: "28px", + borderRadius: "8px", + backgroundColor: canonHeaderTokens.brand.green, + position: "relative", + "&:hover": { + "& .check-icon": { + opacity: 0, + }, + "& .lock-icon": { + opacity: 1, + }, + }, +}); + +const CheckIconWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + transition: "opacity 0.15s ease", +}); + +const LockIconWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + position: "absolute", + opacity: 0, + transition: "opacity 0.15s ease", +}); + +const Checkbox = styled(Box)<{ checked: boolean }>(({ checked }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "28px", + height: "28px", + borderRadius: "8px", + backgroundColor: checked ? canonHeaderTokens.brand.green : "transparent", + border: checked ? "none" : `1px solid ${canonHeaderTokens.foreground.accent40}`, + cursor: "pointer", + transition: "all 0.2s ease", + "&:hover": { + opacity: 0.9, + }, +})); + +const CheckboxLabel = styled(Typography)({ + fontSize: "14px", + fontWeight: 600, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent0, +}); + +const RightContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const SlowPathTag = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const SlowPathLabel = styled(Typography)({ + fontSize: "11px", + fontWeight: 400, + lineHeight: "12px", + color: canonHeaderTokens.status.red, +}); + +const InfoIconWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", +}); + +// Duration input styles +const DurationInputSection = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "8px", + padding: "16px", + paddingLeft: "64px", + backgroundColor: canonHeaderTokens.background.layer1Variation, + borderTop: `1px dashed ${canonHeaderTokens.foreground.accent50}`, +}); + +const DurationLabel = styled(Typography)({ + fontSize: "12px", + fontWeight: 600, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, + textTransform: "uppercase", + letterSpacing: "0.5px", +}); + +const DurationError = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "6px", + marginTop: "4px", +}); + +const ErrorText = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.status.red, +}); diff --git a/src/components/NewAction/steps/ReviewDeployStep.tsx b/src/components/NewAction/steps/ReviewDeployStep.tsx new file mode 100644 index 0000000..d07639b --- /dev/null +++ b/src/components/NewAction/steps/ReviewDeployStep.tsx @@ -0,0 +1,643 @@ +import { useState, useEffect, useMemo } from "react"; +import { Box, Typography, styled } from "@mui/material"; +import { Address } from "viem"; +import { useConfig } from "wagmi"; +import { readContract } from "wagmi/actions"; +import { canonGuardAbi } from "~/abis/canonGuard"; +import { BoxIcon, PlusIcon, MinusIcon, CheckIcon, InfoIcon, ZapOffIcon, LockIcon } from "~/components/icons"; +import { CopyableText } from "~/components/shared/CopyButton"; +import { DurationInput } from "~/components/shared/DurationInput"; +import { StyledTooltip } from "~/components/shared/StyledComponents"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { humanizeDuration } from "~/hooks/useCanonGuardConfig"; +import { DURATION_TIME_MULTIPLIERS, type DurationTimeUnit } from "~/utils/timeUnits"; +import { Breadcrumb, FormSection, ActionButton, ButtonRow } from "../shared"; +import type { TransferFormData, ArbitraryActionFormData } from "./index"; + +// Type guard to detect transfer form data +const isTransferFormData = (data: TransferFormData | ArbitraryActionFormData): data is TransferFormData => { + return "transfers" in data; +}; + +// Tooltip content +const TOOLTIP_DEPLOY_SAVE = + "Deploy and save for future use. Once deployed, you can propose or pre-approve it later. Deploying and saving doesn't require multisig."; +const TOOLTIP_PROPOSE_TRANSACTION = + "Request signatures from Safe signers. This transaction will follow the slow path with a 7-day delay."; +const TOOLTIP_PROPOSE_PREAPPROVAL = + "Request signatures from Safe signers. This transaction will follow the fast-path with a 1 hour delay."; + +interface ReviewDeployStepProps { + formData: TransferFormData | ArbitraryActionFormData; + guardAddress: Address; + chainId: number; + onBack: () => void; + onInitiate: (proposeTransaction: boolean, proposePreApproval: boolean, approvalDurationSeconds?: bigint) => void; + onNavigateToCreate: () => void; + onEdit: () => void; +} + +export const ReviewDeployStep = ({ + formData, + guardAddress, + chainId, + onBack, + onInitiate, + onNavigateToCreate, + onEdit, +}: ReviewDeployStepProps) => { + const config = useConfig(); + + const [parametersExpanded, setParametersExpanded] = useState(false); + const [proposeTransaction, setProposeTransaction] = useState(true); + const [proposePreApproval, setProposePreApproval] = useState(false); + + // Pre-approval duration state + const [durationAmount, setDurationAmount] = useState("1"); + const [durationUnit, setDurationUnit] = useState("hours"); + const [maxApprovalDuration, setMaxApprovalDuration] = useState(null); + + // Fetch MAX_APPROVAL_DURATION from Canon Guard contract + useEffect(() => { + const fetchMaxDuration = async () => { + if (!guardAddress || !chainId) return; + + try { + const maxDuration = await readContract(config, { + address: guardAddress, + abi: canonGuardAbi, + functionName: "MAX_APPROVAL_DURATION", + chainId: chainId, + }); + setMaxApprovalDuration(maxDuration as bigint); + } catch (error) { + console.error("[ReviewDeployStep] Failed to fetch MAX_APPROVAL_DURATION:", error); + } + }; + + fetchMaxDuration(); + }, [config, guardAddress, chainId]); + + // Calculate total duration in seconds and validate + const { totalDurationSeconds, isValid, errorMessage } = useMemo(() => { + const amount = parseFloat(durationAmount) || 0; + if (amount <= 0) { + return { totalDurationSeconds: 0n, isValid: false, errorMessage: "Duration must be greater than 0" }; + } + + const multiplier = DURATION_TIME_MULTIPLIERS[durationUnit]; + const totalSeconds = BigInt(Math.floor(amount * multiplier)); + + if (maxApprovalDuration !== null && totalSeconds > maxApprovalDuration) { + const maxHumanized = humanizeDuration(maxApprovalDuration); + return { + totalDurationSeconds: totalSeconds, + isValid: false, + errorMessage: `Exceeds maximum duration of ${maxHumanized}`, + }; + } + + return { totalDurationSeconds: totalSeconds, isValid: true, errorMessage: null }; + }, [durationAmount, durationUnit, maxApprovalDuration]); + + // Handle initiate with duration + const handleInitiate = () => { + if (proposePreApproval && !isValid) return; + onInitiate(proposeTransaction, proposePreApproval, proposePreApproval ? totalDurationSeconds : undefined); + }; + + return ( + + + + + {/* Preview Action Section */} + + + + {formData.title || "Untitled Transaction"} + + + CANON FACTORY + + {isTransferFormData(formData) ? "TRANSFER" : "ARBITRARY ACTION"} + + EDIT + + + + {/* Parameters Expandable Section */} + setParametersExpanded(!parametersExpanded)}> + + {parametersExpanded ? ( + + ) : ( + + )} + PARAMETERS + + + + {parametersExpanded && ( + + {isTransferFormData(formData) + ? // Render Transfer parameters + formData.transfers.map((transfer, index) => { + const prefix = formData.transfers.length > 1 ? `Token ${index + 1} - ` : ""; + const isLast = index === formData.transfers.length - 1; + return ( + + + {prefix}Token Address + {transfer.tokenAddress ? ( + + {transfer.tokenAddress} + + ) : ( + - + )} + + + {prefix}Recipient Address + {transfer.recipientAddress ? ( + + {transfer.recipientAddress} + + ) : ( + - + )} + + + {prefix}Amount + {transfer.amount ? ( + + {transfer.amount} + + ) : ( + - + )} + + + ); + }) + : // Render Simple Action parameters + formData.actions.map((action, index) => { + const prefix = formData.actions.length > 1 ? `Action ${index + 1} - ` : ""; + const isLast = index === formData.actions.length - 1; + return ( + + + {prefix}Target Address + {action.target ? ( + + {action.target} + + ) : ( + - + )} + + + {prefix}Function Signature + {action.signature ? ( + + {action.signature} + + ) : ( + - + )} + + + {prefix}Encoded Parameters + {action.data ? ( + + {action.data} + + ) : ( + - + )} + + + {prefix}Value (wei) + {action.value ? ( + + {action.value} + + ) : ( + - + )} + + + ); + })} + + )} + + + + {/* Setup Action Deploy Section */} + + + {/* Deploy & Save Transaction - Locked */} + + + + + + + + + + + Deploy & Save Transaction + + + + + + + + + {/* Propose Transaction */} + + + setProposeTransaction(!proposeTransaction)}> + {proposeTransaction && } + + Propose Transaction + + + + + SLOW-PATH + + + + + + + + + + {/* Propose Pre-Approval */} + + + setProposePreApproval(!proposePreApproval)}> + {proposePreApproval && } + + Propose Pre-Approval + + + + + SLOW-PATH + + + + + + + + + + {/* Duration Input - shown when Pre-Approval is checked */} + {proposePreApproval && ( + + Duration + + {!isValid && errorMessage && ( + + {errorMessage} + + )} + + )} + + + + BACK + + + INITIATE + + + + + + + ); +}; + +const Container = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: "32px 120px 64px", + width: "100%", + boxSizing: "border-box", +}); + +const ContentWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + width: "100%", + maxWidth: "576px", +}); + +const ActionPreviewCard = styled(Box)({ + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + overflow: "hidden", +}); + +const PreviewHeader = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "20px", + padding: "16px", +}); + +const ActionTitle = styled(Typography)({ + fontSize: "18px", + fontWeight: 600, + lineHeight: "28px", + color: canonHeaderTokens.foreground.accent0, +}); + +const PreviewRow = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + width: "100%", +}); + +const FactoryInfo = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const FactoryLabel = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const FactoryValue = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const EditButton = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, + cursor: "pointer", + "&:hover": { + color: canonHeaderTokens.foreground.accent10, + }, +}); + +const ParametersToggle = styled(Box)({ + display: "flex", + flexDirection: "column", + height: "36px", + backgroundColor: canonHeaderTokens.background.layer1Variation, + borderTop: `0.5px solid ${canonHeaderTokens.foreground.accent50}`, + cursor: "pointer", + "&:hover": { + opacity: 0.9, + }, +}); + +const ToggleContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", + padding: "12px", +}); + +const ToggleLabel = styled(Typography)({ + fontSize: "11px", + fontWeight: 600, + lineHeight: "12px", + color: canonHeaderTokens.foreground.accent30, + textTransform: "uppercase", +}); + +const ParametersContent = styled(Box)({ + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, +}); + +const ItemGroup = styled(Box)({ + display: "flex", + flexDirection: "column", +}); + +const ParameterRow = styled(Box, { + shouldForwardProp: (prop) => prop !== "$noBorder", +})<{ $noBorder?: boolean }>(({ $noBorder }) => ({ + display: "flex", + flexDirection: "column", + gap: "8px", + padding: "16px 16px 16px 36px", + borderTop: $noBorder ? "none" : `0.5px solid ${canonHeaderTokens.foreground.accent50}`, +})); + +const ParameterLabel = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const ParameterValue = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent0, + fontFamily: "monospace", + wordBreak: "break-all", +}); + +const DeployOptionsCard = styled(Box)({ + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + overflow: "hidden", +}); + +const CheckboxRow = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "16px", + borderBottom: `1px dashed ${canonHeaderTokens.foreground.accent50}`, +}); + +const CheckboxLeft = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "20px", +}); + +const LockedCheckbox = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "28px", + height: "28px", + borderRadius: "8px", + backgroundColor: canonHeaderTokens.brand.green, + position: "relative", + "&:hover": { + "& .check-icon": { + opacity: 0, + }, + "& .lock-icon": { + opacity: 1, + }, + }, +}); + +const CheckIconWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + transition: "opacity 0.15s ease", +}); + +const LockIconWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + position: "absolute", + opacity: 0, + transition: "opacity 0.15s ease", +}); + +const Checkbox = styled(Box)<{ checked: boolean }>(({ checked }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "28px", + height: "28px", + borderRadius: "8px", + backgroundColor: checked ? canonHeaderTokens.brand.green : "transparent", + border: checked ? "none" : `1px solid ${canonHeaderTokens.foreground.accent40}`, + cursor: "pointer", + transition: "all 0.2s ease", + "&:hover": { + opacity: 0.9, + }, +})); + +const CheckboxLabel = styled(Typography)({ + fontSize: "14px", + fontWeight: 600, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent0, +}); + +const RightContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const SlowPathTag = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const SlowPathLabel = styled(Typography)({ + fontSize: "11px", + fontWeight: 400, + lineHeight: "12px", + color: canonHeaderTokens.status.red, +}); + +const InfoIconWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", +}); + +// Duration input styles +const DurationInputSection = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "8px", + padding: "16px", + paddingLeft: "64px", // Align with checkbox labels + backgroundColor: canonHeaderTokens.background.layer1Variation, + borderTop: `1px dashed ${canonHeaderTokens.foreground.accent50}`, +}); + +const DurationLabel = styled(Typography)({ + fontSize: "12px", + fontWeight: 600, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, + textTransform: "uppercase", + letterSpacing: "0.5px", +}); + +const DurationError = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "6px", + marginTop: "4px", +}); + +const ErrorText = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.status.red, +}); diff --git a/src/components/NewAction/steps/SelectFactoryStep.tsx b/src/components/NewAction/steps/SelectFactoryStep.tsx new file mode 100644 index 0000000..7ebcd67 --- /dev/null +++ b/src/components/NewAction/steps/SelectFactoryStep.tsx @@ -0,0 +1,99 @@ +import { Box, Typography, styled } from "@mui/material"; +import { BoxIcon, ChevronRightIcon } from "~/components/icons"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { ActionFactoryType } from "~/types/canon-guard"; +import { FACTORY_DISPLAY_NAMES } from "~/utils/factoryDisplay"; +import { Breadcrumb, FormSection } from "../shared"; +import type { FactoryType } from "./index"; + +interface FactoryOption { + id: FactoryType; + label: string; +} + +const FACTORY_OPTIONS: FactoryOption[] = [ + { id: "arbitrary-action", label: FACTORY_DISPLAY_NAMES[ActionFactoryType.ARBITRARY_ACTIONS]! }, + { id: "transfer", label: FACTORY_DISPLAY_NAMES[ActionFactoryType.SIMPLE_TRANSFERS]! }, + { id: "claim-allowance", label: FACTORY_DISPLAY_NAMES[ActionFactoryType.ALLOWANCE_CLAIMOR]! }, +]; + +interface SelectFactoryStepProps { + onSelectFactory: (factory: FactoryType) => void; + onNavigateToCreate: () => void; +} + +export const SelectFactoryStep = ({ onSelectFactory, onNavigateToCreate }: SelectFactoryStepProps) => { + return ( + + + + + + + {FACTORY_OPTIONS.map((option) => ( + onSelectFactory(option.id)}> + + + {option.label} + + + + ))} + + + + + ); +}; + +const Container = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: "32px 120px 64px", + width: "100%", + boxSizing: "border-box", +}); + +const ContentWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + width: "100%", + maxWidth: "576px", +}); + +const FactoryList = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "6px", + width: "100%", +}); + +const FactoryItem = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "16px", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + cursor: "pointer", + transition: "opacity 0.2s ease", + "&:hover": { + opacity: 0.85, + }, +}); + +const LeftContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const FactoryLabel = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent10, + textTransform: "uppercase", +}); diff --git a/src/components/NewAction/steps/SelectHubTypeStep.tsx b/src/components/NewAction/steps/SelectHubTypeStep.tsx new file mode 100644 index 0000000..29aed2d --- /dev/null +++ b/src/components/NewAction/steps/SelectHubTypeStep.tsx @@ -0,0 +1,97 @@ +import { Box, Typography, styled } from "@mui/material"; +import { Layers2Icon, ChevronRightIcon } from "~/components/icons"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { ActionFactoryType } from "~/types/canon-guard"; +import { HUB_DISPLAY_NAMES } from "~/utils/factoryDisplay"; +import { Breadcrumb, FormSection } from "../shared"; +import type { HubType } from "./index"; + +interface HubOption { + id: HubType; + label: string; +} + +const HUB_OPTIONS: HubOption[] = [ + { id: "capped-transfer-hub", label: `HUB: ${HUB_DISPLAY_NAMES[ActionFactoryType.CAPPED_TOKEN_TRANSFERS]}` }, +]; + +interface SelectHubTypeStepProps { + onSelectHub: (hub: HubType) => void; + onNavigateToCreate: () => void; +} + +export const SelectHubTypeStep = ({ onSelectHub, onNavigateToCreate }: SelectHubTypeStepProps) => { + return ( + + + + + + + {HUB_OPTIONS.map((option) => ( + onSelectHub(option.id)}> + + + {option.label} + + + + ))} + + + + + ); +}; + +const Container = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: "32px 120px 64px", + width: "100%", + boxSizing: "border-box", +}); + +const ContentWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + width: "100%", + maxWidth: "576px", +}); + +const HubList = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "6px", + width: "100%", +}); + +const HubItem = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "16px", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + cursor: "pointer", + transition: "opacity 0.2s ease", + "&:hover": { + opacity: 0.85, + }, +}); + +const LeftContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const HubLabel = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent10, + textTransform: "uppercase", +}); diff --git a/src/components/NewAction/steps/SigningFlowStep.tsx b/src/components/NewAction/steps/SigningFlowStep.tsx new file mode 100644 index 0000000..0e941d0 --- /dev/null +++ b/src/components/NewAction/steps/SigningFlowStep.tsx @@ -0,0 +1,1218 @@ +import { useState, useCallback, useEffect } from "react"; +import { Box, Typography, styled, keyframes, CircularProgress } from "@mui/material"; +import { NonceSelector, calculateRecommendedNonce } from "~/components/NonceSelector"; +import { + CheckIcon, + CheckCheckIcon, + Loader2Icon, + BoxIcon, + PlusIcon, + MinusIcon, + CircleDashedIcon, + ListIcon, + XIcon, +} from "~/components/icons"; +import { CopyableText } from "~/components/shared/CopyButton"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { useWallet, useStateContext, useNavigateWithParams } from "~/hooks"; +import type { QueueItem } from "~/services/queueService"; +import type { TransactionStep } from "~/services/transactionBuilderService"; +import { Breadcrumb, ParametersDisplay } from "../shared"; +import type { TransferFormData, ArbitraryActionFormData, CappedTransferHubFormData } from "./index"; + +interface SigningFlowStepProps { + steps: TransactionStep[]; + currentStepIndex: number; + formData?: TransferFormData | ArbitraryActionFormData | CappedTransferHubFormData; + onBack: () => void; + onNavigateToCreate: () => void; + onSimulateSign: (nonce?: number) => void; + isComplete: boolean; + /** Custom title for the action (overrides formData.title) */ + actionTitle?: string; + /** Custom factory type label (e.g., "TRANSFER", "ACTION") */ + factoryType?: string; + /** If true, hides the parameters section */ + hideParameters?: boolean; + /** Custom breadcrumb settings */ + breadcrumbPage?: string; + breadcrumbStandalone?: boolean; + /** Nonce selection props - if provided, enables nonce selection */ + nonceSelectionEnabled?: boolean; + /** Current Safe nonce (required if nonceSelectionEnabled) */ + currentSafeNonce?: number; + /** Queue items for nonce selection display */ + queueItems?: QueueItem[]; + /** If true, the nonce is locked (for items with existing signatures) */ + isNonceLocked?: boolean; + /** Locked nonce value when isNonceLocked is true */ + lockedNonce?: number; +} + +export const SigningFlowStep = ({ + steps, + currentStepIndex, + formData, + onNavigateToCreate, + onSimulateSign, + isComplete, + actionTitle, + factoryType = "TRANSFER", + hideParameters = false, + breadcrumbPage = "New Action", + breadcrumbStandalone = false, + nonceSelectionEnabled = false, + currentSafeNonce = 0, + queueItems = [], + isNonceLocked = false, + lockedNonce, +}: SigningFlowStepProps) => { + const [parametersExpanded, setParametersExpanded] = useState(false); + const [drawerOpen, setDrawerOpen] = useState(false); + const [isSigning, setIsSigning] = useState(false); + + // Nonce selection state + const recommendedNonce = calculateRecommendedNonce(currentSafeNonce, queueItems); + const [selectedNonce, setSelectedNonce] = useState( + isNonceLocked && lockedNonce !== undefined ? lockedNonce : recommendedNonce, + ); + + // Update selected nonce when recommended nonce changes (e.g., after initial load) + useEffect(() => { + if (!isNonceLocked && currentSafeNonce > 0) { + setSelectedNonce(recommendedNonce); + } + }, [recommendedNonce, isNonceLocked, currentSafeNonce]); + + // Navigation hook for completion buttons + const navigateWithParams = useNavigateWithParams(); + + // Derive title from props or formData + const displayTitle = actionTitle || formData?.title || "Untitled Action"; + + // Wallet state for chain verification at signing time + const { isConnected, connect, switchToChain, isOnCorrectChain, isSwitchingChain } = useWallet(); + const { chainId: appChainId } = useStateContext(); + + const signedCount = steps.filter((s) => s.status === "signed").length; + const totalCount = steps.length; + const progress = totalCount > 0 ? (signedCount / totalCount) * 100 : 0; + + const currentStep = steps[currentStepIndex]; + + /** + * Handle sign button click with wallet and chain verification + * Only prompts for chain switch at signing time (not on connect) + */ + const handleSignClick = useCallback(async () => { + // Step 1: Check if wallet is connected + if (!isConnected) { + connect(); + return; + } + + // Step 2: Check if on correct chain (only at signing time) + if (appChainId && !isOnCorrectChain(appChainId)) { + const switched = await switchToChain(appChainId); + if (!switched) { + // User rejected chain switch or error occurred + return; + } + } + + // Step 3: Proceed with signing (pass nonce if selection is enabled) + setIsSigning(true); + try { + if (nonceSelectionEnabled) { + await onSimulateSign(selectedNonce); + } else { + await onSimulateSign(); + } + } finally { + setIsSigning(false); + } + }, [ + isConnected, + connect, + isOnCorrectChain, + appChainId, + switchToChain, + onSimulateSign, + nonceSelectionEnabled, + selectedNonce, + ]); + + // Format hash for display (2 lines) + // Get transaction type label based on step id/title + const getTransactionTypeLabel = (step: TransactionStep) => { + const title = step.title.toLowerCase(); + if (title.includes("pre-approv") || title.includes("preapprov")) { + return `Pre-Approval: ${step.title.replace(/pre-?approval:?\s*/i, "")}`; + } + return `Transaction: ${step.title}`; + }; + + if (isComplete) { + return ( + <> + {/* Slide-out Drawer */} + setDrawerOpen(false)} /> + + + + Signed {steps.length} / {steps.length} + + setDrawerOpen(false)}> + + + + + + {displayTitle} + + CANON FACTORY + + {factoryType} + + + + {steps.map((step, index) => ( + + + {index + 1} + + + + + + + + {step.title.includes("Pre-Approval") ? "Pre-Approval:" : "Transaction:"} + + + {step.title.replace(/^(Pre-Approval|Transaction):\s*/, "").replace(/^Pre-Approval:\s*/, "")} + + + + + ))} + + + + + + + {/* Breadcrumb */} + + + {/* Action Preview Card */} + + + {displayTitle} + + CANON FACTORY + + {factoryType} + + + {!hideParameters && ( + + + + PARAMETERS + + + )} + + + {/* Status Bar */} + + + + STATUS + + {steps.length}/{steps.length} + + + + + + setDrawerOpen(true)}> + + + + + + + + {/* Sign Transaction Section */} + + SIGN TRANSACTION + + + + + + + Well done! + All requests have been signed successfully. + + + + + navigateWithParams("/canon-list")}>VIEW CANON LIST + navigateWithParams("/queue")}>VIEW QUEUE + + + + + + + + ); + } + + return ( + <> + {/* Slide-out Drawer */} + setDrawerOpen(false)} /> + + + + Signed {signedCount} / {totalCount} + + setDrawerOpen(false)}> + + + + + + {/* Action Info */} + + {displayTitle} + + CANON FACTORY + + {factoryType} + + + + {/* Transaction List */} + + {steps.map((step, index) => { + const typeLabel = getTransactionTypeLabel(step); + const colonIndex = typeLabel.indexOf(":"); + const prefix = colonIndex > -1 ? typeLabel.slice(0, colonIndex + 1) : ""; + const suffix = colonIndex > -1 ? typeLabel.slice(colonIndex + 1).trim() : typeLabel; + const isCurrentStep = index === currentStepIndex; + + return ( + + + {index + 1} + + + + {step.status === "signed" ? ( + + ) : isCurrentStep ? ( + + + + ) : ( + + )} + + + {prefix} + {suffix} + + + + ); + })} + + + + + {/* Main Content */} + + + {/* Breadcrumb */} + + + {/* Action Preview Card */} + + + {displayTitle} + + CANON FACTORY + + {factoryType} + + + + {/* Parameters Toggle - only shown when hideParameters is false */} + {!hideParameters && + (formData ? ( + <> + setParametersExpanded(!parametersExpanded)}> + + {parametersExpanded ? ( + + ) : ( + + )} + PARAMETERS + + + + {parametersExpanded && } + + ) : ( + + + + PARAMETERS + + + ))} + + + {/* Status Bar */} + + + + STATUS + + {signedCount}/{totalCount} + + + + + + + + + + setDrawerOpen(true)}> + + + + + + + + + {/* Nonce Selection (if enabled) */} + {nonceSelectionEnabled && currentStep?.id?.startsWith("sign") && ( + + )} + + {/* Sign Transaction Section */} + + SIGN TRANSACTION + + {/* Header Card */} + + + {currentStep?.status === "waiting" ? ( + + + + ) : ( + + )} + + + Transaction: + {currentStep?.title} + + + + {/* Details Card Container */} + + + {/* Details Content */} + + + Address + {currentStep?.to ? ( + + {currentStep.to} + + ) : ( + + )} + + + + Hash + {currentStep?.data ? ( + + {currentStep.data} + + ) : ( + + )} + + + + {/* Sign Button */} + + {currentStep?.status === "waiting" ? ( + Confirm in Wallet... + ) : isSwitchingChain ? ( + Switching Network... + ) : !isConnected ? ( + CONNECT WALLET + ) : isSigning ? ( + + + Waiting for wallet... + + ) : ( + SIGN + )} + + + + + + + + + ); +}; + +// Styled Components +const Container = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: "32px 120px 64px", + width: "100%", + boxSizing: "border-box", +}); + +const ContentWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + width: "100%", + maxWidth: "576px", +}); + +// Drawer Styles +const DrawerOverlay = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isOpen", +})<{ $isOpen: boolean }>(({ $isOpen }) => ({ + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "rgba(0, 0, 0, 0.5)", + opacity: $isOpen ? 1 : 0, + visibility: $isOpen ? "visible" : "hidden", + transition: "opacity 0.3s ease, visibility 0.3s ease", + zIndex: 1000, +})); + +const DrawerPanel = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isOpen", +})<{ $isOpen: boolean }>(({ $isOpen }) => ({ + position: "fixed", + top: "12px", + right: "12px", + bottom: "12px", + width: "440px", + backgroundColor: canonHeaderTokens.background.layer1, + transform: $isOpen ? "translateX(0)" : "translateX(calc(100% + 24px))", + transition: "transform 0.3s ease", + zIndex: 1001, + display: "flex", + flexDirection: "column", + gap: "32px", + boxShadow: "-4px 0 24px rgba(0, 0, 0, 0.3)", + borderRadius: "16px", + padding: "24px", +})); + +const DrawerHeader = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "0 8px", +}); + +const DrawerTitle = styled(Typography)({ + fontSize: "18px", + fontWeight: 600, + lineHeight: "28px", + color: canonHeaderTokens.foreground.accent0, +}); + +const CloseButton = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "8px", + background: "none", + border: "none", + borderRadius: "100px", + cursor: "pointer", + transition: "background-color 0.2s ease", + "&:hover": { + backgroundColor: canonHeaderTokens.foreground.accent40 + "40", + }, +}); + +const DrawerContent = styled(Box)({ + display: "flex", + flexDirection: "column", + flex: 1, + overflow: "auto", +}); + +const DrawerActionInfo = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + padding: "16px 8px", +}); + +const DrawerActionTitle = styled(Typography)({ + fontSize: "18px", + fontWeight: 600, + lineHeight: "28px", + color: canonHeaderTokens.foreground.accent0, +}); + +const DrawerFactoryInfo = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const DrawerTransactionList = styled(Box)({ + display: "flex", + flexDirection: "column", +}); + +const DrawerTransactionItem = styled(Box)({ + display: "flex", + alignItems: "stretch", + borderTop: `1px solid ${canonHeaderTokens.foreground.accent50}`, +}); + +const StepNumberArea = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "flex-end", + padding: "0 8px", + backgroundColor: canonHeaderTokens.background.layer1, +}); + +const StepNumberBadge = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "48px", + height: "28px", + borderRadius: "100px", + backgroundColor: canonHeaderTokens.background.layer1Variation, + fontSize: "11px", + fontWeight: 600, + color: canonHeaderTokens.foreground.accent10, + flexShrink: 0, +}); + +const StepContentArea = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "16px", + flex: 1, + padding: "16px 9px", + backgroundColor: canonHeaderTokens.background.layer1, +}); + +const DrawerStepIndicator = styled(Box, { + shouldForwardProp: (prop) => prop !== "$status", +})<{ $status: string }>(({ $status }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "20px", + height: "20px", + borderRadius: "50%", + backgroundColor: $status === "signed" ? canonHeaderTokens.brand.green : "transparent", + flexShrink: 0, +})); + +const DrawerTransactionLabelRow = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "6px", +}); + +const DrawerTransactionLabelBold = styled(Typography)({ + fontSize: "14px", + fontWeight: 600, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent0, +}); + +const DrawerTransactionLabelNormal = styled(Typography)({ + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent0, +}); + +// Action Preview Card Styles +const ActionPreviewCard = styled(Box)({ + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + overflow: "hidden", +}); + +const PreviewHeader = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "20px", + padding: "16px", +}); + +const ActionTitle = styled(Typography)({ + fontSize: "18px", + fontWeight: 600, + lineHeight: "28px", + color: canonHeaderTokens.foreground.accent0, +}); + +const FactoryInfo = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const FactoryLabel = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const FactoryValue = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, + textTransform: "uppercase", +}); + +const ParametersToggle = styled(Box)({ + display: "flex", + flexDirection: "column", + height: "36px", + backgroundColor: canonHeaderTokens.background.layer1Variation, + borderTop: `0.5px solid ${canonHeaderTokens.foreground.accent50}`, + cursor: "pointer", + "&:hover": { + opacity: 0.9, + }, +}); + +const ToggleContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", + padding: "12px", +}); + +const ToggleLabel = styled(Typography)({ + fontSize: "11px", + fontWeight: 600, + lineHeight: "12px", + color: canonHeaderTokens.foreground.accent30, + textTransform: "uppercase", +}); + +// Status Bar Styles +const StatusSection = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "center", + position: "relative", +}); + +const StatusBarMain = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + width: "100%", + padding: "16px", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + zIndex: 1, +}); + +const StatusLeft = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "16px", +}); + +const StatusLabel = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const StatusCounter = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, + fontFamily: "monospace", +}); + +const StatusIndicator = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "0", +}); + +const StatusDot = styled(Box)({ + width: "6px", + height: "6px", + borderRadius: "50%", + backgroundColor: canonHeaderTokens.foreground.accent20, + marginRight: "-3px", + zIndex: 1, +}); + +const ProgressBarSmall = styled(Box)({ + width: "100px", + height: "6px", + backgroundColor: canonHeaderTokens.foreground.accent40, + borderRadius: "100px", + overflow: "hidden", +}); + +const ProgressFill = styled(Box, { + shouldForwardProp: (prop) => prop !== "$progress", +})<{ $progress: number }>(({ $progress }) => ({ + width: `${$progress}%`, + height: "100%", + backgroundColor: canonHeaderTokens.foreground.accent20, + borderRadius: "100px", + transition: "width 0.3s ease", +})); + +const StatusBar = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + width: "576px", + padding: "16px", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + zIndex: 1, +}); + +const StatusCount = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, + fontFamily: "'JetBrains Mono', monospace", +}); + +const ProgressBarContainer = styled(Box)({ + width: "100px", + height: "6px", + backgroundColor: canonHeaderTokens.foreground.accent40, + borderRadius: "100px", + overflow: "hidden", +}); + +const ProgressBarFill = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isComplete", +})<{ $isComplete?: boolean }>(({ $isComplete }) => ({ + width: "100%", + height: "100%", + backgroundColor: $isComplete ? canonHeaderTokens.brand.green : canonHeaderTokens.foreground.accent20, + borderRadius: "100px", +})); + +const StatusShadow1 = styled(Box)({ + width: "544px", + height: "6px", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "0 0 8px 8px", + opacity: 0.5, +}); + +const StatusShadow2 = styled(Box)({ + width: "512px", + height: "6px", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "0 0 8px 8px", + opacity: 0.2, +}); + +const StatusRight = styled(Box)({ + display: "flex", + alignItems: "center", +}); + +const ExpandButton = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "center", + background: "none", + border: "none", + padding: "4px", + cursor: "pointer", + borderRadius: "4px", + "&:hover": { + backgroundColor: canonHeaderTokens.foreground.accent40 + "40", + }, +}); + +// Sign Transaction Section Styles +const SignTransactionSection = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "6px", + width: "100%", +}); + +const SectionLabel = styled(Typography)({ + fontSize: "11px", + fontWeight: 600, + lineHeight: "12px", + color: canonHeaderTokens.foreground.accent30, + textTransform: "uppercase", + padding: "8px", +}); + +const SignTransactionCardWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + position: "relative", + isolation: "isolate", + width: "576px", +}); + +const SignItemHeader = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "16px", + padding: "20px 24px", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + boxShadow: "0px 20px 25px -5px rgba(0,0,0,0.1), 0px 8px 10px -6px rgba(0,0,0,0.1)", + position: "relative", + zIndex: 2, +}); + +const SignItemIcon = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isWaiting", +})<{ $isWaiting: boolean }>({ + display: "flex", + alignItems: "center", + justifyContent: "center", +}); + +const SignItemTitleRow = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "6px", +}); + +const SignItemTitleBold = styled(Typography)({ + fontSize: "16px", + fontWeight: 600, + lineHeight: "24px", + color: canonHeaderTokens.foreground.accent0, +}); + +const SignItemTitleNormal = styled(Typography)({ + fontSize: "16px", + fontWeight: 400, + lineHeight: "24px", + color: canonHeaderTokens.foreground.accent0, +}); + +const SignItemDetailsWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + padding: "0 6px", + position: "relative", + zIndex: 1, + width: "100%", +}); + +const SignItemDetailsCard = styled(Box)({ + display: "flex", + flexDirection: "column", + borderRadius: "0 0 16px 16px", + overflow: "hidden", + width: "100%", +}); + +const SignItemDetails = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "16px", + padding: "24px", + backgroundColor: canonHeaderTokens.background.layer1Variation, + width: "100%", + boxSizing: "border-box", +}); + +const DetailRow = styled(Box)({ + display: "flex", + alignItems: "flex-start", + gap: "4px", + width: "100%", + minWidth: 0, +}); + +const DetailLabel = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, + width: "84px", + flexShrink: 0, + whiteSpace: "pre-wrap", +}); + +const DetailValue = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent10, +}); + +const HashValue = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent10, + wordBreak: "break-all", + maxWidth: "100%", + overflow: "hidden", +}); + +const DetailDivider = styled(Box)({ + width: "100%", + height: "0.5px", + backgroundColor: canonHeaderTokens.foreground.accent40, +}); + +const SignButtonWrapper = styled(Box)({ + padding: "20px", + backgroundColor: canonHeaderTokens.background.layer1, +}); + +const SignButton = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "100%", + height: "44px", + padding: "12px 24px", + borderRadius: "100px", + backgroundColor: canonHeaderTokens.brand.green, + color: "#ffffff", + fontSize: "14px", + fontWeight: 600, + lineHeight: "20px", + letterSpacing: "0.6px", + textTransform: "uppercase", + border: "none", + cursor: "pointer", + transition: "opacity 0.2s ease", + "&:hover": { + opacity: 0.9, + }, +}); + +const WaitingButton = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "100%", + height: "44px", + padding: "12px 24px", + borderRadius: "100px", + backgroundColor: "transparent", + color: canonHeaderTokens.foreground.accent20, + fontSize: "14px", + fontWeight: 500, + lineHeight: "20px", + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, + cursor: "default", + "&:disabled": { + opacity: 0.8, + }, +}); + +// Success State Styles +const SuccessCardWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + width: "576px", + borderRadius: "8px", + overflow: "hidden", +}); + +const SuccessContent = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "center", + gap: "20px", + padding: "48px 24px", + backgroundColor: canonHeaderTokens.background.layer1, +}); + +const SuccessIconWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "56px", + height: "56px", + borderRadius: "1000px", + backgroundColor: canonHeaderTokens.brand.green, +}); + +const SuccessTextContainer = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "center", + gap: "6px", +}); + +const SuccessTitle = styled(Typography)({ + fontSize: "18px", + fontWeight: 600, + lineHeight: "28px", + color: canonHeaderTokens.foreground.accent0, +}); + +const SuccessSubtext = styled(Typography)({ + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent20, + textAlign: "center", + width: "160px", +}); + +const SuccessButtonSection = styled(Box)({ + display: "flex", + flexDirection: "column", + padding: "20px", + backgroundColor: canonHeaderTokens.background.layer1, + borderTop: `1px dashed ${canonHeaderTokens.foreground.accent50}`, +}); + +const SuccessButtonRow = styled(Box)({ + display: "flex", + gap: "12px", + width: "100%", +}); + +const OutlineButton = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "center", + flex: 1, + height: "44px", + padding: "8px 20px", + borderRadius: "100px", + backgroundColor: "transparent", + color: canonHeaderTokens.foreground.accent10, + fontSize: "12px", + fontWeight: 600, + lineHeight: "16px", + letterSpacing: "0.6px", + textTransform: "uppercase", + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, + cursor: "pointer", + transition: "opacity 0.2s ease", + "&:hover": { + opacity: 0.8, + backgroundColor: canonHeaderTokens.foreground.accent40 + "20", + }, +}); + +const GreenButton = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "center", + flex: 1, + height: "44px", + padding: "8px 20px", + borderRadius: "100px", + backgroundColor: canonHeaderTokens.brand.green, + color: "#ffffff", + fontSize: "12px", + fontWeight: 600, + lineHeight: "16px", + letterSpacing: "0.6px", + textTransform: "uppercase", + border: "none", + cursor: "pointer", + transition: "opacity 0.2s ease", + "&:hover": { + opacity: 0.9, + }, +}); + +const spin = keyframes` + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +`; + +const SpinningLoader = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + animation: `${spin} 1s linear infinite`, +}); diff --git a/src/components/NewAction/steps/TransferFormStep.tsx b/src/components/NewAction/steps/TransferFormStep.tsx new file mode 100644 index 0000000..c945bde --- /dev/null +++ b/src/components/NewAction/steps/TransferFormStep.tsx @@ -0,0 +1,305 @@ +import { useMemo } from "react"; +import { Box, Typography, styled } from "@mui/material"; +import { BoxIcon, AsteriskIcon } from "~/components/icons"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { ActionFactoryType } from "~/types/canon-guard"; +import { FACTORY_DISPLAY_NAMES } from "~/utils/factoryDisplay"; +import { isValidAddress, isValidAmount } from "~/utils/validation"; +import { + Breadcrumb, + FormSection, + FormInput, + ActionButton, + ItemsCard, + ItemSection, + ItemDividerHeader, + ItemFieldsSection, + AddItemRow, + ActionButtonRow, + ButtonsContainer, +} from "../shared"; +import type { TransferFormData, TransferItem } from "./index"; + +interface TransferFormStepProps { + formData: TransferFormData; + onFormDataChange: (data: TransferFormData) => void; + onContinue: () => void; + onBack: () => void; + onNavigateToCreate: () => void; + onChangeFactory: () => void; +} + +export const TransferFormStep = ({ + formData, + onFormDataChange, + onContinue, + onBack, + onNavigateToCreate, + onChangeFactory, +}: TransferFormStepProps) => { + // Update title field + const updateTitle = (value: string) => { + onFormDataChange({ ...formData, title: value }); + }; + + // Update a specific transfer item + const updateTransfer = (index: number, field: keyof TransferItem, value: string) => { + const newTransfers = [...formData.transfers]; + newTransfers[index] = { ...newTransfers[index], [field]: value }; + onFormDataChange({ ...formData, transfers: newTransfers }); + }; + + // Add a new transfer item + const addTransfer = () => { + onFormDataChange({ + ...formData, + transfers: [...formData.transfers, { tokenAddress: "", recipientAddress: "", amount: "" }], + }); + }; + + // Remove a transfer item + const removeTransfer = (index: number) => { + const newTransfers = formData.transfers.filter((_, i) => i !== index); + onFormDataChange({ ...formData, transfers: newTransfers }); + }; + + // Validation errors for each transfer item + const errors = useMemo(() => { + return formData.transfers.map((transfer) => ({ + tokenAddress: + transfer.tokenAddress.trim() && !isValidAddress(transfer.tokenAddress) ? "Invalid address format" : undefined, + recipientAddress: + transfer.recipientAddress.trim() && !isValidAddress(transfer.recipientAddress) + ? "Invalid address format" + : undefined, + amount: transfer.amount.trim() && !isValidAmount(transfer.amount) ? "Must be a valid number" : undefined, + })); + }, [formData.transfers]); + + const hasErrors = errors.some((e) => e.tokenAddress || e.recipientAddress || e.amount); + + const isValid = + formData.title.trim() !== "" && + formData.transfers.length > 0 && + formData.transfers.every( + (transfer) => + transfer.tokenAddress.trim() !== "" && transfer.recipientAddress.trim() !== "" && transfer.amount.trim() !== "", + ) && + !hasErrors; + + return ( + + + + + {/* Set Action Details Section */} + + {/* Factory Selector */} + + + CANON FACTORY + + {FACTORY_DISPLAY_NAMES[ActionFactoryType.SIMPLE_TRANSFERS]} + + CHANGE + + + {/* Transaction Title Card */} + + + + + Public + + + + + You can encrypt transaction titles to keep them private. Learn more + + + + + + + {/* Set Action Parameters Section */} + + + {/* Transfer Items */} + {formData.transfers.map((transfer, index) => ( + + {/* Divider header with transfer number and remove button */} + 1} + onRemove={() => removeTransfer(index)} + /> + + {/* Transfer Fields */} + + updateTransfer(index, "tokenAddress", value)} + error={errors[index]?.tokenAddress} + /> + updateTransfer(index, "recipientAddress", value)} + error={errors[index]?.recipientAddress} + /> + updateTransfer(index, "amount", value)} + error={errors[index]?.amount} + /> + + + ))} + + {/* Info Row with ADD TRANSFER button */} + + + {/* Action Buttons Row */} + + + + BACK + + + CONTINUE + + + + + + + + ); +}; + +const Container = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: "32px 120px 64px", + width: "100%", + boxSizing: "border-box", +}); + +const ContentWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + width: "100%", + maxWidth: "576px", +}); + +const FactorySelector = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "16px", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + cursor: "pointer", + transition: "opacity 0.2s ease", + "&:hover": { + opacity: 0.85, + }, +}); + +const LeftContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const FactoryLabel = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const FactoryValue = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent10, + textTransform: "uppercase", +}); + +const ChangeButton = styled(Typography)({ + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const TransactionTitleCard = styled(Box)({ + display: "flex", + flexDirection: "column", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + overflow: "hidden", + position: "relative", +}); + +const CardContent = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + padding: "24px", +}); + +const FormInputWrapper = styled(Box)({ + position: "relative", +}); + +const PublicBadge = styled(Box)({ + position: "absolute", + top: "42px", + right: "14px", + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "6px 12px", + borderRadius: "100px", + border: `1px solid ${canonHeaderTokens.amber.border}`, + fontSize: "11px", + fontWeight: 400, + lineHeight: "12px", + color: canonHeaderTokens.amber.text, +}); + +const EncryptionNote = styled(Box)({ + display: "flex", + alignItems: "flex-start", + gap: "6px", +}); + +const NoteText = styled(Typography)({ + fontSize: "13px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent10, +}); + +const LearnMoreLink = styled("span")({ + textDecoration: "underline", + cursor: "pointer", +}); diff --git a/src/components/NewAction/steps/index.ts b/src/components/NewAction/steps/index.ts new file mode 100644 index 0000000..5e1f71c --- /dev/null +++ b/src/components/NewAction/steps/index.ts @@ -0,0 +1,73 @@ +// Re-export time unit types from centralized file +export type { EpochTimeUnit as TimeUnit } from "~/utils/timeUnits"; +export { EPOCH_TIME_MULTIPLIERS } from "~/utils/timeUnits"; + +import type { EpochTimeUnit } from "~/utils/timeUnits"; + +export { SelectFactoryStep } from "./SelectFactoryStep"; +export { TransferFormStep } from "./TransferFormStep"; +export { ArbitraryActionFormStep } from "./ArbitraryActionFormStep"; +export { ClaimAllowanceFormStep } from "./ClaimAllowanceFormStep"; +export { ReviewDeployStep } from "./ReviewDeployStep"; +export { SigningFlowStep } from "./SigningFlowStep"; + +// Hub-specific step components +export { SelectHubTypeStep } from "./SelectHubTypeStep"; +export { CappedTransferHubFormStep } from "./CappedTransferHubFormStep"; +export { HubReviewStep } from "./HubReviewStep"; + +// Hub child deployment components +export { DeployHubChildFormStep } from "./DeployHubChildFormStep"; +export type { HubChildFormData } from "./DeployHubChildFormStep"; +export { DeployHubChildReviewStep } from "./DeployHubChildReviewStep"; + +// Types for individual items (used in arrays) +export type TransferItem = { + tokenAddress: string; + recipientAddress: string; + amount: string; +}; + +export type ArbitraryActionItem = { + target: string; + signature: string; + data: string; + value: string; +}; + +// Form data types (contain arrays of items) +export type TransferFormData = { + title: string; + transfers: TransferItem[]; +}; + +export type ArbitraryActionFormData = { + title: string; + actions: ArbitraryActionItem[]; +}; + +export type ClaimAllowanceFormData = { + title: string; + token: string; + tokenOwner: string; + tokenRecipient: string; +}; + +export type FactoryType = + | "arbitrary-action" + | "transfer" + | "claim-allowance" + | "pre-approve" + | "turn-off-emergency" + | null; + +// Hub types +export type HubType = "capped-transfer-hub" | null; + +export type CappedTransferHubFormData = { + title: string; + recipientAddress: string; + epochLength: string; + epochUnit: EpochTimeUnit; + tokens: Array<{ address: string; amount: string }>; +}; diff --git a/src/components/NoGuardChoiceScreen.tsx b/src/components/NoGuardChoiceScreen.tsx new file mode 100644 index 0000000..6696255 --- /dev/null +++ b/src/components/NoGuardChoiceScreen.tsx @@ -0,0 +1,186 @@ +import { Box, styled } from "@mui/material"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { SafeInfo } from "~/types"; +import { HeaderLogo } from "./Header"; +import { CircleFadingPlusIcon, Link2Icon } from "./icons"; +import { SafeProfileCard } from "./shared/SafeProfileCard"; +import { + PageContainer, + SetupHeader, + SetupContentArea, + SetupFormWrapper, + SetupSectionTitle, +} from "./shared/StyledComponents"; + +interface NoGuardChoiceScreenProps { + safeInfo: SafeInfo; + onDeployNew: () => void; + onUseExisting: () => void; + onBack: () => void; +} + +export const NoGuardChoiceScreen = ({ safeInfo, onDeployNew, onUseExisting, onBack }: NoGuardChoiceScreenProps) => { + return ( + + + + + + + + Add New Safe Account + + {/* Safe Profile Card */} + + + {/* Choice Card */} + + + + It looks like your Safe doesn't have a Canon Guard set. Choose how you'd like to proceed: + + + + + + + + + + Deploy New Canon Guard + + Create and deploy a new Canon Guard for your Safe with custom parameters. + + + + + + + + + + Use Existing Canon Guard + + Connect to an already deployed Canon Guard without attaching it to your Safe. + + + + + + + BACK + + + + + + ); +}; + +// Choice Card +const ChoiceCard = styled(Box)({ + display: "flex", + flexDirection: "column", + borderRadius: "8px", + overflow: "hidden", + backgroundColor: canonHeaderTokens.background.layer1, +}); + +const InfoSection = styled(Box)({ + padding: "24px", +}); + +const InfoText = styled("p")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent10, + margin: 0, +}); + +const OptionsSection = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + padding: "0 24px 24px", +}); + +const OptionButton = styled("button")({ + display: "flex", + alignItems: "flex-start", + gap: "16px", + padding: "16px", + backgroundColor: canonHeaderTokens.background.layer1Variation, + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, + borderRadius: "8px", + cursor: "pointer", + textAlign: "left", + transition: "all 0.2s ease", + "&:hover": { + borderColor: canonHeaderTokens.foreground.accent20, + backgroundColor: canonHeaderTokens.background.layer0, + }, +}); + +const OptionIconWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "40px", + height: "40px", + borderRadius: "8px", + backgroundColor: canonHeaderTokens.background.layer1, + flexShrink: 0, +}); + +const OptionContent = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "4px", +}); + +const OptionTitle = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 600, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent0, +}); + +const OptionDescription = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const ButtonSection = styled(Box)({ + display: "flex", + justifyContent: "center", + padding: "24px", + borderTop: `1px dashed ${canonHeaderTokens.foreground.accent50}`, +}); + +const BackButton = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "center", + flex: 1, + height: "36px", + padding: "8px 20px", + borderRadius: "100px", + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, + background: "transparent", + cursor: "pointer", + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 600, + letterSpacing: "0.6px", + textTransform: "uppercase", + color: canonHeaderTokens.foreground.accent10, + "&:hover": { + backgroundColor: `${canonHeaderTokens.foreground.accent40}20`, + }, +}); diff --git a/src/components/NonceSelector/index.tsx b/src/components/NonceSelector/index.tsx new file mode 100644 index 0000000..73404d1 --- /dev/null +++ b/src/components/NonceSelector/index.tsx @@ -0,0 +1,314 @@ +/** + * NonceSelector Component + * + * Allows users to select which Safe nonce to sign a transaction at. + * Shows existing queued transactions per nonce and highlights the recommended nonce. + */ + +import { useMemo } from "react"; +import { styled, Box, Typography, Select, MenuItem, SelectChangeEvent, alpha } from "@mui/material"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { QueueItem } from "~/services/queueService"; + +/** + * Represents a nonce option in the dropdown + */ +export interface NonceOption { + nonce: number; + label?: string; // Label of the transaction at this nonce (if any) + isRecommended: boolean; + isUsed: boolean; // Whether there's already a transaction at this nonce + approversCount?: number; // Number of signers for existing tx + threshold?: number; // Safe threshold for existing tx +} + +export interface NonceSelectorProps { + /** Current Safe nonce (minimum selectable) */ + currentNonce: number; + /** Recommended nonce (next available after queue) */ + recommendedNonce: number; + /** Currently selected nonce */ + selectedNonce: number; + /** Callback when nonce changes */ + onNonceChange: (nonce: number) => void; + /** If true, the nonce is locked and cannot be changed */ + isReadOnly?: boolean; + /** Queue items to show in dropdown */ + queueItems?: QueueItem[]; + /** Maximum future nonce to show (defaults to recommended + 5) */ + maxFutureNonces?: number; +} + +// Styled components +const Container = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "8px", + marginBottom: "16px", +}); + +const Label = styled(Typography)({ + fontSize: "11px", + fontWeight: 600, + lineHeight: "12px", + color: canonHeaderTokens.foreground.accent30, + textTransform: "uppercase", + padding: "8px", +}); + +const StyledSelect = styled(Select)(() => ({ + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "8px", + color: canonHeaderTokens.foreground.accent0, + width: "fit-content", + minWidth: "100px", + "& .MuiOutlinedInput-notchedOutline": { + borderColor: `${alpha(canonHeaderTokens.foreground.accent10, 0.3)}`, + }, + "&:hover .MuiOutlinedInput-notchedOutline": { + borderColor: canonHeaderTokens.foreground.accent10, + }, + "&.Mui-focused .MuiOutlinedInput-notchedOutline": { + borderColor: `${alpha(canonHeaderTokens.foreground.accent10, 0.3)}`, + }, + "& .MuiSelect-icon": { + color: canonHeaderTokens.foreground.accent10, + }, +})); + +const StyledMenuItem = styled(MenuItem, { + shouldForwardProp: (prop) => prop !== "isRecommended" && prop !== "isUsed", +})<{ isRecommended?: boolean; isUsed?: boolean }>(({ isRecommended }) => ({ + display: "flex", + justifyContent: "space-between", + alignItems: "center", + gap: "16px", + padding: "10px 16px", + backgroundColor: isRecommended ? alpha(canonHeaderTokens.brand.green, 0.1) : "transparent", + "&:hover": { + backgroundColor: isRecommended + ? alpha(canonHeaderTokens.brand.green, 0.15) + : alpha(canonHeaderTokens.foreground.accent10, 0.1), + }, + "&.Mui-selected": { + backgroundColor: isRecommended + ? alpha(canonHeaderTokens.brand.green, 0.2) + : alpha(canonHeaderTokens.foreground.accent10, 0.15), + "&:hover": { + backgroundColor: isRecommended + ? alpha(canonHeaderTokens.brand.green, 0.25) + : alpha(canonHeaderTokens.foreground.accent10, 0.2), + }, + }, +})); + +const NonceNumber = styled(Typography)({ + fontFamily: "monospace", + fontSize: "14px", + fontWeight: 600, + color: canonHeaderTokens.foreground.accent0, +}); + +const TxLabel = styled(Typography)({ + fontSize: "13px", + color: canonHeaderTokens.foreground.accent10, + marginLeft: "12px", + maxWidth: "200px", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", +}); + +const RecommendedBadge = styled(Box)({ + backgroundColor: alpha(canonHeaderTokens.brand.green, 0.2), + color: canonHeaderTokens.brand.green, + fontSize: "10px", + fontWeight: 600, + padding: "2px 6px", + borderRadius: "4px", + textTransform: "uppercase", + letterSpacing: "0.5px", + flexShrink: 0, +}); + +const ReadOnlyText = styled(Typography)({ + fontFamily: "monospace", + fontSize: "14px", + fontWeight: 500, + color: canonHeaderTokens.foreground.accent0, + backgroundColor: canonHeaderTokens.background.layer1, + padding: "12px 16px", + borderRadius: "8px", + border: `1px solid ${alpha(canonHeaderTokens.foreground.accent10, 0.3)}`, +}); + +/** + * Build nonce options from queue items + * When multiple txs exist for same nonce, picks the one with most signatures (tie-breaker: earliest executableAt) + */ +export function buildNonceOptions( + currentNonce: number, + recommendedNonce: number, + queueItems: QueueItem[], + maxFutureNonces: number = 5, +): NonceOption[] { + // Filter to only include items with at least 1 signature + // Items with 0 signatures haven't been assigned a nonce yet (just queued, not signed) + const signedItems = queueItems.filter((item) => item.approversCount > 0); + + // Group queue items by nonce + const nonceToItems = new Map(); + for (const item of signedItems) { + const existing = nonceToItems.get(item.nonce) || []; + existing.push(item); + nonceToItems.set(item.nonce, existing); + } + + // For each nonce with items, pick the best one (most signatures, then earliest executableAt) + const nonceToItem = new Map(); + for (const [nonce, items] of nonceToItems) { + const sorted = items.sort((a, b) => { + // First by approversCount descending + if (b.approversCount !== a.approversCount) { + return b.approversCount - a.approversCount; + } + // Then by executableAt ascending (earlier first) + return a.executableAt.getTime() - b.executableAt.getTime(); + }); + nonceToItem.set(nonce, sorted[0]); + } + + // Build options from currentNonce to recommended + maxFutureNonces + const maxNonce = recommendedNonce + maxFutureNonces; + const options: NonceOption[] = []; + + for (let nonce = currentNonce; nonce <= maxNonce; nonce++) { + const existingItem = nonceToItem.get(nonce); + options.push({ + nonce, + label: existingItem?.label, + isRecommended: nonce === recommendedNonce, + isUsed: !!existingItem, + approversCount: existingItem?.approversCount, + threshold: existingItem?.threshold, + }); + } + + return options; +} + +/** + * Calculate the recommended nonce + * Finds the first available (empty) nonce starting from currentNonce + * Only considers items with at least 1 signature as "occupied" + */ +export function calculateRecommendedNonce(currentNonce: number, queueItems: QueueItem[]): number { + // Get set of occupied nonces (only signed items count as occupying a nonce) + const occupiedNonces = new Set(queueItems.filter((item) => item.approversCount > 0).map((item) => item.nonce)); + + // Find first empty nonce starting from currentNonce + let nonce = currentNonce; + while (occupiedNonces.has(nonce)) { + nonce++; + } + return nonce; +} + +export function NonceSelector({ + currentNonce, + recommendedNonce, + selectedNonce, + onNonceChange, + isReadOnly = false, + queueItems = [], + maxFutureNonces = 5, +}: NonceSelectorProps) { + const nonceOptions = useMemo( + () => buildNonceOptions(currentNonce, recommendedNonce, queueItems, maxFutureNonces), + [currentNonce, recommendedNonce, queueItems, maxFutureNonces], + ); + + const handleChange = (event: SelectChangeEvent) => { + onNonceChange(event.target.value as number); + }; + + // Find the selected option to display info + const selectedOption = nonceOptions.find((opt) => opt.nonce === selectedNonce); + + if (isReadOnly) { + return ( + + + + # {selectedNonce} + {selectedOption?.label && ( + + ({selectedOption.label}) + + )} + + + ); + } + + return ( + + + # {value}} + MenuProps={{ + disableScrollLock: true, + anchorOrigin: { + vertical: "bottom", + horizontal: "left", + }, + transformOrigin: { + vertical: "top", + horizontal: "left", + }, + sx: { + "& .MuiBackdrop-root": { + opacity: "0 !important", + }, + }, + PaperProps: { + sx: { + backgroundColor: canonHeaderTokens.background.layer1, + border: `1px solid ${alpha(canonHeaderTokens.foreground.accent10, 0.3)}`, + borderRadius: "8px", + "& .MuiList-root": { + padding: 0, + }, + }, + }, + }} + > + {nonceOptions.map((option) => ( + + + # {option.nonce} + {option.isUsed && option.label && ( + + {option.label} + {option.approversCount !== undefined && + option.threshold !== undefined && + ` (${option.approversCount}/${option.threshold} sigs)`} + + )} + + {option.isRecommended && Recommended} + + ))} + + + ); +} + +export default NonceSelector; diff --git a/src/components/QueueActionSection/index.tsx b/src/components/QueueActionSection/index.tsx new file mode 100644 index 0000000..73ae8ea --- /dev/null +++ b/src/components/QueueActionSection/index.tsx @@ -0,0 +1,389 @@ +import { useState, useCallback, useEffect, useMemo } from "react"; +import { Box, CircularProgress, styled } from "@mui/material"; +import { useLocation } from "react-router-dom"; +import { Address, Hex, encodeFunctionData } from "viem"; +import { canonGuardAbi, safeAbi, preApproveActionFactoryAbi } from "~/abis/canonGuard"; +import { getRpcUrlForChain, getViemChain } from "~/config/chains"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { PRE_APPROVE_ACTION_FACTORY } from "~/constants/canonGuard"; +import { useNavigateWithParams, useTransactionExecutor } from "~/hooks"; +import { useStateContext } from "~/hooks/useStateContext"; +import { ClientService } from "~/services/clientService"; +import { QueueService, type QueueItem } from "~/services/queueService"; +import type { TransactionStep } from "~/services/transactionBuilderService"; +import { SigningFlowStep } from "../NewAction/steps"; + +interface QueueActionState { + actionBuilderAddress: Address; + label: string; + factoryType: string; + /** For pre-approve mode: approval duration in seconds */ + approvalDuration?: bigint; +} + +interface QueueActionSectionProps { + onQueueCountChange?: () => void; +} + +/** + * QueueActionSection - Handles queuing/pre-approving existing actions from Canon List + * + * This component reuses the SigningFlowStep to provide a consistent UX. + * + * Supports two modes (determined by presence of approvalDuration in state): + * - Queue mode: Queue action + Sign (2 steps) + * - Pre-approve mode: Deploy pre-approve + Queue + Sign (3 steps) + * + * Note: This flow requires navigation state. If state is missing (e.g., on page refresh), + * it redirects back to canon-list to restart the flow. + */ +export const QueueActionSection = ({ onQueueCountChange }: QueueActionSectionProps) => { + const location = useLocation(); + const navigateWithParams = useNavigateWithParams(); + const { safeAddress, guardAddress, chainId } = useStateContext(); + + // Get state passed from Canon List (required - redirects if missing) + const navigationState = location.state as QueueActionState | null; + + // Determine if we're in pre-approve mode based on presence of approvalDuration (includes 0n for removal) + const isPreApproveMode = navigationState?.approvalDuration !== undefined; + + // Determine if this is a removal (duration is 0) + const isRemovePreApproval = isPreApproveMode && navigationState?.approvalDuration === 0n; + + // Transaction executor hook for real blockchain transactions + const { executeQueueTransaction, executeSignTransaction, executeDeployPreApproval } = useTransactionExecutor(); + + // Pre-approve mode state - track deployed address + const [deployedPreApproveAddress, setDeployedPreApproveAddress] = useState
(null); + + // Steps state + const [transactionSteps, setTransactionSteps] = useState([]); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [initialized, setInitialized] = useState(false); + + // Nonce selection state + const [currentSafeNonce, setCurrentSafeNonce] = useState(0); + const [queueItems, setQueueItems] = useState([]); + const [nonceDataLoaded, setNonceDataLoaded] = useState(false); + + // Create service instances + const clientService = useMemo(() => { + const rpcUrl = getRpcUrlForChain(chainId as number); + const chain = getViemChain(chainId as number); + return new ClientService(rpcUrl, chain); + }, [chainId]); + const queueService = useMemo(() => new QueueService(clientService), [clientService]); + + // Build transaction steps for queue mode (2 steps) + const buildQueueSteps = useCallback( + (actionBuilderAddress: Address): TransactionStep[] => { + const steps: TransactionStep[] = []; + + // Step 1: Queue Transaction + const queueData = encodeFunctionData({ + abi: canonGuardAbi, + functionName: "queueTransaction", + args: [actionBuilderAddress], + }); + + steps.push({ + id: "queue-transaction", + title: "Queue Transaction", + description: "Queue the action builder in Canon Guard", + status: "pending", + to: guardAddress as Address, + data: queueData, + }); + + // Step 2: Sign Transaction + const signData = encodeFunctionData({ + abi: safeAbi, + functionName: "approveHash", + args: ["0x0000000000000000000000000000000000000000000000000000000000000000" as Hex], + }); + + steps.push({ + id: "sign-transaction", + title: "Sign Transaction", + description: "Approve the transaction hash in the Safe", + status: "pending", + to: safeAddress as Address, + data: signData, + }); + + return steps; + }, + [guardAddress, safeAddress], + ); + + // Build transaction steps for pre-approve mode (3 steps) + const buildPreApproveSteps = useCallback( + (actionBuilderAddress: Address, duration: bigint): TransactionStep[] => { + const steps: TransactionStep[] = []; + + // Step 1: Deploy Pre-Approve Action + const deployData = encodeFunctionData({ + abi: preApproveActionFactoryAbi, + functionName: "createPreApproveAction", + args: [actionBuilderAddress, duration], + }); + + steps.push({ + id: "deploy-preapprove", + title: "Deploy Pre-Approval", + description: "Deploy a pre-approval action for this action builder", + status: "pending", + to: PRE_APPROVE_ACTION_FACTORY, + data: deployData, + }); + + // Step 2: Queue Pre-Approve (placeholder - will use deployed address) + const queueData = encodeFunctionData({ + abi: canonGuardAbi, + functionName: "queueTransaction", + args: ["0x0000000000000000000000000000000000000000" as Address], // Placeholder + }); + + steps.push({ + id: "queue-preapprove", + title: "Queue Pre-Approval", + description: "Queue the pre-approval action in Canon Guard", + status: "pending", + to: guardAddress as Address, + data: queueData, + }); + + // Step 3: Sign Pre-Approve + const signData = encodeFunctionData({ + abi: safeAbi, + functionName: "approveHash", + args: ["0x0000000000000000000000000000000000000000000000000000000000000000" as Hex], + }); + + steps.push({ + id: "sign-preapprove", + title: "Sign Pre-Approval", + description: "Approve the pre-approval transaction hash in the Safe", + status: "pending", + to: safeAddress as Address, + data: signData, + }); + + return steps; + }, + [guardAddress, safeAddress], + ); + + // Initialize on mount - requires navigation state + useEffect(() => { + // If no navigation state, redirect back to canon list + if (!navigationState?.actionBuilderAddress) { + navigateWithParams("/canon-list"); + return; + } + + // Build steps based on mode + const steps = + isPreApproveMode && navigationState.approvalDuration + ? buildPreApproveSteps(navigationState.actionBuilderAddress, navigationState.approvalDuration) + : buildQueueSteps(navigationState.actionBuilderAddress); + + setTransactionSteps(steps); + setInitialized(true); + }, [navigationState, isPreApproveMode, buildQueueSteps, buildPreApproveSteps, navigateWithParams]); + + // Fetch nonce data for nonce selection + useEffect(() => { + const fetchNonceData = async () => { + if (!guardAddress || !safeAddress || nonceDataLoaded) { + return; + } + + try { + console.log("[QueueActionSection] Fetching nonce data"); + + const [nonce, items] = await Promise.all([ + queueService.getCurrentSafeNonce(guardAddress as Address), + queueService.getQueueItems(guardAddress as Address, safeAddress as Address), + ]); + + console.log("[QueueActionSection] Nonce data loaded:", { nonce, queueItemsCount: items.length }); + setCurrentSafeNonce(nonce); + setQueueItems(items); + setNonceDataLoaded(true); + } catch (error) { + console.error("[QueueActionSection] Failed to fetch nonce data:", error); + setNonceDataLoaded(true); + } + }; + + fetchNonceData(); + }, [guardAddress, safeAddress, queueService, nonceDataLoaded]); + + // Handle executing current step + const handleExecuteStep = useCallback( + async (nonce?: number) => { + if (!navigationState?.actionBuilderAddress || !guardAddress || !safeAddress) return; + if (currentStepIndex >= transactionSteps.length) return; + + const currentStep = transactionSteps[currentStepIndex]; + const stepIndex = currentStepIndex; + + // Update status to waiting + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "waiting" }; + return updated; + }); + + try { + let result = null; + + // Queue mode steps + if (currentStep.id === "queue-transaction") { + result = await executeQueueTransaction(guardAddress as Address, navigationState.actionBuilderAddress); + } else if (currentStep.id === "sign-transaction") { + result = await executeSignTransaction( + safeAddress as Address, + guardAddress as Address, + navigationState.actionBuilderAddress, + nonce, + ); + console.log("[QueueActionSection] Sign transaction result:", result, "nonce:", nonce); + } + // Pre-approve mode steps + else if (currentStep.id === "deploy-preapprove") { + if (!navigationState.approvalDuration) { + throw new Error("Approval duration not set"); + } + const deployResult = await executeDeployPreApproval( + navigationState.actionBuilderAddress, + navigationState.approvalDuration, + ); + if (deployResult?.preApprovalAddress) { + setDeployedPreApproveAddress(deployResult.preApprovalAddress); + result = deployResult; + } + } else if (currentStep.id === "queue-preapprove") { + if (!deployedPreApproveAddress) { + throw new Error("Pre-approve address not available"); + } + result = await executeQueueTransaction(guardAddress as Address, deployedPreApproveAddress); + } else if (currentStep.id === "sign-preapprove") { + if (!deployedPreApproveAddress) { + throw new Error("Pre-approve address not available"); + } + result = await executeSignTransaction( + safeAddress as Address, + guardAddress as Address, + deployedPreApproveAddress, + nonce, + ); + console.log("[QueueActionSection] Sign pre-approve result:", result, "nonce:", nonce); + } + + if (result) { + // Update status to signed + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "signed" }; + return updated; + }); + + // Move to next step + if (stepIndex < transactionSteps.length - 1) { + setCurrentStepIndex(stepIndex + 1); + } + + // Notify that queue count may have changed after sign steps + if (currentStep.id === "sign-transaction" || currentStep.id === "sign-preapprove") { + onQueueCountChange?.(); + } + } else { + // Transaction failed or was rejected + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "pending" }; + return updated; + }); + } + } catch (error) { + console.error("Error executing step:", error); + setTransactionSteps((prev) => { + const updated = [...prev]; + updated[stepIndex] = { ...updated[stepIndex], status: "error" }; + return updated; + }); + } + }, + [ + currentStepIndex, + transactionSteps, + navigationState, + guardAddress, + safeAddress, + deployedPreApproveAddress, + executeQueueTransaction, + executeSignTransaction, + executeDeployPreApproval, + onQueueCountChange, + ], + ); + + // Handle back navigation + const handleBack = useCallback(() => { + navigateWithParams("/canon-list"); + }, [navigateWithParams]); + + // Handle navigate to create (actually goes to canon list for queue flow) + const handleNavigateToCanonList = useCallback(() => { + navigateWithParams("/canon-list"); + }, [navigateWithParams]); + + // Check if signing is complete + const isSigningComplete = transactionSteps.length > 0 && transactionSteps.every((s) => s.status === "signed"); + + if (!initialized || !navigationState) { + return ( + + + + ); + } + + // Determine breadcrumb based on mode + const breadcrumbPage = isRemovePreApproval + ? "Remove Pre-Approval" + : isPreApproveMode + ? "Pre-Approve Action" + : "Queue Action"; + + return ( + + ); +}; + +const LoadingContainer = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + height: "400px", +}); + +export default QueueActionSection; diff --git a/src/components/QueueSection.tsx b/src/components/QueueSection.tsx index 6beb7a2..3343afd 100644 --- a/src/components/QueueSection.tsx +++ b/src/components/QueueSection.tsx @@ -1,62 +1,2 @@ -import { Queue, HourglassEmpty } from "@mui/icons-material"; -import { Box, Typography, styled } from "@mui/material"; -import { safeDesignTokens } from "~/config/themes/safeTheme"; -import { QueuedTransaction } from "~/types/canon-guard"; -import { ActionColumn } from "./ActionColumn"; - -interface QueueSectionProps { - queuedActions: QueuedTransaction[]; - waitingForApprovalActions: QueuedTransaction[]; -} - -export const QueueSection = ({ queuedActions, waitingForApprovalActions }: QueueSectionProps) => { - return ( - - Queue Management - - } - title='Queued Actions' - actions={queuedActions} - emptyMessage='No queued actions at the moment' - showApprovalInfo={false} - /> - } - title='Waiting for Approval' - actions={waitingForApprovalActions} - emptyMessage='No actions waiting for approval' - showApprovalInfo={true} - /> - - - ); -}; - -const QueueContentSection = styled(Box)(() => ({ - display: "flex", - flexDirection: "column", - gap: safeDesignTokens.spacing.xl, - padding: safeDesignTokens.spacing.xl, - maxWidth: "1400px", - margin: "0 auto", - width: "100%", -})); - -const PageTitle = styled(Typography)(({ theme }) => ({ - fontSize: "1.75rem", - fontWeight: 700, - color: theme.palette.text.primary, - marginBottom: safeDesignTokens.spacing.lg, - textAlign: "center", -})); - -const ColumnsLayout = styled(Box)(({ theme }) => ({ - display: "flex", - gap: safeDesignTokens.spacing.xl, - alignItems: "stretch", - [theme.breakpoints.down("md")]: { - flexDirection: "column", - gap: safeDesignTokens.spacing.lg, - }, -})); +// Re-export from the new QueueSection directory +export { QueueSection } from "./QueueSection/index"; diff --git a/src/components/QueueSection/QueueItem.tsx b/src/components/QueueSection/QueueItem.tsx new file mode 100644 index 0000000..3970f78 --- /dev/null +++ b/src/components/QueueSection/QueueItem.tsx @@ -0,0 +1,620 @@ +import { useState, useEffect } from "react"; +import { Box, styled, CircularProgress } from "@mui/material"; +import { BoxIcon, ZapIcon, ZapOffIcon, CheckIcon, LockIcon, InfoIcon, VectorSquareIcon } from "~/components/icons"; +import { CopyableText } from "~/components/shared/CopyButton"; +import { StyledTooltip } from "~/components/shared/StyledComponents"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { QueueItem as QueueItemType } from "~/services"; +import { getFactoryDisplayName as getFactoryDisplay } from "~/utils/factoryDisplay"; +import type { Address } from "viem"; + +interface QueueItemProps { + item: QueueItemType; + connectedAddress?: Address; + safeOwners?: Address[]; + emergencyMode?: boolean; + emergencyCaller?: Address | null; + onSign?: () => void; + onExecute?: () => void; + onRemove?: () => void; + isLoading?: boolean; + isSignLoading?: boolean; + isRemoveLoading?: boolean; +} + +export const QueueItem = ({ + item, + connectedAddress, + safeOwners = [], + emergencyMode = false, + emergencyCaller, + onSign, + onExecute, + onRemove, + isLoading, + isSignLoading, + isRemoveLoading, +}: QueueItemProps) => { + const [isHovered, setIsHovered] = useState(false); + const { + actionBuilderAddress, + nonce, + currentNonce, + label, + factoryType, + factoryLabel, + isHubChild, + hubType, + hubLabel, + proposer, + approversCount, + threshold, + isPreApproved, + hasExecutionDelay, + executionDelayRemaining, + isFullySigned, + isAtCurrentNonce, + } = item; + + // Stale nonce: item's nonce is behind current nonce (signatures are invalid) + const isStaleNonce = nonce < currentNonce; + + // Warning state: 0 signatures OR stale nonce (needs re-signing) + const isWarningState = approversCount === 0 || isStaleNonce; + + // Untitled state: no label in registry + const isUntitled = !label || label.trim() === ""; + const displayLabel = isUntitled ? "Untitled Transaction" : label; + + // Real-time countdown for execution delay + const [remainingSeconds, setRemainingSeconds] = useState(executionDelayRemaining); + + // Sync with prop when it changes + useEffect(() => { + setRemainingSeconds(executionDelayRemaining); + }, [executionDelayRemaining]); + + // Countdown timer - decrements every second while there's time remaining + useEffect(() => { + if (!hasExecutionDelay || remainingSeconds <= 0) return; + + const interval = setInterval(() => { + setRemainingSeconds((prev) => Math.max(0, prev - 1)); + }, 1000); + + return () => clearInterval(interval); + }, [hasExecutionDelay, remainingSeconds]); + + // Get display label for factory type using centralized utility + const getFactoryDisplayName = (): string => { + const displayName = getFactoryDisplay(factoryType); + // Fall back to factoryLabel if unknown (CSS handles uppercase) + return displayName !== "Unknown" ? displayName : factoryLabel || "Unknown"; + }; + + // Humanize execution delay - never show seconds + const humanizeDelay = (seconds: number): string => { + if (seconds <= 0) return ""; + + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + + if (days > 0) { + return days === 1 ? "1 day left" : `${days} days left`; + } + if (hours > 0) { + return hours === 1 ? "1 hour left" : `${hours} hours left`; + } + if (minutes > 0) { + return minutes === 1 ? "1 minute left" : `${minutes} minutes left`; + } + return "<1 minute left"; + }; + + // Check if connected wallet is a Safe signer + const isSigner = + connectedAddress && safeOwners.some((owner) => owner.toLowerCase() === connectedAddress.toLowerCase()); + + // Check if connected wallet is the emergency caller + const isEmergencyCaller = + connectedAddress && emergencyCaller && connectedAddress.toLowerCase() === emergencyCaller.toLowerCase(); + + // Get the reason why Execute is disabled (prioritized order) + const getExecuteDisableReason = (): string | null => { + if (!isSigner && !isEmergencyCaller) return "Connected wallet is not a signer"; + if (emergencyMode && !isEmergencyCaller) return "Only emergency caller while in emergency mode"; + if (!isFullySigned) return "Waiting for signatures"; + if (!isAtCurrentNonce) return "Waiting to be upcoming nonce"; + if (remainingSeconds > 0) return "Cooldown in progress"; + return null; // Executable + }; + + // Determine action button state + // Sign: signers can sign if not fully signed, including stale items that need re-signing + const showSignButton = isSigner && !isFullySigned; + // Execute: show when wallet is connected AND sign button is not showing + const showExecuteButton = !!connectedAddress && !showSignButton; + const executeDisableReason = showExecuteButton ? getExecuteDisableReason() : null; + const showNoAction = !isAtCurrentNonce && !showSignButton && !showExecuteButton; + + // Show remove button only if connected wallet is the proposer + const isProposer = connectedAddress && proposer && connectedAddress.toLowerCase() === proposer.toLowerCase(); + const showRemoveButton = isProposer; + + return ( + setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}> + {/* Left Panel - Nonce/Warning + Signed indicator */} + + + {isWarningState ? ( + + + Warning + + ) : ( + {nonce} + )} + + + + Signed + + {approversCount}/{threshold} + + + + + {/* Right Panel - Details */} + + + + {displayLabel} + + + {actionBuilderAddress} + + + + + {showRemoveButton && (isHovered || isRemoveLoading) && ( + + {isRemoveLoading ? ( + + ) : ( + "REMOVE" + )} + + )} + {showExecuteButton && ( + + + + {isLoading ? ( + + ) : ( + "EXECUTE" + )} + + + + )} + {showSignButton && ( + + {isSignLoading ? ( + + ) : ( + "SIGN" + )} + + )} + {showNoAction && !showRemoveButton && } + + + + + + + + {isHubChild ? hubType || "Capped Transfer" : getFactoryDisplayName()} + + {isHubChild && ( + + + {hubLabel || "Untitled Hub"} + + )} + + + + {/* Execution Delay */} + + {remainingSeconds > 0 ? ( + <> + + {humanizeDelay(remainingSeconds)} + + + ) : ( + <> + + Execution Delay Over + + )} + + + + + {/* Fast/Slow Path */} + + {isPreApproved ? ( + <> + + Fast-path + + ) : ( + <> + + Slow-path + + )} + + + + + + ); +}; + +// Helper to determine signed indicator state +type SignedState = "full" | "partial" | "warning"; + +const getSignedState = (count: number, threshold: number): SignedState => { + if (count >= threshold) return "full"; + if (count > 0) return "partial"; + return "warning"; +}; + +// Styled Components +const ItemContainer = styled(Box)({ + display: "flex", + width: "100%", + borderRadius: "8px", + overflow: "hidden", +}); + +const LeftPanel = styled(Box)({ + display: "flex", + flexDirection: "column", + justifyContent: "space-between", + width: "160px", + minWidth: "160px", + padding: "16px", + backgroundColor: "#202026", // layer1-variation +}); + +const NonceSection = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "flex-end", +}); + +const NonceText = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 600, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent20, + textAlign: "right", +}); + +const WarningContainer = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const WarningText = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.status.amber, +}); + +const SignedSection = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + width: "100%", +}); + +const SignedIndicator = styled(Box, { + shouldForwardProp: (prop) => prop !== "$state", +})<{ $state: SignedState }>(({ $state }) => ({ + width: "14px", + height: "14px", + borderRadius: "50%", + backgroundColor: $state === "full" ? canonHeaderTokens.brand.green : "transparent", + border: + $state === "full" + ? "none" + : $state === "warning" + ? `2px solid ${canonHeaderTokens.status.amberLight}` + : `2px solid ${canonHeaderTokens.foreground.accent40}`, + position: "relative", + "&::after": + $state === "full" + ? { + content: '"✓"', + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + fontSize: "8px", + color: canonHeaderTokens.background.layer0, + } + : {}, +})); + +const SignedLabel = styled("span", { + shouldForwardProp: (prop) => prop !== "$state", +})<{ $state: SignedState }>(({ $state }) => ({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: + $state === "full" + ? canonHeaderTokens.brand.green + : $state === "warning" + ? canonHeaderTokens.status.amberLight + : canonHeaderTokens.foreground.accent10, +})); + +const SignedCount = styled("span", { + shouldForwardProp: (prop) => prop !== "$state", +})<{ $state: SignedState }>(({ $state }) => ({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: + $state === "full" + ? canonHeaderTokens.brand.green + : $state === "warning" + ? canonHeaderTokens.status.amberLight + : canonHeaderTokens.foreground.accent10, +})); + +const RightPanel = styled(Box)({ + display: "flex", + flexDirection: "column", + flex: 1, + padding: "16px 20px", + gap: "32px", + backgroundColor: canonHeaderTokens.background.layer1, + justifyContent: "center", +}); + +const TopRow = styled(Box)({ + display: "flex", + alignItems: "flex-start", + justifyContent: "space-between", + width: "100%", +}); + +const TitleSection = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "8px", + flex: 1, +}); + +const Title = styled("span", { + shouldForwardProp: (prop) => prop !== "$isUntitled", +})<{ $isUntitled: boolean }>(({ $isUntitled }) => ({ + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 600, + lineHeight: "20px", + color: $isUntitled ? canonHeaderTokens.foreground.accent20 : canonHeaderTokens.foreground.accent0, + textAlign: "left", +})); + +const AddressRow = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isVisible", +})<{ $isVisible: boolean }>(({ $isVisible }) => ({ + display: "flex", + alignItems: "center", + gap: "8px", + opacity: $isVisible ? 1 : 0, + transition: "opacity 0.2s ease", + pointerEvents: $isVisible ? "auto" : "none", +})); + +const AddressText = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const ActionSection = styled(Box)({ + display: "flex", + flexDirection: "row", + alignItems: "center", + gap: "8px", + height: "36px", +}); + +const ExecuteButton = styled("button")<{ disabled?: boolean }>(({ disabled }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + minWidth: "90px", + height: "28px", + padding: "8px 20px", + borderRadius: "100px", + border: `1px solid ${disabled ? canonHeaderTokens.foreground.accent40 : "rgba(21, 164, 62, 0.3)"}`, + backgroundColor: "transparent", + cursor: disabled ? "not-allowed" : "pointer", + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 600, + letterSpacing: "0.6px", + textTransform: "uppercase", + color: disabled ? canonHeaderTokens.foreground.accent20 : canonHeaderTokens.brand.green, + "&:hover": { + opacity: disabled ? 1 : 0.8, + }, +})); + +const SignButton = styled("button")<{ disabled?: boolean }>(({ disabled }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + minWidth: "80px", + height: "28px", + padding: "8px 20px", + borderRadius: "100px", + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, + backgroundColor: "transparent", + cursor: disabled ? "not-allowed" : "pointer", + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 600, + letterSpacing: "0.6px", + textTransform: "uppercase", + color: canonHeaderTokens.foreground.accent10, + opacity: disabled ? 0.6 : 1, + "&:hover": { + opacity: disabled ? 0.6 : 0.8, + }, +})); + +const RemoveButton = styled("button")<{ disabled?: boolean }>(({ disabled }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + minWidth: "80px", + height: "28px", + padding: "8px 20px", + borderRadius: "100px", + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, + backgroundColor: "transparent", + cursor: disabled ? "not-allowed" : "pointer", + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 600, + letterSpacing: "0.6px", + textTransform: "uppercase", + color: canonHeaderTokens.foreground.accent10, + opacity: disabled ? 0.6 : 1, + "&:hover": { + opacity: disabled ? 0.6 : 0.8, + }, +})); + +const EmptyAction = styled(Box)({ + width: "104px", + height: "36px", +}); + +const BottomRow = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + width: "100%", +}); + +const FactoryInfo = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const FactoryLabel = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, + textTransform: "uppercase", +}); + +const FactoryInfoSection = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "16px", +}); + +const HubInfo = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const HubLabel = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const StatusInfo = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", +}); + +const DelayInfo = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const DelayDot = styled(Box)({ + width: "6px", + height: "6px", + borderRadius: "50%", + backgroundColor: canonHeaderTokens.status.amber, +}); + +const DelayText = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const Divider = styled(Box)({ + width: "16px", + height: "0.5px", + backgroundColor: canonHeaderTokens.foreground.accent30, +}); + +const PathInfo = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isFastPath", +})<{ $isFastPath: boolean }>({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const PathText = styled("span", { + shouldForwardProp: (prop) => prop !== "$isFastPath", +})<{ $isFastPath: boolean }>(({ $isFastPath }) => ({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: $isFastPath ? canonHeaderTokens.brand.green : canonHeaderTokens.status.red, +})); diff --git a/src/components/QueueSection/index.tsx b/src/components/QueueSection/index.tsx new file mode 100644 index 0000000..0480852 --- /dev/null +++ b/src/components/QueueSection/index.tsx @@ -0,0 +1,622 @@ +import { useState, useEffect, useCallback } from "react"; +import { Box, styled, CircularProgress } from "@mui/material"; +import { Address } from "viem"; +import { SearchIcon, HelpCircleIcon, ChevronLeftIcon, ChevronRightIcon, EllipsisIcon } from "~/components/icons"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { useCanonGuardConfig } from "~/hooks/useCanonGuardConfig"; +import { useNavigateWithParams } from "~/hooks/useNavigateWithParams"; +import { useQueueService } from "~/hooks/useServices"; +import { useStateContext } from "~/hooks/useStateContext"; +import { useTransactionExecutor } from "~/hooks/useTransactionExecutor"; +import { useWallet } from "~/hooks/useWallet"; +import { QueueItem as QueueItemType } from "~/services"; +import { ActionFactoryType } from "~/types"; +import { QueueItem } from "./QueueItem"; + +const ITEMS_PER_PAGE = 25; + +interface QueueSectionProps { + safeOwners?: Address[]; + onQueueCountChange?: (count: number) => void; +} + +export const QueueSection = ({ safeOwners = [], onQueueCountChange }: QueueSectionProps) => { + const queueService = useQueueService(); + const { guardAddress, safeAddress } = useStateContext(); + const { executeCanonTransaction, executeCancelTransaction, isExecuting } = useTransactionExecutor(); + const navigateWithParams = useNavigateWithParams(); + const { address: connectedAddress } = useWallet(); + const { emergencyMode, emergencyCaller, refetch: refetchConfig } = useCanonGuardConfig(); + + const [queueItems, setQueueItems] = useState([]); + const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(""); + const [activeFilter, setActiveFilter] = useState<"all" | "in-review" | "signed">("all"); + const [currentPage, setCurrentPage] = useState(1); + const [executingItemAddress, setExecutingItemAddress] = useState
(null); + const [removingItemAddress, setRemovingItemAddress] = useState
(null); + const [isRemoving, setIsRemoving] = useState(false); + + const fetchQueueItems = useCallback(async () => { + if (!guardAddress || !safeAddress) return; + + setLoading(true); + try { + const items = await queueService.getQueueItems(guardAddress as Address, safeAddress as Address); + console.log(`[QueueSection] Fetched queue items count: ${items.length}`); + items.forEach((item, idx) => { + console.log( + `[QueueSection] Item ${idx}: ${JSON.stringify({ + label: item.label, + nonce: item.nonce, + currentNonce: item.currentNonce, + isAtCurrentNonce: item.isAtCurrentNonce, + approversCount: item.approversCount, + threshold: item.threshold, + isFullySigned: item.isFullySigned, + isExecutable: item.isExecutable, + })}`, + ); + }); + setQueueItems(items); + onQueueCountChange?.(items.length); + } catch (error) { + console.error("Failed to fetch queue items:", error); + } finally { + setLoading(false); + } + }, [queueService, guardAddress, safeAddress, onQueueCountChange]); + + useEffect(() => { + fetchQueueItems(); + }, [fetchQueueItems]); + + // Filter items based on search and active filter + const filteredItems = queueItems.filter((item) => { + // Search filter + if (searchQuery) { + const query = searchQuery.toLowerCase(); + const matchesLabel = item.label?.toLowerCase().includes(query); + const matchesAddress = item.actionBuilderAddress.toLowerCase().includes(query); + if (!matchesLabel && !matchesAddress) return false; + } + + // Status filter + switch (activeFilter) { + case "in-review": + // Items with 0 signatures (need review) + return item.approversCount === 0; + case "signed": + // Items with at least 1 signature + return item.approversCount > 0; + default: + return true; + } + }); + + // Group items into three sections (all items are now at current nonce): + // 1. MISSING NONCE - items with 0 signatures (need to be signed) + // 2. READY TO EXECUTE - items that are fully signed and executable + // 3. WAITING FOR APPROVAL - items with some signatures but not fully signed + // All sections sorted by nonce ascending (lowest nonce first = can execute first) + const missingNonce = filteredItems.filter((item) => item.approversCount === 0).sort((a, b) => a.nonce - b.nonce); + const readyToExecute = filteredItems.filter((item) => item.isFullySigned).sort((a, b) => a.nonce - b.nonce); + const waitingForApproval = filteredItems + .filter((item) => item.approversCount > 0 && !item.isFullySigned) + .sort((a, b) => a.nonce - b.nonce); + + // Debug: log section counts + console.log( + `[QueueSection] Section counts: ${JSON.stringify({ + filteredItems: filteredItems.length, + missingNonce: missingNonce.length, + readyToExecute: readyToExecute.length, + waitingForApproval: waitingForApproval.length, + })}`, + ); + + // Pagination - count only items at current nonce + const displayedItems = missingNonce.length + readyToExecute.length + waitingForApproval.length; + const totalPages = Math.ceil(displayedItems / ITEMS_PER_PAGE); + const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; + const endIndex = Math.min(startIndex + ITEMS_PER_PAGE, displayedItems); + + // Get counts for filter badges + const allCount = queueItems.length; + const inReviewCount = queueItems.filter((i) => i.approversCount === 0).length; + const signedCount = queueItems.filter((i) => i.approversCount > 0).length; + + // Navigate to sign flow for the queue item + const handleSign = useCallback( + (item: QueueItemType) => { + console.log("[QueueSection] Navigating to sign flow for:", item.label); + navigateWithParams("/queue/sign", { + state: { + actionBuilderAddress: item.actionBuilderAddress, + label: item.label || "Untitled Transaction", + factoryLabel: item.factoryLabel || "Unknown", + nonce: item.nonce, + approversCount: item.approversCount, + threshold: item.threshold, + }, + }); + }, + [navigateWithParams], + ); + + const handleExecute = useCallback( + async (item: QueueItemType) => { + if (!guardAddress || isExecuting) return; + + setExecutingItemAddress(item.actionBuilderAddress); + try { + const result = await executeCanonTransaction(guardAddress as Address, item.actionBuilderAddress); + + if (result) { + console.log("Transaction executed successfully:", result); + + // If this was a CHANGE_SAFE_GUARD action, do a full page refresh to landing + // (the guard is now detached/changed, so the current context is invalid) + if (item.factoryType === ActionFactoryType.CHANGE_SAFE_GUARD) { + window.location.href = "/"; + return; + } + + // Optimistically update local state: + // 1. Remove the executed item + // 2. Increment currentNonce for remaining items (Safe nonce has increased) + // 3. Recalculate isAtCurrentNonce for remaining items + // 4. Reset signature fields for stale nonce items (signatures are now invalid) + setQueueItems((prev) => { + const newCurrentNonce = item.currentNonce + 1; + const updated = prev + .filter((i) => i.actionBuilderAddress !== item.actionBuilderAddress) + .map((i) => { + const isStale = i.nonce < newCurrentNonce; + return { + ...i, + currentNonce: newCurrentNonce, + isAtCurrentNonce: i.nonce === newCurrentNonce, + // Reset signature fields for stale items (signatures are invalid) + ...(isStale && { + approversCount: 0, + isFullySigned: false, + }), + }; + }); + onQueueCountChange?.(updated.length); + return updated; + }); + // Refetch config to update emergency mode status if it changed + await refetchConfig(); + } + } catch (error) { + console.error("Failed to execute transaction:", error); + } + setExecutingItemAddress(null); + }, + [guardAddress, isExecuting, executeCanonTransaction, refetchConfig, onQueueCountChange], + ); + + const handleRemove = useCallback( + async (item: QueueItemType) => { + if (!guardAddress || isRemoving) return; + + setRemovingItemAddress(item.actionBuilderAddress); + setIsRemoving(true); + try { + const result = await executeCancelTransaction(guardAddress as Address, item.actionBuilderAddress); + + if (result) { + console.log("Transaction cancelled successfully:", result); + // Optimistically remove from local state and update count + setQueueItems((prev) => { + const updated = prev.filter((i) => i.actionBuilderAddress !== item.actionBuilderAddress); + onQueueCountChange?.(updated.length); + return updated; + }); + } + } catch (error) { + console.error("Failed to cancel transaction:", error); + } + setRemovingItemAddress(null); + setIsRemoving(false); + }, + [guardAddress, isRemoving, executeCancelTransaction, onQueueCountChange], + ); + + if (loading) { + return ( + + + + ); + } + + return ( + + + {/* Title Section */} + + + Queue + + + + + {/* Search and Filter Bar */} + + + + setSearchQuery(e.target.value)} + /> + + + setActiveFilter("all")}> + ALL + {allCount} + + + setActiveFilter("in-review")}> + IN REVIEW + {inReviewCount} + + + setActiveFilter("signed")}> + SIGNED + {signedCount} + + + + {/* Queue Items */} + + {/* Missing Nonce (0 signatures at current nonce) */} + {missingNonce.length > 0 && ( + + + MISSING NONCE + + + {missingNonce.map((item) => ( + handleSign(item)} + onExecute={() => handleExecute(item)} + onRemove={() => handleRemove(item)} + isLoading={executingItemAddress === item.actionBuilderAddress && isExecuting} + isSignLoading={false} + isRemoveLoading={removingItemAddress === item.actionBuilderAddress && isRemoving} + /> + ))} + + + )} + + {/* Ready to Execute (fully signed) */} + {readyToExecute.length > 0 && ( + + + READY TO EXECUTE + + + {readyToExecute.map((item) => ( + handleSign(item)} + onExecute={() => handleExecute(item)} + onRemove={() => handleRemove(item)} + isLoading={executingItemAddress === item.actionBuilderAddress && isExecuting} + isSignLoading={false} + isRemoveLoading={removingItemAddress === item.actionBuilderAddress && isRemoving} + /> + ))} + + + )} + + {/* Waiting for Approval (some signatures but not fully signed) */} + {waitingForApproval.length > 0 && ( + + + WAITING FOR APPROVAL + + + {waitingForApproval.map((item) => ( + handleSign(item)} + onExecute={() => handleExecute(item)} + onRemove={() => handleRemove(item)} + isLoading={executingItemAddress === item.actionBuilderAddress && isExecuting} + isSignLoading={false} + isRemoveLoading={removingItemAddress === item.actionBuilderAddress && isRemoving} + /> + ))} + + + )} + + {/* Empty State */} + {missingNonce.length === 0 && readyToExecute.length === 0 && waitingForApproval.length === 0 && ( + {searchQuery ? "No items match your search" : "No transactions in queue"} + )} + + {/* Pagination */} + {displayedItems > 0 && ( + + + Showing {startIndex + 1} - {endIndex} / {displayedItems} + + + setCurrentPage((p) => Math.max(1, p - 1))} + disabled={currentPage === 1} + > + + + {Array.from({ length: Math.min(3, totalPages) }, (_, i) => i + 1).map((page) => ( + setCurrentPage(page)}> + {page} + + ))} + {totalPages > 3 && ( + <> + + setCurrentPage(totalPages)}> + {totalPages} + + + )} + setCurrentPage((p) => Math.min(totalPages, p + 1))} + disabled={currentPage === totalPages} + > + + + + + )} + + + + ); +}; + +// Styled Components +const Container = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "center", + width: "100%", + padding: "32px 120px", + minHeight: "100%", +}); + +const ContentWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "20px", + width: "1024px", + maxWidth: "100%", +}); + +const LoadingContainer = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + height: "400px", +}); + +const TitleSection = styled(Box)({ + display: "flex", + flexDirection: "column", + width: "100%", +}); + +const TitleRow = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", + padding: "32px 8px 12px 8px", +}); + +const Title = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "24px", + fontWeight: 500, + fontStyle: "italic", + lineHeight: "32px", + color: canonHeaderTokens.foreground.accent0, +}); + +const SearchFilterBar = styled(Box)({ + display: "flex", + alignItems: "center", + height: "48px", + borderRadius: "8px", + overflow: "hidden", + width: "100%", +}); + +const SearchSection = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", + flex: 1, + height: "100%", + padding: "16px", + backgroundColor: canonHeaderTokens.background.layer1, +}); + +const SearchInput = styled("input")({ + flex: 1, + background: "transparent", + border: "none", + outline: "none", + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent0, + "&::placeholder": { + color: canonHeaderTokens.foreground.accent30, + }, +}); + +const FilterDivider = styled(Box)({ + width: "1px", + minWidth: "1px", + height: "100%", + backgroundColor: canonHeaderTokens.background.layer0, +}); + +const FilterTab = styled("button")<{ $isActive: boolean }>(({ $isActive }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + gap: "8px", + height: "100%", + padding: "0 20px", + backgroundColor: canonHeaderTokens.background.layer1, + border: "none", + cursor: "pointer", + opacity: $isActive ? 1 : 0.8, + "&:hover": { + opacity: 1, + }, +})); + +const FilterLabel = styled("span")<{ $isActive: boolean }>(({ $isActive }) => ({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: $isActive ? canonHeaderTokens.foreground.accent0 : canonHeaderTokens.foreground.accent20, +})); + +const FilterCount = styled("span")<{ $isActive: boolean }>(({ $isActive }) => ({ + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: $isActive ? canonHeaderTokens.brand.green : canonHeaderTokens.foreground.accent30, +})); + +const QueueItemsContainer = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + width: "100%", +}); + +const SectionGroup = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "6px", + width: "100%", +}); + +const SectionHeader = styled(Box)({ + display: "flex", + alignItems: "center", + padding: "8px", +}); + +const SectionTitle = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "11px", + fontWeight: 600, + lineHeight: "12px", + color: canonHeaderTokens.foreground.accent30, +}); + +const ItemsList = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "6px", + width: "100%", +}); + +const EmptyState = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + height: "200px", + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 400, + color: canonHeaderTokens.foreground.accent20, + fontStyle: "italic", +}); + +const PaginationRow = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "12px 0", + borderRadius: "12px", + width: "100%", +}); + +const PaginationInfo = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent30, + padding: "0 16px", +}); + +const PaginationControls = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", + padding: "0 16px", +}); + +const PaginationButton = styled("button")<{ disabled?: boolean }>(({ disabled }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + background: "transparent", + border: "none", + cursor: disabled ? "not-allowed" : "pointer", + opacity: disabled ? 0.3 : 1, + padding: 0, +})); + +const PageNumber = styled("button")<{ $isActive: boolean }>(({ $isActive }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "28px", + height: "28px", + borderRadius: "8px", + backgroundColor: $isActive ? canonHeaderTokens.background.layer1 : "transparent", + border: "none", + cursor: "pointer", + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 600, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +})); diff --git a/src/components/QueueSignSection/index.tsx b/src/components/QueueSignSection/index.tsx new file mode 100644 index 0000000..17d2936 --- /dev/null +++ b/src/components/QueueSignSection/index.tsx @@ -0,0 +1,252 @@ +/** + * QueueSignSection - Signing flow for queue items + * + * This component provides a dedicated signing flow when clicking "Sign" on a queue item. + * It shows the NonceSelector and Sign Transaction step. + * + * - If the item has no signatures (approversCount === 0), nonce is editable + * - If the item has existing signatures (approversCount > 0), nonce is read-only (locked to item.nonce) + */ + +import { useState, useCallback, useEffect, useMemo } from "react"; +import { Box, CircularProgress, styled } from "@mui/material"; +import { useLocation } from "react-router-dom"; +import { Address, Hex, encodeFunctionData } from "viem"; +import { safeAbi } from "~/abis/canonGuard"; +import { getRpcUrlForChain, getViemChain } from "~/config/chains"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { useNavigateWithParams, useTransactionExecutor } from "~/hooks"; +import { useStateContext } from "~/hooks/useStateContext"; +import { ClientService } from "~/services/clientService"; +import { QueueService, type QueueItem } from "~/services/queueService"; +import type { TransactionStep } from "~/services/transactionBuilderService"; +import { SigningFlowStep } from "../NewAction/steps"; + +interface QueueSignState { + actionBuilderAddress: Address; + label: string; + factoryLabel: string; + nonce: number; + approversCount: number; + threshold: number; +} + +interface QueueSignSectionProps { + onQueueCountChange?: () => void; +} + +export const QueueSignSection = ({ onQueueCountChange }: QueueSignSectionProps) => { + const location = useLocation(); + const navigateWithParams = useNavigateWithParams(); + const { safeAddress, guardAddress, chainId } = useStateContext(); + + // Get state passed from Queue + const navigationState = location.state as QueueSignState | null; + + // Transaction executor + const { executeSignTransaction } = useTransactionExecutor(); + + // Steps state + const [transactionSteps, setTransactionSteps] = useState([]); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [initialized, setInitialized] = useState(false); + const [isComplete, setIsComplete] = useState(false); + + // Nonce selection state + const [currentSafeNonce, setCurrentSafeNonce] = useState(0); + const [queueItems, setQueueItems] = useState([]); + const [nonceDataLoaded, setNonceDataLoaded] = useState(false); + + // Create service instances + const clientService = useMemo(() => { + const rpcUrl = getRpcUrlForChain(chainId as number); + const chain = getViemChain(chainId as number); + return new ClientService(rpcUrl, chain); + }, [chainId]); + const queueService = useMemo(() => new QueueService(clientService), [clientService]); + + // Determine if nonce should be read-only (item already has signatures) + const isNonceLocked = (navigationState?.approversCount ?? 0) > 0; + + // Build transaction steps (single Sign step) + const buildSignSteps = useCallback((): TransactionStep[] => { + const signData = encodeFunctionData({ + abi: safeAbi, + functionName: "approveHash", + args: ["0x0000000000000000000000000000000000000000000000000000000000000000" as Hex], + }); + + return [ + { + id: "sign-transaction", + title: "Sign Transaction", + description: "Approve the transaction hash in the Safe", + status: "pending", + to: safeAddress as Address, + data: signData, + }, + ]; + }, [safeAddress]); + + // Initialize on mount + useEffect(() => { + // If no navigation state, redirect back to queue + if (!navigationState) { + console.log("[QueueSignSection] No navigation state, redirecting to queue"); + navigateWithParams("/queue"); + return; + } + + if (!initialized && guardAddress && safeAddress) { + console.log("[QueueSignSection] Initializing sign flow for:", navigationState.label); + const steps = buildSignSteps(); + setTransactionSteps(steps); + setCurrentStepIndex(0); + setInitialized(true); + } + }, [navigationState, initialized, guardAddress, safeAddress, buildSignSteps, navigateWithParams]); + + // Fetch nonce data + useEffect(() => { + if (!initialized || !guardAddress || !safeAddress || nonceDataLoaded) { + return; + } + + const fetchNonceData = async () => { + try { + console.log("[QueueSignSection] Fetching nonce data"); + const [nonce, items] = await Promise.all([ + queueService.getCurrentSafeNonce(guardAddress as Address), + queueService.getQueueItems(guardAddress as Address, safeAddress as Address), + ]); + console.log("[QueueSignSection] Nonce data loaded:", { nonce, queueItemsCount: items.length }); + setCurrentSafeNonce(nonce); + setQueueItems(items); + setNonceDataLoaded(true); + } catch (error) { + console.error("[QueueSignSection] Failed to fetch nonce data:", error); + setCurrentSafeNonce(navigationState?.nonce ?? 0); + setQueueItems([]); + setNonceDataLoaded(true); + } + }; + + fetchNonceData(); + }, [initialized, guardAddress, safeAddress, queueService, nonceDataLoaded, navigationState?.nonce]); + + // Handle executing the sign step + const handleExecuteStep = useCallback( + async (selectedNonce?: number) => { + if (!navigationState || !safeAddress || !guardAddress) { + console.error("[QueueSignSection] Missing required state for signing"); + return; + } + + const currentStep = transactionSteps[currentStepIndex]; + if (!currentStep || currentStep.id !== "sign-transaction") { + console.error("[QueueSignSection] Invalid step"); + return; + } + + // Use provided nonce, or fall back to item's nonce (for locked nonce case) + const nonceToUse = selectedNonce !== undefined ? selectedNonce : navigationState.nonce; + console.log("[QueueSignSection] Signing transaction with nonce:", nonceToUse); + + // Update step status to in-progress + setTransactionSteps((prev) => + prev.map((step, idx) => (idx === currentStepIndex ? { ...step, status: "in-progress" as const } : step)), + ); + + try { + const result = await executeSignTransaction( + safeAddress as Address, + guardAddress as Address, + navigationState.actionBuilderAddress, + nonceToUse, + ); + + if (result) { + console.log("[QueueSignSection] Sign successful:", result.safeTxHash); + + // Update step to success + setTransactionSteps((prev) => + prev.map((step, idx) => + idx === currentStepIndex ? { ...step, status: "success" as const, hash: result.txHash } : step, + ), + ); + + // Mark as complete to show success screen + setIsComplete(true); + + // Notify that queue count may have changed + onQueueCountChange?.(); + } else { + throw new Error("Sign transaction returned no result"); + } + } catch (error) { + console.error("[QueueSignSection] Sign failed:", error); + setTransactionSteps((prev) => + prev.map((step, idx) => (idx === currentStepIndex ? { ...step, status: "error" as const } : step)), + ); + } + }, + [ + navigationState, + safeAddress, + guardAddress, + transactionSteps, + currentStepIndex, + executeSignTransaction, + onQueueCountChange, + ], + ); + + // Handle back navigation + const handleBack = useCallback(() => { + navigateWithParams("/queue"); + }, [navigateWithParams]); + + // Handle navigate to create (not used in queue sign flow, just go back) + const handleNavigateToCreate = useCallback(() => { + navigateWithParams("/queue"); + }, [navigateWithParams]); + + // Loading state + if (!navigationState || !initialized || !nonceDataLoaded) { + return ( + + + + ); + } + + return ( + + ); +}; + +const LoadingContainer = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + minHeight: "400px", +}); + +export default QueueSignSection; diff --git a/src/components/SafeSidebar.tsx b/src/components/SafeSidebar.tsx deleted file mode 100644 index dee73ef..0000000 --- a/src/components/SafeSidebar.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { Menu as MenuIcon } from "@mui/icons-material"; -import { Box, Drawer, IconButton, styled } from "@mui/material"; -import { optimism } from "viem/chains"; -import { VaultInfo, TabType } from "~/types"; -import { SidebarFooter } from "./sidebar/SidebarFooter"; -import { SidebarHeader } from "./sidebar/SidebarHeader"; -import { SidebarNavigation } from "./sidebar/SidebarNavigation"; -import { SidebarSafeInfo } from "./sidebar/SidebarSafeInfo"; - -interface SafeSidebarProps { - safeInfo: VaultInfo; - activeTab: TabType; - onTabChange: (tab: TabType) => void; - collapsed: boolean; - onToggleCollapse: () => void; -} - -export const SafeSidebar = ({ safeInfo, activeTab, onTabChange, collapsed, onToggleCollapse }: SafeSidebarProps) => { - const drawerContent = ( - - - - - - - ); - - const isMobile = window.innerWidth < 768; - - return ( - <> - {isMobile && ( - - - - )} - - {!isMobile && ( - - {drawerContent} - - )} - - {isMobile && ( - - {drawerContent} - - )} - - ); -}; - -const SidebarContainer = styled(Box)(({ theme }) => ({ - display: "flex", - flexDirection: "column", - height: "100vh", - backgroundColor: theme.palette.background.paper, - borderRight: `1px solid ${theme.palette.divider}`, -})); - -const DesktopSidebar = styled(Drawer, { - shouldForwardProp: (prop) => prop !== "collapsed", -})<{ collapsed: boolean }>(({ collapsed }) => ({ - "& .MuiDrawer-paper": { - width: collapsed ? 60 : 240, - transition: "width 0.3s ease-in-out", - overflowX: "hidden", - position: "fixed", - height: "100vh", - zIndex: 1200, - }, -})); - -const MobileToggleButton = styled(IconButton)(({ theme }) => ({ - position: "fixed", - top: 16, - left: 16, - zIndex: 1300, - backgroundColor: theme.palette.background.paper, - boxShadow: theme.shadows[3], - "&:hover": { - backgroundColor: theme.palette.action.hover, - }, -})); diff --git a/src/components/SettingsSection/index.tsx b/src/components/SettingsSection/index.tsx new file mode 100644 index 0000000..4403bca --- /dev/null +++ b/src/components/SettingsSection/index.tsx @@ -0,0 +1,548 @@ +import { useState } from "react"; +import { Box, styled, CircularProgress } from "@mui/material"; +import { DeploymentModesPanel } from "~/components/DeploymentModesPanel"; +import { EmergencyModePanel } from "~/components/EmergencyModePanel"; +import { HelpCircleIcon, ShieldCheckIcon, AsteriskIcon, ShieldAlertIcon, Link2Icon } from "~/components/icons"; +import { CopyableText } from "~/components/shared/CopyButton"; +import { getChainConfig } from "~/config/chains"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { useStateContext, useCanonGuardConfig, humanizeDuration, useNavigateWithParams } from "~/hooks"; + +export const SettingsSection = () => { + const { safeAddress, guardAddress, chainId, isDetached } = useStateContext(); + const { shortTxExecutionDelay, longTxExecutionDelay, txExpiryDelay, maxApprovalDuration, emergencyMode, isLoading } = + useCanonGuardConfig(); + const navigateWithParams = useNavigateWithParams(); + + const [emergencyPanelOpen, setEmergencyPanelOpen] = useState(false); + const [deploymentModesPanelOpen, setDeploymentModesPanelOpen] = useState(false); + + const chainConfig = getChainConfig(chainId); + + const handleDetachGuard = () => { + navigateWithParams("/settings/detach"); + }; + + const handleAttachGuard = () => { + navigateWithParams("/settings/attach"); + }; + + if (isLoading) { + return ( + + + + ); + } + + return ( + + + {/* Page Title */} + + General Settings + + + + {/* Section Label */} + CANON GUARD SETUP + + {/* Safe Profile Card with Config Stats */} + + {/* Top Section - Safe Profile */} + + + + + + + + Safe + + {safeAddress || ""} + + + {chainConfig?.chain.name || "Unknown Chain"} + + + MANAGE SAFE ACCOUNTS + + + {/* Config Stats Row */} + + + {/* Fast Path Delay */} + + + {humanizeDuration(shortTxExecutionDelay)} + + + Fast Path Delay + + + + + {/* Slow Path Delay */} + + + {humanizeDuration(longTxExecutionDelay)} + + + Slow Path Delay + + + + + {/* Execution Timeframe */} + + + {humanizeDuration(txExpiryDelay)} + + Execution Timeframe + + + + + {/* Max Pre-Approval */} + + + {humanizeDuration(maxApprovalDuration)} + + Max Pre-Approval + + + + + + {/* Password Encryption Card */} + + + + + + + + Password Encryption: + + + ON + + + + Transaction names are public onchain. Set a password to keep them private. + + + + EDIT + + + {/* Emergency Mode Card */} + + + + + + + + Emergency Mode: + + + {emergencyMode ? "ON" : "OFF"} + + + Use this when keys are compromised or signers are under pressure. + + + setEmergencyPanelOpen(true)}> + EDIT + + + + {/* Emergency Mode Panel */} + setEmergencyPanelOpen(false)} /> + + {/* Canon Guard Status Card */} + + + + + + + + + Canon Guard: + + + {isDetached ? "Detached" : "Attached"} + + + + {isDetached + ? "Transactions are routed through the guard but it's not attached to your Safe. " + : "You can adopt Canon Guard in two modes: Attached and Detached. "} + setDeploymentModesPanelOpen(true)}>Learn more + + + + {isDetached ? ( + + ATTACH + + ) : ( + + DETACH + + )} + + + + {guardAddress || ""} + + + + {/* Deployment Modes Panel */} + setDeploymentModesPanelOpen(false)} + isDetached={isDetached} + /> + + + ); +}; + +// Layout +const PageContainer = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "center", + width: "100%", + padding: "32px 120px", + minHeight: "calc(100vh - 72px)", +}); + +const ContentContainer = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "8px", + width: "1024px", + maxWidth: "100%", +}); + +const LoadingContainer = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + height: "400px", +}); + +// Title +const TitleRow = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", + padding: "32px 8px 12px 8px", +}); + +const PageTitle = styled("h1")({ + fontFamily: "Inter, sans-serif", + fontSize: "24px", + fontWeight: 500, + fontStyle: "italic", + lineHeight: "32px", + color: canonHeaderTokens.foreground.accent0, + margin: 0, +}); + +// Section Label +const SectionLabel = styled("p")({ + fontFamily: "Inter, sans-serif", + fontSize: "11px", + fontWeight: 600, + lineHeight: "12px", + color: canonHeaderTokens.foreground.accent30, + padding: "8px", + margin: 0, + textTransform: "uppercase", +}); + +// Safe Profile Card +const SafeProfileCard = styled(Box)({ + display: "flex", + flexDirection: "column", + borderRadius: "8px", + overflow: "hidden", + backgroundColor: canonHeaderTokens.background.layer1, +}); + +const SafeProfileRow = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "24px", +}); + +const SafeProfileLeft = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "16px", +}); + +const SafeIconWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "48px", + height: "48px", + padding: "16px", + borderRadius: "12px", + backgroundColor: canonHeaderTokens.background.layer1Variation, +}); + +const SafeInfo = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "6px", +}); + +const SafeAddressRow = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "6px", +}); + +const SafeLabel = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent0, +}); + +const AddressText = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent0, +}); + +const ChainName = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +// Config Stats +const ConfigStatsRow = styled(Box)({ + borderTop: `1px dashed ${canonHeaderTokens.foreground.accent50}`, + padding: "24px", +}); + +const ConfigStatsContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "32px", +}); + +const ConfigStat = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "4px", +}); + +const ConfigStatValue = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const ConfigStatText = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 600, + lineHeight: "20px", + color: "#ffffff", +}); + +const ConfigStatLabel = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); + +const ConfigDivider = styled(Box)({ + width: "1px", + height: "40px", + backgroundColor: canonHeaderTokens.foreground.accent40, +}); + +// Status Dot +const StatusDot = styled("div")<{ $color: string }>(({ $color }) => ({ + width: "6px", + height: "6px", + borderRadius: "50%", + backgroundColor: $color, +})); + +// Buttons +const OutlineButton = styled("button")<{ $width?: string }>(({ $width }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + height: "36px", + width: $width || "226px", + padding: "8px 20px", + borderRadius: "100px", + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, + background: "transparent", + cursor: "pointer", + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 600, + letterSpacing: "0.6px", + textTransform: "uppercase", + color: canonHeaderTokens.foreground.accent10, + "&:hover": { + backgroundColor: `${canonHeaderTokens.foreground.accent40}20`, + }, +})); + +// Setting Cards +const SettingCard = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "20px", + borderRadius: "8px", + backgroundColor: canonHeaderTokens.background.layer1, +}); + +const SettingCardLeft = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "16px", +}); + +const IconCircle = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "48px", + height: "48px", + borderRadius: "1000px", + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, +}); + +const EmergencyIconCircle = styled(Box, { + shouldForwardProp: (prop) => prop !== "$isActive", +})<{ $isActive: boolean }>(({ $isActive }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "48px", + height: "48px", + borderRadius: "1000px", + backgroundColor: $isActive ? "#DA2828" : "transparent", + border: $isActive ? "none" : `1px solid ${canonHeaderTokens.foreground.accent40}`, +})); + +const SettingInfo = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "6px", +}); + +const SettingTitleRow = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "8px", +}); + +const SettingTitle = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 600, + lineHeight: "20px", + color: "#ffffff", +}); + +const StatusIndicator = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "6px", +}); + +const StatusLabel = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 600, + lineHeight: "20px", + color: "#ffffff", +}); + +const SettingDescription = styled("p")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, + margin: 0, +}); + +// Canon Guard Card (with additional address row) +const CanonGuardCard = styled(Box)({ + display: "flex", + flexDirection: "column", + padding: "20px", + borderRadius: "8px", + backgroundColor: canonHeaderTokens.background.layer1, + gap: "20px", +}); + +const CanonGuardTop = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", +}); + +const LearnMoreLink = styled("button")({ + display: "inline", + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, + textDecoration: "underline", + background: "transparent", + border: "none", + cursor: "pointer", + padding: 0, + "&:hover": { + opacity: 0.8, + }, +}); + +const CardDivider = styled(Box)({ + width: "100%", + height: "1px", + backgroundColor: canonHeaderTokens.foreground.accent40, +}); + +const GuardAddressText = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); diff --git a/src/components/VaultSetupModal.tsx b/src/components/VaultSetupModal.tsx index 9d9c26b..1d46cdd 100644 --- a/src/components/VaultSetupModal.tsx +++ b/src/components/VaultSetupModal.tsx @@ -1,126 +1,173 @@ import { useState } from "react"; -import { Modal, Box, Typography, Switch, FormControlLabel, Button, TextField } from "@mui/material"; -import { styled } from "@mui/material/styles"; +import { Box, Typography, Button, styled, CircularProgress } from "@mui/material"; import { Address, isAddress } from "viem"; -import { safeDesignTokens } from "~/config/themes/safeTheme"; -import { DEMO_SAFE_WITH_GUARD, OPTIMISM_MAINNET_RPC } from "~/constants/addresses"; - -const DEMO_DATA = { - vaultAddress: DEMO_SAFE_WITH_GUARD, - rpcUrl: OPTIMISM_MAINNET_RPC, -}; +import { SupportedChainId, SUPPORTED_CHAINS_LIST, DEFAULT_CHAIN_ID } from "~/config/chains"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { HeaderLogo } from "./Header"; +import { FormInput } from "./NewAction/shared/FormInput"; +import { + PageContainer, + SetupHeader, + SetupContentArea, + SetupFormWrapper, + SetupSectionTitle, +} from "./shared/StyledComponents"; interface VaultSetupModalProps { open: boolean; - onSubmit: (vaultAddress: Address, rpcUrl: string) => void; + onSubmit: (safeAddress: Address, chainId: SupportedChainId) => Promise; } export const VaultSetupModal = ({ open, onSubmit }: VaultSetupModalProps) => { - const [vaultAddress, setVaultAddress] = useState(""); - const [rpcUrl, setRpcUrl] = useState(""); - const [demoMode, setDemoMode] = useState(false); - - const handleDemoModeToggle = (checked: boolean) => { - setDemoMode(checked); - if (checked) { - setVaultAddress(DEMO_DATA.vaultAddress); - setRpcUrl(DEMO_DATA.rpcUrl); - } else { - setVaultAddress(""); - setRpcUrl(""); + const [safeAddress, setSafeAddress] = useState(""); + const [chainId, setChainId] = useState(DEFAULT_CHAIN_ID); + const [errors, setErrors] = useState<{ safeAddress?: string }>({}); + const [isLoading, setIsLoading] = useState(false); + + const validateInputs = (): boolean => { + const newErrors: { safeAddress?: string } = {}; + + if (!safeAddress) { + newErrors.safeAddress = "Safe address is required"; + } else if (!isAddress(safeAddress)) { + newErrors.safeAddress = "Invalid Ethereum address"; } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; }; - const handleSubmit = () => { - if (!vaultAddress || !rpcUrl || !isAddress(vaultAddress)) { + const handleSubmit = async () => { + if (!validateInputs() || isLoading) { return; } - onSubmit(vaultAddress as Address, rpcUrl); + setIsLoading(true); + try { + await onSubmit(safeAddress as Address, chainId); + } finally { + setIsLoading(false); + } }; + if (!open) return null; + return ( - - - - - Setup Canon Vault - Enter your Canon Vault address and RPC endpoint to get started - - - - handleDemoModeToggle(e.target.checked)} />} - label='Demo Mode (Use example Canon Guard Safe)' - /> - - - - setVaultAddress(e.target.value)} - disabled={demoMode} - data-testid='vault-address-input' - /> - - setRpcUrl(e.target.value)} - disabled={demoMode} - data-testid='rpc-url-input' - /> - - - - - - - - + + + { + setSafeAddress(""); + setChainId(DEFAULT_CHAIN_ID); + setErrors({}); + }} + /> + + + + + Add New Safe Account + + + + + Canon Guard sits on top of a Safe Account, so before starting, ensure that you have a Safe Account + already deployed on-chain. + + + + { + setSafeAddress(value); + if (errors.safeAddress) setErrors((prev) => ({ ...prev, safeAddress: undefined })); + }} + disabled={isLoading} + error={errors.safeAddress} + /> + + setChainId(Number(value) as SupportedChainId)} + selectOptions={SUPPORTED_CHAINS_LIST.map((chain) => ({ + label: chain.name, + value: chain.id.toString(), + }))} + disabled={isLoading} + /> + + + + + + {isLoading ? : "CONTINUE"} + + + + + + ); }; -const StyledModal = styled(Modal)({ - zIndex: 2000, +// Form card +const FormCard = styled(Box)({ + display: "flex", + flexDirection: "column", + borderRadius: "8px", + overflow: "hidden", + backgroundColor: canonHeaderTokens.background.layer1, }); -const SetupModalContainer = styled(Box)(() => ({ - position: "relative", - width: "100%", - maxWidth: "500px", - margin: safeDesignTokens.spacing.lg, -})); +const FormSection = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "32px", + padding: "24px", +}); -const SetupModalContent = styled(Box)(({ theme }) => ({ +const InfoText = styled(Typography)({ + fontSize: "13px", + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent10, +}); + +const InputsContainer = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "20px", +}); + +// Button section +const ButtonSection = styled(Box)({ display: "flex", flexDirection: "column", - gap: safeDesignTokens.spacing.md, - backgroundColor: safeDesignTokens[theme.palette.mode].surfaces.primary, - padding: safeDesignTokens.spacing.xxl, - boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)", - border: `1px solid ${safeDesignTokens[theme.palette.mode].borders.primary}`, -})); - -const SetupModalHeader = styled(Box)(() => ({ - textAlign: "center", - marginBottom: safeDesignTokens.spacing.xl, -})); - -const SetupModalTitle = styled(Typography)(({ theme }) => ({ - ...safeDesignTokens.typography.sectionTitle, - color: theme.palette.text.primary, - marginBottom: safeDesignTokens.spacing.sm, -})); - -const SetupModalSubtitle = styled(Typography)(({ theme }) => ({ - ...safeDesignTokens.typography.cardBody, - color: theme.palette.text.secondary, -})); + padding: "24px", + borderTop: `1px dashed ${canonHeaderTokens.background.layer0}`, +}); + +const ContinueButton = styled(Button)({ + width: "100%", + height: "36px", + backgroundColor: canonHeaderTokens.brand.green, + color: "#ffffff", + fontSize: "12px", + fontWeight: 600, + letterSpacing: "0.6px", + textTransform: "uppercase", + borderRadius: "100px", + border: "none", + cursor: "pointer", + "&:hover": { + backgroundColor: "#129035", + }, + "&:disabled": { + backgroundColor: canonHeaderTokens.brand.green, + opacity: 0.8, + cursor: "not-allowed", + }, +}); diff --git a/src/components/WalletConnect/WalletConnectModal.tsx b/src/components/WalletConnect/WalletConnectModal.tsx new file mode 100644 index 0000000..ce33bbf --- /dev/null +++ b/src/components/WalletConnect/WalletConnectModal.tsx @@ -0,0 +1,446 @@ +/** + * WalletConnect Modal + * Input field for pasting WalletConnect URI from dApps + */ + +import { useRef, useEffect, useState } from "react"; +import { Box, styled, CircularProgress } from "@mui/material"; +import { XIcon } from "~/components/icons"; +import { WalletConnectIcon } from "~/components/icons/WalletConnectIcon"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { useWalletConnect } from "~/providers/WalletConnectProvider"; + +export const WalletConnectModal = () => { + const { isModalOpen, closeModal, isPairing, pairingError, sessions, disconnect, pairWithUri, clearPairingError } = + useWalletConnect(); + + const [pairingCode, setPairingCode] = useState(""); + const inputRef = useRef(null); + const modalRef = useRef(null); + + // Focus input when modal opens + useEffect(() => { + if (isModalOpen && inputRef.current) { + inputRef.current.focus(); + } + }, [isModalOpen]); + + // Close modal when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (modalRef.current && !modalRef.current.contains(event.target as Node)) { + closeModal(); + } + }; + + if (isModalOpen) { + setTimeout(() => { + document.addEventListener("mousedown", handleClickOutside); + }, 0); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isModalOpen, closeModal]); + + // Close on escape key + useEffect(() => { + const handleEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") { + closeModal(); + } + }; + + if (isModalOpen) { + document.addEventListener("keydown", handleEscape); + } + + return () => { + document.removeEventListener("keydown", handleEscape); + }; + }, [isModalOpen, closeModal]); + + const handlePaste = async () => { + try { + const text = await navigator.clipboard.readText(); + setPairingCode(text); + clearPairingError(); + + // Auto-connect if it looks like a valid WC URI + if (text.startsWith("wc:")) { + await pairWithUri(text); + } + } catch (error) { + console.error("Failed to read clipboard:", error); + } + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setPairingCode(value); + clearPairingError(); + + // Auto-connect when a valid WC URI is pasted/typed + if (value.startsWith("wc:") && value.includes("@")) { + pairWithUri(value); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && pairingCode.startsWith("wc:")) { + pairWithUri(pairingCode); + } + }; + + // Handle disconnect and clear the pairing code + const handleDisconnect = async (topic: string) => { + await disconnect(topic); + setPairingCode(""); // Clear the old URI when disconnecting + }; + + if (!isModalOpen) return null; + + const hasActiveSessions = sessions.length > 0; + + return ( + + + + + + WalletConnect + + + + + + + + {/* Active Sessions Section */} + {hasActiveSessions && ( + + CONNECTED DAPPS + {sessions.map((session) => ( + + + {session.peer?.metadata?.name || "Unknown dApp"} + {session.peer?.metadata?.url || session.topic.slice(0, 20) + "..."} + + handleDisconnect(session.topic)}>Disconnect + + ))} + + )} + + {/* Pairing Code Input Section - only show when not connected */} + {!hasActiveSessions && ( + + Paste the pairing code below to connect to your dApp via WalletConnect + + + Pairing code + + + + {isPairing ? : "Paste"} + + + + + {pairingError && {pairingError}} + + + How do I connect to a dApp? + + 1. Open a dApp and click "Connect Wallet" +
+ 2. Select WalletConnect +
+ 3. Copy the pairing code or scan QR with your phone +
+ 4. Paste the code here +
+
+
+ )} +
+
+
+ ); +}; + +const Overlay = styled(Box)({ + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "rgba(0, 0, 0, 0.6)", + display: "flex", + alignItems: "center", + justifyContent: "center", + zIndex: 1000, +}); + +const ModalContainer = styled(Box)({ + width: "420px", + maxHeight: "90vh", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "16px", + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, + boxShadow: "0px 20px 25px -5px rgba(0, 0, 0, 0.3), 0px 8px 10px -6px rgba(0, 0, 0, 0.3)", + overflow: "hidden", + display: "flex", + flexDirection: "column", +}); + +const ModalHeader = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "20px 24px 16px 24px", + borderBottom: `1px solid ${canonHeaderTokens.foreground.accent40}`, +}); + +const HeaderLeft = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", +}); + +const ModalTitle = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "18px", + fontWeight: 600, + lineHeight: "24px", + color: canonHeaderTokens.foreground.accent0, +}); + +const CloseButton = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "4px", + backgroundColor: "transparent", + border: "none", + cursor: "pointer", + borderRadius: "4px", + "&:hover": { + backgroundColor: canonHeaderTokens.background.layer1Variation, + }, +}); + +const ModalContent = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "24px", + padding: "20px 24px 24px 24px", + overflowY: "auto", +}); + +const SessionsSection = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", +}); + +const SectionLabel = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "11px", + fontWeight: 600, + lineHeight: "12px", + letterSpacing: "0.5px", + color: canonHeaderTokens.foreground.accent30, + textTransform: "uppercase", +}); + +const SessionItem = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "12px 16px", + backgroundColor: canonHeaderTokens.background.layer0, + borderRadius: "8px", +}); + +const SessionInfo = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "4px", + flex: 1, + minWidth: 0, +}); + +const SessionName = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 500, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent0, + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", +}); + +const SessionUrl = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", +}); + +const DisconnectButton = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "6px 12px", + backgroundColor: "transparent", + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, + borderRadius: "6px", + cursor: "pointer", + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 500, + color: canonHeaderTokens.foreground.accent20, + transition: "all 0.2s ease", + "&:hover": { + backgroundColor: canonHeaderTokens.background.layer1Variation, + borderColor: canonHeaderTokens.foreground.accent30, + color: canonHeaderTokens.foreground.accent10, + }, +}); + +const PairingSection = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "16px", +}); + +const InstructionText = styled("p")({ + margin: 0, + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent20, +}); + +const InputWrapper = styled(Box, { + shouldForwardProp: (prop) => prop !== "$hasError", +})<{ $hasError?: boolean }>(({ $hasError }) => ({ + position: "relative", + display: "flex", + flexDirection: "column", + padding: "12px 16px", + backgroundColor: canonHeaderTokens.background.layer0, + borderRadius: "8px", + border: `1px solid ${$hasError ? "#ff6b6b" : canonHeaderTokens.brand.green}`, +})); + +const InputLabel = styled("span", { + shouldForwardProp: (prop) => prop !== "$hasError", +})<{ $hasError?: boolean }>(({ $hasError }) => ({ + position: "absolute", + top: "-8px", + left: "12px", + padding: "0 4px", + backgroundColor: canonHeaderTokens.background.layer0, + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 500, + lineHeight: "16px", + color: $hasError ? "#ff6b6b" : canonHeaderTokens.brand.green, +})); + +const InputRow = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", +}); + +const PairingInput = styled("input")({ + flex: 1, + backgroundColor: "transparent", + border: "none", + outline: "none", + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent0, + "&::placeholder": { + color: canonHeaderTokens.foreground.accent30, + }, + "&:disabled": { + opacity: 0.6, + }, +}); + +const PasteButton = styled("button")({ + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "8px 16px", + backgroundColor: canonHeaderTokens.brand.green, + border: "none", + borderRadius: "6px", + cursor: "pointer", + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 600, + lineHeight: "20px", + color: "#000", + transition: "opacity 0.2s ease", + "&:hover:not(:disabled)": { + opacity: 0.9, + }, + "&:disabled": { + opacity: 0.6, + cursor: "not-allowed", + }, +}); + +const ErrorText = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "18px", + color: "#ff6b6b", +}); + +const HelpSection = styled(Box)({ + padding: "16px", + backgroundColor: canonHeaderTokens.background.layer0, + borderRadius: "8px", +}); + +const HelpTitle = styled("span")({ + display: "block", + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 600, + lineHeight: "18px", + color: canonHeaderTokens.foreground.accent10, + marginBottom: "8px", +}); + +const HelpText = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent20, +}); diff --git a/src/components/WalletConnect/WalletConnectNavigator.tsx b/src/components/WalletConnect/WalletConnectNavigator.tsx new file mode 100644 index 0000000..b6480f9 --- /dev/null +++ b/src/components/WalletConnect/WalletConnectNavigator.tsx @@ -0,0 +1,31 @@ +/** + * WalletConnect Navigator + * Watches for pending transactions and navigates to the Arbitrary Action form + * Must be rendered inside the Router context + */ + +import { useEffect } from "react"; +import { useNavigateWithParams } from "~/hooks"; +import { useWalletConnect } from "~/providers/WalletConnectProvider"; + +export const WalletConnectNavigator = () => { + const { pendingTransaction, clearPendingTransaction } = useWalletConnect(); + const navigateWithParams = useNavigateWithParams(); + + useEffect(() => { + if (pendingTransaction) { + // Navigate to the arbitrary action form with prefilled data + navigateWithParams("/create/action/arbitrary-action", { + state: { + walletConnectTx: pendingTransaction, + }, + }); + + // Clear the pending transaction so we don't navigate again + clearPendingTransaction(); + } + }, [pendingTransaction, navigateWithParams, clearPendingTransaction]); + + // This component doesn't render anything + return null; +}; diff --git a/src/components/WalletConnect/index.ts b/src/components/WalletConnect/index.ts new file mode 100644 index 0000000..543c858 --- /dev/null +++ b/src/components/WalletConnect/index.ts @@ -0,0 +1,2 @@ +export { WalletConnectModal } from "./WalletConnectModal"; +export { WalletConnectNavigator } from "./WalletConnectNavigator"; diff --git a/src/components/icons/WalletConnectIcon.tsx b/src/components/icons/WalletConnectIcon.tsx new file mode 100644 index 0000000..8a884ff --- /dev/null +++ b/src/components/icons/WalletConnectIcon.tsx @@ -0,0 +1,18 @@ +/** + * Official WalletConnect icon + * From: https://github.com/WalletConnect/walletconnect-assets + */ + +interface WalletConnectIconProps { + size?: number; + color?: string; +} + +export const WalletConnectIcon = ({ size = 24, color = "#fff" }: WalletConnectIconProps) => ( + + + +); diff --git a/src/components/icons/index.ts b/src/components/icons/index.ts new file mode 100644 index 0000000..2a03521 --- /dev/null +++ b/src/components/icons/index.ts @@ -0,0 +1,115 @@ +/** + * Icon exports from Lucide React + * + * These icons match the Figma design system (node 119:823). + * All icons are from the Lucide icon library: https://lucide.dev/icons + * + * Usage: + * import { BoxIcon, StarIcon } from "~/components/icons"; + * + */ + +export { + // Navigation & UI + ChevronDown as ChevronDownIcon, + ChevronRight as ChevronRightIcon, + ChevronLeft as ChevronLeftIcon, + ChevronUp as ChevronUpIcon, + ArrowUpRight as ArrowUpRightIcon, + MoveRight as MoveRightIcon, + MoveLeft as MoveLeftIcon, + + // Actions + Plus as PlusIcon, + Minus as MinusIcon, + X as XIcon, + Check as CheckIcon, + CheckCheck as CheckCheckIcon, + Search as SearchIcon, + Download as DownloadIcon, + CloudDownload as CloudDownloadIcon, + Copy as CopyIcon, + Trash2 as TrashIcon, + SquarePen as SquarePenIcon, + RefreshCcw as RefreshCcwIcon, + RotateCcw as RotateCcwIcon, + Share2 as Share2Icon, + LogOut as LogOutIcon, + CircleFadingPlus as CircleFadingPlusIcon, + + // Files & Data + FileJson2 as FileJsonIcon, + Box as BoxIcon, + Layers2 as Layers2Icon, + List as ListIcon, + ListIndentIncrease as ListIndentIncreaseIcon, + ListFilter as ListFilterIcon, + + // Status & Indicators + Activity as ActivityIcon, + Circle as CircleIcon, + CircleCheck as CircleCheckIcon, + CircleCheckBig as CircleCheckBigIcon, + CircleDashed as CircleDashedIcon, + CircleDot as CircleDotIcon, + CircleArrowUp as CircleArrowUpIcon, + CircleArrowDown as CircleArrowDownIcon, + CircleHelp as HelpCircleIcon, + Info as InfoIcon, + Eye as EyeIcon, + AlertTriangle as AlertTriangleIcon, + + // Security & Protection + ShieldCheck as ShieldCheckIcon, + ShieldAlert as ShieldAlertIcon, + ShieldBan as ShieldBanIcon, + Lock as LockIcon, + LockOpen as LockOpenIcon, + + // User + UserRoundPlus as UserRoundPlusIcon, + UserRoundCheck as UserRoundCheckIcon, + + // Wallet & Finance + Wallet as WalletIcon, + WalletMinimal as WalletMinimalIcon, + PiggyBank as PiggyBankIcon, + + // Time & History + History as HistoryIcon, + TimerReset as TimerResetIcon, + ClipboardClock as ClipboardClockIcon, + + // Connectivity + Cable as CableIcon, + Link2 as Link2Icon, + Link2Off as Link2OffIcon, + Unplug as UnplugIcon, + QrCode as QrCodeIcon, + SquareArrowOutUpRight as ExternalLinkIcon, + + // Settings + Settings as SettingsIcon, + Settings2 as Settings2Icon, + Ellipsis as EllipsisIcon, + GripVertical as GripIcon, + + // Theme + Sun as SunIcon, + Moon as MoonIcon, + + // Special + Star as StarIcon, + Asterisk as AsteriskIcon, + SquareAsterisk as SquareAsteriskIcon, + Zap as ZapIcon, + ZapOff as ZapOffIcon, + Loader2 as Loader2Icon, + + // Shapes & Layout + Waypoints as VectorSquareIcon, // matches vector-square concept (nodes connected) + SquareDashedBottom as SquareDashedIcon, +} from "lucide-react"; + +// Re-export the type for convenience +export type { LucideProps as IconProps } from "lucide-react"; diff --git a/src/components/index.ts b/src/components/index.ts index cbc7cf6..43e55cc 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -4,4 +4,15 @@ export * from "./ActionCard"; export * from "./ActionColumn"; export * from "./QueueSection"; export * from "./VaultSetupModal"; +export * from "./GuardSetupWizard"; +export * from "./ErrorState"; export * from "./shared/StyledComponents"; +export * from "./shared/CopyButton"; +export * from "./shared/DurationInput"; +export * from "./shared/SafeProfileCard"; +export * from "./shared/WarningBanner"; +export * from "./NoGuardChoiceScreen"; +export * from "./DetachedGuardInput"; +export * from "./EmergencyModeBanner"; +export * from "./DetachedModeBanner"; +export * from "./DeploymentModesPanel"; diff --git a/src/components/shared/CopyButton.tsx b/src/components/shared/CopyButton.tsx new file mode 100644 index 0000000..b452a7c --- /dev/null +++ b/src/components/shared/CopyButton.tsx @@ -0,0 +1,143 @@ +import { useState, useCallback, useRef, ReactNode } from "react"; +import { Box, styled, keyframes } from "@mui/material"; +import { CopyIcon } from "~/components/icons"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; + +interface CopyableTextProps { + text: string; + children: ReactNode; + iconSize?: number; + iconColor?: string; + className?: string; +} + +/** + * CopyableText - Wraps text content with copy functionality + * Clicking anywhere on the text or icon copies to clipboard + * Shows a "Copied!" tooltip above the icon + */ +export const CopyableText = ({ + text, + children, + iconSize = 10, + iconColor = canonHeaderTokens.foreground.accent30, + className, +}: CopyableTextProps) => { + const [showTooltip, setShowTooltip] = useState(false); + const timeoutRef = useRef | null>(null); + + const handleCopy = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + navigator.clipboard.writeText(text); + + // Clear any existing timeout to prevent early dismissal + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + setShowTooltip(true); + timeoutRef.current = setTimeout(() => setShowTooltip(false), 1000); + }, + [text], + ); + + return ( + + {children} + + + + + {showTooltip && ( + + Copied! + + + )} + + + ); +}; + +// Keep CopyButton as alias for backward compatibility +export const CopyButton = CopyableText; + +const fadeIn = keyframes` + from { + opacity: 0; + transform: translateX(-50%) translateY(4px); + } + to { + opacity: 1; + transform: translateX(-50%) translateY(0); + } +`; + +const CopyableWrapper = styled(Box)({ + display: "inline-flex", + alignItems: "center", + gap: "6px", + cursor: "pointer", + "&:hover": { + "& .copyable-content": { + opacity: 0.7, + }, + }, +}); + +const TextContent = styled("span")({ + display: "inline", + transition: "opacity 0.15s ease", +}); + +const IconWrapper = styled(Box)({ + position: "relative", + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + flexShrink: 0, +}); + +const IconInner = styled(Box)({ + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + transition: "opacity 0.15s ease", +}); + +const Tooltip = styled(Box)({ + position: "absolute", + bottom: "100%", + left: "50%", + transform: "translateX(-50%)", + marginBottom: "6px", + animation: `${fadeIn} 0.15s ease-out`, + zIndex: 10000, + opacity: 1, +}); + +const TooltipText = styled("span")({ + display: "block", + padding: "4px 8px", + backgroundColor: canonHeaderTokens.background.layer1, + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, + borderRadius: "4px", + fontFamily: "Inter, sans-serif", + fontSize: "11px", + fontWeight: 500, + color: canonHeaderTokens.brand.green, + whiteSpace: "nowrap", +}); + +const TooltipArrow = styled(Box)({ + position: "absolute", + top: "100%", + left: "50%", + transform: "translateX(-50%)", + width: 0, + height: 0, + borderLeft: "5px solid transparent", + borderRight: "5px solid transparent", + borderTop: `5px solid ${canonHeaderTokens.foreground.accent40}`, +}); diff --git a/src/components/shared/DurationInput.tsx b/src/components/shared/DurationInput.tsx new file mode 100644 index 0000000..ad3e12d --- /dev/null +++ b/src/components/shared/DurationInput.tsx @@ -0,0 +1,115 @@ +import { Box, styled } from "@mui/material"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { DurationTimeUnit } from "~/utils/timeUnits"; + +const ALL_DURATION_UNITS: DurationTimeUnit[] = ["seconds", "minutes", "hours", "days", "weeks", "months"]; + +interface DurationInputProps { + value: string; + unit: DurationTimeUnit; + onValueChange: (value: string) => void; + onUnitChange: (unit: DurationTimeUnit) => void; + hasError?: boolean; + placeholder?: string; + className?: string; + /** Units to exclude from the dropdown */ + excludeUnits?: DurationTimeUnit[]; +} + +/** + * DurationInput - A reusable input for entering time durations + * Combines a number input with a unit dropdown (seconds, minutes, hours, days, weeks, months) + */ +export const DurationInput = ({ + value, + unit, + onValueChange, + onUnitChange, + hasError = false, + placeholder = "Enter duration", + className, + excludeUnits = [], +}: DurationInputProps) => { + const availableUnits = ALL_DURATION_UNITS.filter((u) => !excludeUnits.includes(u)); + + return ( + + onValueChange(e.target.value)} + placeholder={placeholder} + $hasError={hasError} + /> + onUnitChange(e.target.value as DurationTimeUnit)}> + {availableUnits.map((u) => ( + + ))} + + + ); +}; + +const DurationInputRow = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "12px", +}); + +const StyledInput = styled("input")<{ $hasError?: boolean }>(({ $hasError }) => ({ + flex: 1, + height: "40px", + padding: "0 12px", + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent0, + backgroundColor: canonHeaderTokens.background.layer0, + border: `1px solid ${$hasError ? canonHeaderTokens.status.red : canonHeaderTokens.foreground.accent40}`, + borderRadius: "6px", + outline: "none", + transition: "border-color 0.2s ease", + "&:focus": { + borderColor: $hasError ? canonHeaderTokens.status.red : canonHeaderTokens.foreground.accent20, + }, + "&::placeholder": { + color: canonHeaderTokens.foreground.accent30, + }, + // Remove number input spinners + "&::-webkit-outer-spin-button, &::-webkit-inner-spin-button": { + WebkitAppearance: "none", + margin: 0, + }, + "&[type=number]": { + MozAppearance: "textfield", + }, +})); + +const StyledSelect = styled("select")({ + height: "40px", + padding: "0 32px 0 12px", + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent0, + backgroundColor: canonHeaderTokens.background.layer0, + border: `1px solid ${canonHeaderTokens.foreground.accent40}`, + borderRadius: "6px", + outline: "none", + cursor: "pointer", + appearance: "none", + backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E")`, + backgroundRepeat: "no-repeat", + backgroundPosition: "right 10px center", + transition: "border-color 0.2s ease", + "&:focus": { + borderColor: canonHeaderTokens.foreground.accent20, + }, + "& option": { + backgroundColor: canonHeaderTokens.background.layer0, + color: canonHeaderTokens.foreground.accent0, + }, +}); diff --git a/src/components/shared/SafeProfileCard.tsx b/src/components/shared/SafeProfileCard.tsx new file mode 100644 index 0000000..fd9cccd --- /dev/null +++ b/src/components/shared/SafeProfileCard.tsx @@ -0,0 +1,100 @@ +import { Box, styled } from "@mui/material"; +import { ShieldCheckIcon } from "~/components/icons"; +import { getChainConfig } from "~/config/chains"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import { CopyableText } from "./CopyButton"; + +interface SafeProfileCardProps { + address: string; + chainId: number; +} + +/** + * Shared Safe profile card component used across setup screens + * Shows Safe address with copy functionality and chain name + */ +export const SafeProfileCard = ({ address, chainId }: SafeProfileCardProps) => { + const chainConfig = getChainConfig(chainId); + + return ( + + + + + +
+ + + + {address} + + + {chainConfig?.chain.name || "Unknown Chain"} +
+
+
+ ); +}; + +const CardContainer = styled(Box)({ + display: "flex", + flexDirection: "column", + borderRadius: "8px", + backgroundColor: canonHeaderTokens.background.layer1, +}); + +const CardContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "16px", + padding: "16px", +}); + +const IconWrapper = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "48px", + height: "48px", + borderRadius: "12px", + backgroundColor: canonHeaderTokens.background.layer1Variation, + border: `0.5px solid ${canonHeaderTokens.background.layer1Variation}`, + flexShrink: 0, +}); + +const Details = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "6px", + minWidth: 0, // Allow text truncation +}); + +const AddressRow = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "6px", +}); + +const Label = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent0, +}); + +const AddressText = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "14px", + fontWeight: 400, + lineHeight: "20px", + color: canonHeaderTokens.foreground.accent0, +}); + +const ChainName = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "13px", + fontWeight: 400, + lineHeight: "16px", + color: canonHeaderTokens.foreground.accent20, +}); diff --git a/src/components/shared/StyledComponents.tsx b/src/components/shared/StyledComponents.tsx index 22e2343..a69c10a 100644 --- a/src/components/shared/StyledComponents.tsx +++ b/src/components/shared/StyledComponents.tsx @@ -1,27 +1,108 @@ -import { styled, Box, Card, Typography, Chip, alpha } from "@mui/material"; -import { safeDesignTokens } from "~/config/themes/safeTheme"; +import { styled, Box, Card, Typography, Chip, alpha, Tooltip, tooltipClasses } from "@mui/material"; +import { safeDesignTokens, canonHeaderTokens } from "~/config/themes/safeTheme"; +import type { TooltipProps } from "@mui/material"; -export const SafePageContainer = styled(Box)(({ theme }) => ({ - display: "flex", - height: "100vh", - overflow: "hidden", - backgroundColor: safeDesignTokens[theme.palette.mode].surfaces.secondary, -})); +/** + * Styled tooltip with consistent dark theme styling + * Use this across the app for all tooltips + */ +export const StyledTooltip = styled(({ className, ...props }: TooltipProps) => ( + +))({ + [`& .${tooltipClasses.tooltip}`]: { + backgroundColor: "#37373e", + color: "#b5b5b7", + fontSize: "13px", + fontWeight: 400, + lineHeight: "20px", + padding: "12px 16px", + borderRadius: "6px", + maxWidth: "347px", + boxShadow: "0px 20px 25px -5px rgba(0,0,0,0.1), 0px 8px 10px -6px rgba(0,0,0,0.1)", + }, +}); -export const SafeMainContent = styled(Box, { - shouldForwardProp: (prop) => prop !== "sidebarCollapsed", -})<{ sidebarCollapsed?: boolean }>(({ theme, sidebarCollapsed }) => ({ - flexGrow: 1, +// Page container with vertical flex layout (header + content) +export const PageContainer = styled(Box)({ + display: "flex", + flexDirection: "column", + minHeight: "100vh", width: "100%", - backgroundColor: safeDesignTokens[theme.palette.mode].surfaces.secondary, + backgroundColor: canonHeaderTokens.background.layer0, +}); + +// Main content area below header +export const MainContent = styled(Box)({ + flex: 1, overflow: "auto", - transition: safeDesignTokens.components.sidebar.transition, - marginLeft: 0, - [theme.breakpoints.up("md")]: { - marginLeft: 0, - paddingLeft: sidebarCollapsed ? safeDesignTokens.sizes.sidebar.collapsed : safeDesignTokens.sizes.sidebar.expanded, - }, -})); + backgroundColor: canonHeaderTokens.background.layer0, +}); + +// Legacy exports for backwards compatibility (will be removed in future) +export const SafePageContainer = PageContainer; +export const SafeMainContent = MainContent; + +// ============================================ +// Setup Flow Layout Components +// ============================================ + +/** + * Header bar for setup screens (72px height, layer1 background) + */ +export const SetupHeader = styled(Box)({ + display: "flex", + alignItems: "center", + height: "72px", + backgroundColor: canonHeaderTokens.background.layer1, + width: "100%", +}); + +/** + * Centered content area for setup screens + */ +export const SetupContentArea = styled(Box)({ + flex: 1, + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: "32px 24px 64px", +}); + +/** + * Form wrapper with max-width constraint (576px) + */ +export const SetupFormWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "8px", + width: "100%", + maxWidth: "576px", +}); + +/** + * Section title for setup screens (uppercase, small text) + */ +export const SetupSectionTitle = styled("h2")({ + fontFamily: "Inter, sans-serif", + fontSize: "12px", + fontWeight: 600, + letterSpacing: "0.6px", + textTransform: "uppercase", + color: canonHeaderTokens.foreground.accent30, + padding: "8px", + margin: 0, +}); + +/** + * Card container for setup screens + */ +export const SetupCard = styled(Box)({ + display: "flex", + flexDirection: "column", + borderRadius: "8px", + overflow: "hidden", + backgroundColor: canonHeaderTokens.background.layer1, +}); export const SafeActionCard = styled(Card, { shouldForwardProp: (prop) => prop !== "isPreApproved", @@ -34,7 +115,7 @@ export const SafeActionCard = styled(Card, { marginBottom: safeDesignTokens.spacing.md, border: `${safeDesignTokens.sizes.card.borderWidth} solid ${borderColor}`, minHeight: "80px", - backgroundColor: safeDesignTokens[theme.palette.mode].surfaces.elevated, + backgroundColor: canonHeaderTokens.background.layer1, [theme.breakpoints.down("sm")]: { marginBottom: safeDesignTokens.spacing.xs, minHeight: "60px", @@ -49,22 +130,19 @@ export const SafeCardContent = styled(Box)(({ theme }) => ({ }, })); -export const SafeCardTitle = styled(Typography)(({ theme }) => ({ +export const SafeCardTitle = styled(Typography)({ ...safeDesignTokens.typography.cardTitle, - color: theme.palette.text.primary, + color: canonHeaderTokens.foreground.accent0, fontSize: "1.125rem", fontWeight: 600, lineHeight: 1.4, marginBottom: safeDesignTokens.spacing.xs, - [theme.breakpoints.down("sm")]: { - fontSize: "1rem", - }, -})); +}); -export const SafeCardBody = styled(Typography)(({ theme }) => ({ +export const SafeCardBody = styled(Typography)({ ...safeDesignTokens.typography.cardBody, - color: theme.palette.text.secondary, -})); + color: canonHeaderTokens.foreground.accent10, +}); export const SafeStatusChip = styled(Chip, { shouldForwardProp: (prop) => prop !== "isPreApproved", @@ -88,7 +166,7 @@ export const SafeStatusChip = styled(Chip, { export const SafeAddress = styled(Typography)(({ theme }) => ({ fontFamily: "monospace", fontSize: "0.875rem", - color: theme.palette.text.secondary, + color: canonHeaderTokens.foreground.accent10, cursor: "pointer", padding: `${safeDesignTokens.spacing.xs} ${safeDesignTokens.spacing.sm}`, backgroundColor: alpha(theme.palette.primary.main, 0.04), @@ -96,6 +174,6 @@ export const SafeAddress = styled(Typography)(({ theme }) => ({ transition: "all 0.2s ease-in-out", "&:hover": { backgroundColor: alpha(theme.palette.primary.main, 0.08), - color: theme.palette.primary.main, + color: canonHeaderTokens.brand.green, }, })); diff --git a/src/components/shared/WarningBanner.tsx b/src/components/shared/WarningBanner.tsx new file mode 100644 index 0000000..3546636 --- /dev/null +++ b/src/components/shared/WarningBanner.tsx @@ -0,0 +1,61 @@ +import { ReactNode } from "react"; +import { Box, styled } from "@mui/material"; + +interface WarningBannerProps { + backgroundColor: string; + children: ReactNode; +} + +/** + * Generic Warning Banner - displays a colored banner at the top of the app. + * Used for emergency mode and detached mode warnings. + * Accepts children for flexible content including inline links. + */ +export const WarningBanner = ({ backgroundColor, children }: WarningBannerProps) => { + return ( + + {children} + + ); +}; + +/** + * Styled link component for use within WarningBanner + */ +export const BannerLink = styled("button")({ + fontFamily: "Inter, sans-serif", + fontSize: "10px", + fontWeight: 600, + lineHeight: "12px", + letterSpacing: "1px", + textTransform: "uppercase", + color: "#ffffff", + background: "none", + border: "none", + cursor: "pointer", + textDecoration: "underline", + padding: 0, + "&:hover": { + opacity: 0.8, + }, +}); + +const BannerContainer = styled(Box)({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "100%", + padding: "8px 16px", + gap: "6px", +}); + +const BannerContent = styled("span")({ + fontFamily: "Inter, sans-serif", + fontSize: "10px", + fontWeight: 600, + lineHeight: "12px", + letterSpacing: "1px", + textTransform: "uppercase", + color: "#ffffff", + textAlign: "center", +}); diff --git a/src/components/sidebar/SidebarFooter.tsx b/src/components/sidebar/SidebarFooter.tsx deleted file mode 100644 index 44b8d29..0000000 --- a/src/components/sidebar/SidebarFooter.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { AccountBalanceWallet } from "@mui/icons-material"; -import { Box, Button, IconButton, styled, Tooltip } from "@mui/material"; - -interface SidebarFooterProps { - collapsed: boolean; -} - -export const SidebarFooter = ({ collapsed }: SidebarFooterProps) => { - return ( - - {collapsed && ( - - - - - - - - )} - {!collapsed && ( - - )} - - ); -}; - -const StyledFooter = styled(Box)(({ theme }) => ({ - padding: theme.spacing(2), - marginTop: "auto", - borderTop: `1px solid ${theme.palette.divider}`, -})); - -const StyledSpan = styled("span")({ - width: "100%", -}); - -const StyledIconButton = styled(IconButton)({ - width: "100%", -}); diff --git a/src/components/sidebar/SidebarHeader.tsx b/src/components/sidebar/SidebarHeader.tsx deleted file mode 100644 index 5e4da59..0000000 --- a/src/components/sidebar/SidebarHeader.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Menu as MenuIcon, DarkMode as DarkModeIcon, LightMode as LightModeIcon } from "@mui/icons-material"; -import { Box, Typography, IconButton, styled } from "@mui/material"; -import { useColorScheme } from "@mui/material/styles"; - -interface SidebarHeaderProps { - collapsed: boolean; - onToggleCollapse: () => void; -} - -export const SidebarHeader = ({ collapsed, onToggleCollapse }: SidebarHeaderProps) => { - const { mode, setMode } = useColorScheme(); - - const changeTheme = () => { - setMode(mode === "dark" ? "light" : "dark"); - }; - - return ( - - {!collapsed && ( - - Canon Vault - - )} - - - - {!collapsed && ( - - {mode === "dark" ? : } - - )} - - ); -}; - -const StyledHeader = styled(Box)(() => ({ - padding: 12, - borderBottom: 1, - borderColor: "divider", - display: "flex", - alignItems: "center", - justifyContent: "space-between", -})); diff --git a/src/components/sidebar/SidebarNavigation.tsx b/src/components/sidebar/SidebarNavigation.tsx deleted file mode 100644 index fdb544b..0000000 --- a/src/components/sidebar/SidebarNavigation.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Queue, CheckCircle, History, Settings } from "@mui/icons-material"; -import { List, ListItem, ListItemButton, ListItemIcon, ListItemText, IconButton, Tooltip, styled } from "@mui/material"; -import { TabType } from "~/types/canon-guard"; - -const navigationItems = [ - { id: TabType.QUEUE, label: "Queue", icon: }, - { id: TabType.PRE_APPROVED, label: "Pre-approved", icon: }, - { id: TabType.HISTORY, label: "History", icon: }, - { id: TabType.CONFIGURATION, label: "Configuration", icon: }, -]; - -interface SidebarNavigationProps { - activeTab: TabType; - onTabChange: (tab: TabType) => void; - collapsed: boolean; -} - -export const SidebarNavigation = ({ activeTab, onTabChange, collapsed }: SidebarNavigationProps) => { - return ( - - {navigationItems.map((item) => ( - - {collapsed ? ( - - onTabChange(item.id)} $isActive={activeTab === item.id}> - {item.icon} - - - ) : ( - onTabChange(item.id)}> - {item.icon} - - - )} - - ))} - - ); -}; - -const StyledList = styled(List)({ - flex: 1, - paddingTop: 0, - paddingBottom: 0, -}); - -const StyledIconButton = styled(IconButton)<{ $isActive?: boolean }>(({ theme, $isActive }) => ({ - width: "100%", - borderRadius: 0, - color: $isActive ? theme.palette.primary.main : theme.palette.text.secondary, - backgroundColor: $isActive ? theme.palette.action.selected : "transparent", -})); - -const StyledListItemButton = styled(ListItemButton)(({ theme }) => ({ - borderRadius: 0, - "&.Mui-selected": { - backgroundColor: theme.palette.action.selected, - color: theme.palette.primary.main, - "& .MuiListItemIcon-root": { - color: theme.palette.primary.main, - }, - }, -})); diff --git a/src/components/sidebar/SidebarSafeInfo.tsx b/src/components/sidebar/SidebarSafeInfo.tsx deleted file mode 100644 index dad8566..0000000 --- a/src/components/sidebar/SidebarSafeInfo.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import React, { useMemo, useCallback } from "react"; -import { ContentCopy, OpenInNew, Explore, Clear } from "@mui/icons-material"; -import { Box, Menu, MenuItem, ListItemIcon, ListItemText, styled } from "@mui/material"; -import { Chain, mainnet, optimism } from "viem/chains"; -import { useStateContext } from "~/hooks/useStateContext"; -import { VaultInfo } from "~/types/canon-guard"; -import { SidebarSafeInfoCollapsed, SidebarSafeInfoExpanded } from "./SidebarSafeInfoParts"; - -const getSafeNetworkPrefix = (chain: Chain): string => { - switch (chain.id) { - case optimism.id: - return "oeth"; - case mainnet.id: - return "eth"; - default: - return "eth"; - } -}; - -const createMenuItems = ( - safeInfo: VaultInfo, - clearVaultConfig: () => void, - handleMenuClose: () => void, - chain: Chain, -) => { - const safeNetworkPrefix = getSafeNetworkPrefix(chain); - - return [ - { - icon: , - label: "Copy Address", - onClick: () => { - navigator.clipboard.writeText(safeInfo.address); - handleMenuClose(); - }, - }, - { - icon: , - label: "View on Safe", - onClick: () => { - window.open(`https://app.safe.global/home?safe=${safeNetworkPrefix}:${safeInfo.address}`, "_blank"); - handleMenuClose(); - }, - }, - { - icon: , - label: "View on Explorer", - onClick: () => { - window.open(`${chain.blockExplorers?.default?.url}/address/${safeInfo.address}`, "_blank"); - handleMenuClose(); - }, - }, - { - icon: , - label: "Clear Vault Configuration", - onClick: () => { - clearVaultConfig(); - handleMenuClose(); - }, - }, - ]; -}; - -interface SidebarSafeInfoProps { - safeInfo: VaultInfo; - collapsed: boolean; - chain: Chain; -} - -export const SidebarSafeInfo = ({ safeInfo, collapsed, chain }: SidebarSafeInfoProps) => { - const { clearVaultConfig } = useStateContext(); - const [anchorEl, setAnchorEl] = React.useState(null); - - const handleMenuClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - - const handleMenuClose = useCallback(() => { - setAnchorEl(null); - }, []); - - const menuItems = useMemo( - () => createMenuItems(safeInfo, clearVaultConfig, handleMenuClose, chain), - [safeInfo, clearVaultConfig, handleMenuClose, chain], - ); - - return ( - <> - - {collapsed && } - - {!collapsed && } - - - - {menuItems.map((item, index) => ( - - {item.icon} - - - ))} - - - ); -}; - -const SafeInfoContainer = styled(Box)(({ theme }) => ({ - padding: theme.spacing(2), - borderBottom: `1px solid ${theme.palette.divider}`, - display: "flex", - flexDirection: "column", - gap: theme.spacing(1), -})); - -const StyledMenuItem = styled(MenuItem)({ - fontSize: "0.875rem", -}); - -const StyledListItemIcon = styled(ListItemIcon)({ - minWidth: 32, -}); diff --git a/src/components/sidebar/SidebarSafeInfoParts.tsx b/src/components/sidebar/SidebarSafeInfoParts.tsx deleted file mode 100644 index 83e85d8..0000000 --- a/src/components/sidebar/SidebarSafeInfoParts.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import React from "react"; -import { MoreVert } from "@mui/icons-material"; -import { Box, Typography, IconButton, Chip, Tooltip, useTheme, styled } from "@mui/material"; -import { optimism } from "viem/chains"; -import { VaultInfo } from "~/types/canon-guard"; -import { truncateAddress } from "~/utils"; -import safeLogoBlack from "~/assets/safe-logo-black.png"; -import safeLogoWhite from "~/assets/safe-logo-white.png"; - -interface SidebarSafeInfoPartProps { - safeInfo: VaultInfo; - onMenuClick: (event: React.MouseEvent) => void; -} - -export const SidebarSafeInfoCollapsed = ({ safeInfo, onMenuClick }: SidebarSafeInfoPartProps) => { - const theme = useTheme(); - - return ( - <> - - - - - - - - - - - - ); -}; - -export const SidebarSafeInfoExpanded = ({ safeInfo, onMenuClick }: SidebarSafeInfoPartProps) => { - const theme = useTheme(); - - const formatAddress = (address: string) => truncateAddress(address); - - return ( - <> - - - - - - Canon Vault - navigator.clipboard.writeText(safeInfo.address)}> - {formatAddress(safeInfo.address)} - - - - - - - - - - Network - - - - Threshold - - {safeInfo.threshold}/{safeInfo.totalOwners} - - - - - ); -}; - -const SafeLogoWrapper = styled(Box)(() => ({ - width: 32, - height: 32, - borderRadius: "50%", - overflow: "hidden", - flexShrink: 0, -})); - -const SafeLogo = styled("img")(() => ({ - width: "100%", - height: "100%", - objectFit: "cover", -})); - -const SafeHeader = styled(Box)(() => ({ - display: "flex", - alignItems: "center", - gap: 12, -})); - -const SafeDetails = styled(Box)(() => ({ - flex: 1, - minWidth: 0, - display: "flex", - flexDirection: "column", -})); - -const SafeTitle = styled(Typography)(({ theme }) => ({ - fontWeight: 600, - color: theme.palette.text.primary, - lineHeight: 1.2, -})); - -const SafeAddress = styled(Typography)(({ theme }) => ({ - fontFamily: "monospace", - color: theme.palette.text.secondary, - cursor: "pointer", - "&:hover": { - color: theme.palette.primary.main, - }, -})); - -const SafeMetrics = styled(Box)(({ theme }) => ({ - display: "flex", - justifyContent: "space-between", - gap: theme.spacing(1), - marginTop: theme.spacing(1), -})); - -const MetricItem = styled(Box)(() => ({ - display: "flex", - flexDirection: "column", - alignItems: "center", - flex: 1, -})); - -const MetricLabel = styled(Typography)(({ theme }) => ({ - color: theme.palette.text.secondary, - marginBottom: 4, -})); - -const MetricValue = styled(Typography)(({ theme }) => ({ - fontWeight: 600, - color: theme.palette.text.primary, -})); - -const NetworkChip = styled(Chip)(({ theme }) => ({ - height: 20, - fontSize: "0.75rem", - color: theme.palette.primary.main, - backgroundColor: theme.palette.primary.main + "20", - "& .MuiChip-label": { - padding: "0 6px", - }, -})); diff --git a/src/config/chains.ts b/src/config/chains.ts new file mode 100644 index 0000000..c0c80d0 --- /dev/null +++ b/src/config/chains.ts @@ -0,0 +1,112 @@ +/** + * Chain Configuration - Defines supported chains and their RPC endpoints + * + * RPC URLs are loaded from environment variables to keep sensitive data out of code. + * To add a new chain: + * 1. Add the RPC URL env var (VITE_RPC_CHAINNAME) + * 2. Add the chain to SupportedChainId enum + * 3. Add the chain config to SUPPORTED_CHAINS + */ + +import { Chain } from "viem"; +import { mainnet, optimism } from "viem/chains"; + +/** + * Enum of supported chain IDs for type safety + */ +export enum SupportedChainId { + ETHEREUM = 1, + OPTIMISM = 10, +} + +/** + * Configuration for a supported chain + */ +export interface ChainConfig { + id: SupportedChainId; + name: string; + shortName: string; + chain: Chain; + rpcUrl: string; + blockExplorerUrl: string; + iconColor: string; // For UI display +} + +/** + * Get RPC URL from environment variables with fallback + */ +const getRpcUrl = (envVar: string, fallback: string): string => { + const url = import.meta.env[envVar]; + return url || fallback; +}; + +/** + * Map of supported chains with their configurations + */ +export const SUPPORTED_CHAINS: Record = { + [SupportedChainId.ETHEREUM]: { + id: SupportedChainId.ETHEREUM, + name: "Ethereum Mainnet", + shortName: "Ethereum", + chain: mainnet, + rpcUrl: getRpcUrl("VITE_RPC_ETHEREUM", "https://eth-mainnet.g.alchemy.com/v2/J9qI8VqN68cHHr4-b4wB2"), + blockExplorerUrl: "https://etherscan.io", + iconColor: "#627EEA", + }, + [SupportedChainId.OPTIMISM]: { + id: SupportedChainId.OPTIMISM, + name: "OP Mainnet", + shortName: "OP Mainnet", + chain: optimism, + rpcUrl: getRpcUrl("VITE_RPC_OPTIMISM", "https://opt-mainnet.g.alchemy.com/v2/J9qI8VqN68cHHr4-b4wB2"), + blockExplorerUrl: "https://optimistic.etherscan.io", + iconColor: "#FF0420", + }, +}; + +/** + * Array of supported chains for iteration (e.g., dropdowns) + */ +export const SUPPORTED_CHAINS_LIST: ChainConfig[] = Object.values(SUPPORTED_CHAINS); + +/** + * Default chain ID + */ +export const DEFAULT_CHAIN_ID = SupportedChainId.ETHEREUM; + +/** + * Get chain config by ID + */ +export const getChainConfig = (chainId: SupportedChainId): ChainConfig => { + return SUPPORTED_CHAINS[chainId]; +}; + +/** + * Get RPC URL for a chain + */ +export const getRpcUrlForChain = (chainId: SupportedChainId): string => { + return SUPPORTED_CHAINS[chainId].rpcUrl; +}; + +/** + * Get viem Chain object for a chain ID + */ +export const getViemChain = (chainId: SupportedChainId): Chain => { + return SUPPORTED_CHAINS[chainId].chain; +}; + +/** + * Check if a chain ID is supported + */ +export const isSupportedChain = (chainId: number): chainId is SupportedChainId => { + return chainId in SUPPORTED_CHAINS; +}; + +/** + * Parse chain ID from string (for URL params) + */ +export const parseChainId = (value: string | null): SupportedChainId | null => { + if (!value) return null; + const parsed = parseInt(value, 10); + return isSupportedChain(parsed) ? parsed : null; +}; diff --git a/src/config/factories.ts b/src/config/factories.ts new file mode 100644 index 0000000..555d587 --- /dev/null +++ b/src/config/factories.ts @@ -0,0 +1,23 @@ +/** + * Known Canon Guard Factory Addresses + * + * This list contains all legitimate Canon Guard factory addresses + * that are used to validate Canon Guard deployments. + * + * When validating a Canon Guard: + * 1. We call PARENT() on the guard to get its factory + * 2. We check if that factory is in this list + * 3. We verify isChild() on the factory + */ + +import { Address } from "viem"; +import { CANON_GUARD_FACTORY } from "../constants/addresses"; + +/** + * List of known Canon Guard factory addresses across all supported chains. + * These are deployed via CREATE2 and have the same address on all chains. + */ +export const KNOWN_CANON_GUARD_FACTORIES: Address[] = [ + // Main Canon Guard Factory (deployed via CREATE2) + CANON_GUARD_FACTORY, +]; diff --git a/src/config/index.ts b/src/config/index.ts index e1f4b02..71d8f83 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -8,3 +8,5 @@ export const getConfig = (): Config => ({ constants: getConstants(), customThemes: getCustomThemes(), }); + +export * from "./chains"; diff --git a/src/config/themes/safeTheme.ts b/src/config/themes/safeTheme.ts index 18309e9..7321ddd 100644 --- a/src/config/themes/safeTheme.ts +++ b/src/config/themes/safeTheme.ts @@ -45,9 +45,43 @@ type ThemeTokens = { primary: string; secondary: string; tertiary: string; + muted: string; }; }; +// Figma design tokens for Canon Guard header +export const canonHeaderTokens = { + background: { + layer0: "#111114", + layer1: "#24242b", + layer1Variation: "#202026", + }, + foreground: { + accent0: "#f9f9fa", + accent10: "#b5b5b7", + accent20: "#858589", + accent30: "#505057", + accent40: "#37373e", + accent50: "#111114", + }, + brand: { + green: "#15a43e", + greenLight: "#149b3a", + }, + status: { + red: "#da2828", + greenTransparent10: "rgba(21, 164, 62, 0.1)", + greenTransparent50: "rgba(21, 164, 62, 0.5)", + amber: "#e1ab11", + amberLight: "rgba(225, 171, 17, 0.5)", + }, + amber: { + base: "#e1ab11", + border: "rgba(225, 171, 17, 0.1)", + text: "rgba(225, 171, 17, 0.5)", + }, +}; + export const safeDesignTokens: Record & { spacing: { xs: string; @@ -195,58 +229,60 @@ export const safeDesignTokens: Record & { primary: "#111827", secondary: "#6b7280", tertiary: "#9ca3af", + muted: "#d1d5db", }, }, dark: { brand: { primary: { - main: "#6366f1", // Indigo - light: "#a5b4fc", - dark: "#4338ca", + main: "#15a43e", // Canon Green + light: "#149b3a", + dark: "#0d7a2d", contrast: "#ffffff", }, secondary: { - main: "#8b5cf6", // Purple - light: "#c4b5fd", - dark: "#7c3aed", + main: "#15a43e", // Canon Green + light: "#149b3a", + dark: "#0d7a2d", contrast: "#ffffff", }, }, actionStatus: { preApproved: { - main: "#10b981", // Emerald - light: "#d1fae5", - dark: "#047857", + main: "#15a43e", // Canon Green + light: "#149b3a", + dark: "#0d7a2d", contrast: "#ffffff", - surface: "#ecfdf5", + surface: "#111114", }, notPreApproved: { main: "#ef4444", // Red light: "#fecaca", dark: "#dc2626", contrast: "#ffffff", - surface: "#fef2f2", + surface: "#111114", }, }, surfaces: { - primary: "#1f2937", - secondary: "#111827", - elevated: "#374151", + primary: "#24242b", // layer-1 (header/cards) + secondary: "#111114", // layer-0 (page background) + elevated: "#24242b", // layer-1 }, borders: { - primary: "#4b5563", - secondary: "#374151", - accent: "#6b7280", + primary: "#505057", // accent-30 + secondary: "#24242b", // layer-1 + accent: "#858589", // accent-20 }, text: { - primary: "#f9fafb", - secondary: "#d1d5db", - tertiary: "#9ca3af", + primary: "#f9f9fa", // accent-0 + secondary: "#b5b5b7", // accent-10 + tertiary: "#858589", // accent-20 + muted: "#505057", // accent-30 }, }, diff --git a/src/config/wagmiConfig.ts b/src/config/wagmiConfig.ts index 2d3cd59..12544b1 100644 --- a/src/config/wagmiConfig.ts +++ b/src/config/wagmiConfig.ts @@ -1,11 +1,11 @@ import { connectorsForWallets } from "@rainbow-me/rainbowkit"; import { rainbowWallet, walletConnectWallet, injectedWallet } from "@rainbow-me/rainbowkit/wallets"; import { createConfig, http, cookieStorage, createStorage } from "wagmi"; -import { sepolia } from "wagmi/chains"; +import { mainnet, optimism } from "wagmi/chains"; import { getConfig } from "~/config"; +import { SUPPORTED_CHAINS, SupportedChainId } from "~/config/chains"; -const { PROJECT_ID, ALCHEMY_KEY, IS_PLAYWRIGHT } = getConfig().env; -const { RPC_URL_TESTING } = getConfig().constants; +const { PROJECT_ID } = getConfig().env; const getWallets = () => { if (PROJECT_ID) { @@ -23,19 +23,20 @@ const connectors = connectorsForWallets( }, ], { - appName: "Web3 React boilerplate", + appName: "Canon Guard", projectId: PROJECT_ID, }, ); export const config = createConfig({ - chains: [sepolia], + chains: [mainnet, optimism], ssr: true, storage: createStorage({ storage: cookieStorage, }), transports: { - [sepolia.id]: IS_PLAYWRIGHT ? http(RPC_URL_TESTING) : ALCHEMY_KEY ? http(ALCHEMY_KEY) : http(), + [mainnet.id]: http(SUPPORTED_CHAINS[SupportedChainId.ETHEREUM].rpcUrl), + [optimism.id]: http(SUPPORTED_CHAINS[SupportedChainId.OPTIMISM].rpcUrl), }, batch: { multicall: true }, connectors, diff --git a/src/constants/addresses.ts b/src/constants/addresses.ts index 8aa642f..fc562de 100644 --- a/src/constants/addresses.ts +++ b/src/constants/addresses.ts @@ -1,19 +1,15 @@ /** - * Known addresses and RPC URLs for the Canon Guard UI + * Known addresses for the Canon Guard UI */ import { Address } from "viem"; -// Demo/Example Safe addresses (real deployed addresses on Optimism mainnet) -export const DEMO_SAFE_WITH_GUARD: Address = "0x275b3926a58AA47Ba66B53725c7ceF9A3D725157"; -export const DEMO_GUARD_ADDRESS: Address = "0xfba6ab4dfca44973d52014d16c00053de53a0c26"; -export const DEMO_SAFE_NO_GUARD: Address = "0xCec63a937C7daa0147b350fF09E4f1889b64227b"; +// Canon Guard Factory address (deployed via CREATE2, same on all supported chains) +// From: contracts/scripts/Constants.s.sol +export const CANON_GUARD_FACTORY: Address = "0x656c264F914bd8Fe7bbAfb9B4F2EBcB4f259F67C"; -// Known owner of demo Safe -export const DEMO_SAFE_OWNER: Address = "0xd550780b24C8c25ef1471773498dcb63eF415298"; // EOA +// MultiSendCallOnly address (Safe standard deployment) +export const MULTI_SEND_CALL_ONLY: Address = "0x9641d764fc13c8B624c04430C7356C1C7C8102e2"; // Known contract addresses for testing/validation export const USDC_OPTIMISM: Address = "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"; - -// RPC URLs -export const OPTIMISM_MAINNET_RPC = "https://mainnet.optimism.io"; diff --git a/src/constants/canonGuard.ts b/src/constants/canonGuard.ts new file mode 100644 index 0000000..92b9df5 --- /dev/null +++ b/src/constants/canonGuard.ts @@ -0,0 +1,124 @@ +import { Address } from "viem"; +import { ActionFactoryType, HubFactoryType } from "../types/canon-guard"; + +// Delay in milliseconds to wait after a transaction is confirmed before refetching data. +// This gives RPC nodes time to index the new blockchain state. +export const RPC_INDEXING_DELAY_MS = 2000; + +// Core contract addresses (deployed via CREATE2, same across all chains) +export const CANON_GUARD_REGISTRY: Address = "0x1d6f006964fBDf260B06cA38283Ec952B51f4f84"; +export const PRE_APPROVE_ACTION_FACTORY: Address = "0x2A62b0644BA7F4648179BfAE9a279D63DC44eF4a"; +export const SIMPLE_TRANSFERS_FACTORY: Address = "0xC2E8c09Eb985Dd34285bc154D1B6886e6886aE54"; +export const ARBITRARY_ACTIONS_FACTORY: Address = "0xD1F0e9D34B292E3EB27324F041f28929dBa41845"; +export const ALLOWANCE_CLAIMOR_FACTORY: Address = "0x6636eDd0125880677f3a1f7411555ea64411d60E"; +export const CAPPED_TOKEN_TRANSFERS_HUB_FACTORY: Address = "0x8531f72986374445507c29A0753fcc9cA36468D0"; +export const CHANGE_SAFE_GUARD_ACTION_FACTORY: Address = "0xE4Fd8EBFC17aA71b41E15143D35FCE6F48cB1a38"; + +// Canon Guard factory addresses from scripts/scripts/Constants.s.sol +// These are deployed via CREATE2 and share the same addresses across all supported chains +export const KNOWN_FACTORY_MAPPINGS: Record = { + // Core factories (all chains) + "0x656c264F914bd8Fe7bbAfb9B4F2EBcB4f259F67C": { + type: ActionFactoryType.SAFE_ENTRYPOINT, + label: "Canon Guard Factory", + }, + "0x6636eDd0125880677f3a1f7411555ea64411d60E": { + type: ActionFactoryType.ALLOWANCE_CLAIMOR, + label: "Allowance Claimor Factory", + }, + "0x2A62b0644BA7F4648179BfAE9a279D63DC44eF4a": { + type: ActionFactoryType.APPROVE_ACTION, + label: "Pre-Approve Action Factory", + }, + "0x8531f72986374445507c29A0753fcc9cA36468D0": { + type: ActionFactoryType.CAPPED_TOKEN_TRANSFERS, + label: "Capped Token Transfers Hub Factory", + }, + "0xD1F0e9D34B292E3EB27324F041f28929dBa41845": { + type: ActionFactoryType.ARBITRARY_ACTIONS, + label: "Arbitrary Actions Factory", + }, + "0xC2E8c09Eb985Dd34285bc154D1B6886e6886aE54": { + type: ActionFactoryType.SIMPLE_TRANSFERS, + label: "Simple Transfers Factory", + }, + "0xE4Fd8EBFC17aA71b41E15143D35FCE6F48cB1a38": { + type: ActionFactoryType.CHANGE_SAFE_GUARD, + label: "Change Safe Guard Action Factory", + }, + "0xDeA9DC1E5f5ac14A923A198349e3D175C3F8D175": { + type: ActionFactoryType.SET_EMERGENCY_CALLER, + label: "Set Emergency Caller Action Factory", + }, + "0x701f4342800ddF69F00f7be7d32240d70E586568": { + type: ActionFactoryType.SET_EMERGENCY_TRIGGER, + label: "Set Emergency Trigger Action Factory", + }, + // Chain-specific factories + // Ethereum Mainnet only + "0xB926E033a519bC7e073aaC9B8eD81b2D6D6C48Cd": { + type: ActionFactoryType.EVERCLEAR_TOKEN_CONVERSION, + label: "Everclear Token Conversion Factory", + }, + // OP Mainnet only + "0x4161480e48C715e076916a4Dce70A61c74193D37": { + type: ActionFactoryType.OPX_ACTION, + label: "OPx Action Factory", + }, +}; + +export const FACTORY_ADDRESSES = Object.keys(KNOWN_FACTORY_MAPPINGS) as Address[]; + +// Hub factory addresses - entities whose PARENT() returns one of these are hubs +export const KNOWN_HUB_FACTORIES: Record = { + "0x8531f72986374445507c29A0753fcc9cA36468D0": { + type: HubFactoryType.CAPPED_TOKEN_TRANSFERS_HUB, + label: "Capped Token Transfers Hub", + }, +}; + +export const HUB_FACTORY_ADDRESSES = Object.keys(KNOWN_HUB_FACTORIES) as Address[]; + +export const isHubFactory = (parentAddress: Address): boolean => { + const lowerParent = parentAddress.toLowerCase(); + return HUB_FACTORY_ADDRESSES.some((addr) => addr.toLowerCase() === lowerParent); +}; + +export const getHubFactoryType = (parentAddress: Address): HubFactoryType | null => { + const lowerParent = parentAddress.toLowerCase(); + for (const [addr, mapping] of Object.entries(KNOWN_HUB_FACTORIES)) { + if (addr.toLowerCase() === lowerParent) { + return mapping.type; + } + } + return null; +}; + +export const getFactoryType = (actionBuilderAddress: Address): ActionFactoryType => { + const mapping = KNOWN_FACTORY_MAPPINGS[actionBuilderAddress]; + return mapping?.type || ActionFactoryType.UNKNOWN; +}; + +export const FACTORY_TYPE_TO_LABEL: Record = { + [ActionFactoryType.SAFE_ENTRYPOINT]: "Canon Guard Factory", + [ActionFactoryType.ALLOWANCE_CLAIMOR]: "Allowance Claimor Factory", + [ActionFactoryType.APPROVE_ACTION]: "Pre-Approve Action Factory", + [ActionFactoryType.CAPPED_TOKEN_TRANSFERS]: "Capped Token Transfers Hub Factory", + [ActionFactoryType.ARBITRARY_ACTIONS]: "Arbitrary Actions Factory", + [ActionFactoryType.SIMPLE_TRANSFERS]: "Simple Transfers Factory", + [ActionFactoryType.CHANGE_SAFE_GUARD]: "Change Safe Guard Action Factory", + [ActionFactoryType.SET_EMERGENCY_CALLER]: "Set Emergency Caller Action Factory", + [ActionFactoryType.SET_EMERGENCY_TRIGGER]: "Set Emergency Trigger Action Factory", + [ActionFactoryType.EVERCLEAR_TOKEN_CONVERSION]: "Everclear Token Conversion Factory", + [ActionFactoryType.OPX_ACTION]: "OPx Action Factory", + [ActionFactoryType.UNKNOWN]: "Unknown Factory", +}; + +export const getFactoryLabel = (actionBuilderAddress: Address): string => { + const mapping = KNOWN_FACTORY_MAPPINGS[actionBuilderAddress]; + return mapping?.label || FACTORY_TYPE_TO_LABEL[ActionFactoryType.UNKNOWN]; +}; + +export const getFactoryLabelByType = (factoryType: ActionFactoryType): string => { + return FACTORY_TYPE_TO_LABEL[factoryType] || FACTORY_TYPE_TO_LABEL[ActionFactoryType.UNKNOWN]; +}; diff --git a/src/constants/index.ts b/src/constants/index.ts index 1a1cee6..aaa81ff 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1 +1,2 @@ export * from "./addresses"; +export * from "./canonGuard"; diff --git a/src/containers/AppLayout.tsx b/src/containers/AppLayout.tsx index 648d606..eb53451 100644 --- a/src/containers/AppLayout.tsx +++ b/src/containers/AppLayout.tsx @@ -1,10 +1,12 @@ import { CssBaseline, styled } from "@mui/material"; import { Outlet } from "react-router-dom"; +import { WalletConnectNavigator } from "~/components/WalletConnect"; export const AppLayout = () => { return ( <> +

This website requires JavaScript to function properly.

diff --git a/src/containers/Landing.tsx b/src/containers/Landing.tsx index d5eb0f3..c4808ca 100644 --- a/src/containers/Landing.tsx +++ b/src/containers/Landing.tsx @@ -5,8 +5,8 @@ export const Landing = () => { return ( - Canon Guard - View-only interface for Canon Guard management + Canon Guard + View-only interface for Canon Guard management ); @@ -28,13 +28,11 @@ const LandingContent = styled(Box)(() => ({ })); const LandingTitle = styled(Typography)(() => ({ - variant: "h3", fontWeight: 700, marginBottom: 16, })); const LandingSubtitle = styled(Typography)(({ theme }) => ({ - variant: "h6", color: theme.palette.text.secondary, marginBottom: 32, })); diff --git a/src/contexts/CanonGuardConfigContext.tsx b/src/contexts/CanonGuardConfigContext.tsx new file mode 100644 index 0000000..97894c5 --- /dev/null +++ b/src/contexts/CanonGuardConfigContext.tsx @@ -0,0 +1,184 @@ +import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from "react"; +import { useConfig } from "wagmi"; +import { readContracts } from "wagmi/actions"; +import { canonGuardAbi } from "~/abis/canonGuard"; +import { useStateContext } from "~/hooks/useStateContext"; +import type { Address } from "viem"; + +export interface CanonGuardConfigState { + shortTxExecutionDelay: bigint | null; + longTxExecutionDelay: bigint | null; + txExpiryDelay: bigint | null; + maxApprovalDuration: bigint | null; + emergencyMode: boolean | null; + emergencyTrigger: Address | null; + emergencyCaller: Address | null; + isLoading: boolean; + error: Error | null; + refetch: () => Promise; +} + +const defaultState: CanonGuardConfigState = { + shortTxExecutionDelay: null, + longTxExecutionDelay: null, + txExpiryDelay: null, + maxApprovalDuration: null, + emergencyMode: null, + emergencyTrigger: null, + emergencyCaller: null, + isLoading: true, + error: null, + refetch: async () => {}, +}; + +const CanonGuardConfigContext = createContext(defaultState); + +interface CanonGuardConfigProviderProps { + children: ReactNode; +} + +/** + * Provider component that manages Canon Guard configuration state. + * Wraps the app to provide a single source of truth for all config values. + */ +export const CanonGuardConfigProvider = ({ children }: CanonGuardConfigProviderProps) => { + const config = useConfig(); + const { guardAddress, chainId } = useStateContext(); + + const [shortTxExecutionDelay, setShortTxExecutionDelay] = useState(null); + const [longTxExecutionDelay, setLongTxExecutionDelay] = useState(null); + const [txExpiryDelay, setTxExpiryDelay] = useState(null); + const [maxApprovalDuration, setMaxApprovalDuration] = useState(null); + const [emergencyMode, setEmergencyMode] = useState(null); + const [emergencyTrigger, setEmergencyTrigger] = useState
(null); + const [emergencyCaller, setEmergencyCaller] = useState
(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchConfig = useCallback(async () => { + if (!guardAddress || !chainId) { + setIsLoading(false); + return; + } + + setIsLoading(true); + setError(null); + + try { + const results = await readContracts(config, { + contracts: [ + { + address: guardAddress as Address, + abi: canonGuardAbi, + functionName: "SHORT_TX_EXECUTION_DELAY", + chainId, + }, + { + address: guardAddress as Address, + abi: canonGuardAbi, + functionName: "LONG_TX_EXECUTION_DELAY", + chainId, + }, + { + address: guardAddress as Address, + abi: canonGuardAbi, + functionName: "TX_EXPIRY_DELAY", + chainId, + }, + { + address: guardAddress as Address, + abi: canonGuardAbi, + functionName: "MAX_APPROVAL_DURATION", + chainId, + }, + { + address: guardAddress as Address, + abi: canonGuardAbi, + functionName: "emergencyMode", + chainId, + }, + { + address: guardAddress as Address, + abi: canonGuardAbi, + functionName: "emergencyTrigger", + chainId, + }, + { + address: guardAddress as Address, + abi: canonGuardAbi, + functionName: "emergencyCaller", + chainId, + }, + ], + }); + + const [ + shortDelayResult, + longDelayResult, + expiryResult, + maxApprovalResult, + emergencyResult, + triggerResult, + callerResult, + ] = results; + + if (shortDelayResult.status === "success") { + setShortTxExecutionDelay(shortDelayResult.result as bigint); + } + if (longDelayResult.status === "success") { + setLongTxExecutionDelay(longDelayResult.result as bigint); + } + if (expiryResult.status === "success") { + setTxExpiryDelay(expiryResult.result as bigint); + } + if (maxApprovalResult.status === "success") { + setMaxApprovalDuration(maxApprovalResult.result as bigint); + } + if (emergencyResult.status === "success") { + setEmergencyMode(emergencyResult.result as boolean); + } + if (triggerResult.status === "success") { + setEmergencyTrigger(triggerResult.result as Address); + } + if (callerResult.status === "success") { + setEmergencyCaller(callerResult.result as Address); + } + } catch (err) { + console.error("[CanonGuardConfigContext] Failed to fetch config:", err); + setError(err instanceof Error ? err : new Error("Failed to fetch config")); + } finally { + setIsLoading(false); + } + }, [config, guardAddress, chainId]); + + useEffect(() => { + fetchConfig(); + }, [fetchConfig]); + + const value: CanonGuardConfigState = { + shortTxExecutionDelay, + longTxExecutionDelay, + txExpiryDelay, + maxApprovalDuration, + emergencyMode, + emergencyTrigger, + emergencyCaller, + isLoading, + error, + refetch: fetchConfig, + }; + + return {children}; +}; + +/** + * Hook to consume Canon Guard configuration from context. + * All components using this hook share the same state instance. + */ +export const useCanonGuardConfigContext = (): CanonGuardConfigState => { + const context = useContext(CanonGuardConfigContext); + if (context === undefined) { + throw new Error("useCanonGuardConfigContext must be used within a CanonGuardConfigProvider"); + } + return context; +}; diff --git a/src/contexts/index.ts b/src/contexts/index.ts new file mode 100644 index 0000000..d4f4773 --- /dev/null +++ b/src/contexts/index.ts @@ -0,0 +1,2 @@ +export { CanonGuardConfigProvider, useCanonGuardConfigContext } from "./CanonGuardConfigContext"; +export type { CanonGuardConfigState } from "./CanonGuardConfigContext"; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 55c121d..c55d315 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1 +1,5 @@ export * from "./useStateContext"; +export * from "./useNavigateWithParams"; +export * from "./useWallet"; +export * from "./useTransactionExecutor"; +export * from "./useCanonGuardConfig"; diff --git a/src/hooks/useCanonGuardConfig.ts b/src/hooks/useCanonGuardConfig.ts new file mode 100644 index 0000000..e703363 --- /dev/null +++ b/src/hooks/useCanonGuardConfig.ts @@ -0,0 +1,64 @@ +import { useCanonGuardConfigContext, CanonGuardConfigState } from "~/contexts"; + +// Re-export the type for backwards compatibility +export type CanonGuardConfig = CanonGuardConfigState; + +/** + * Humanize seconds into a readable duration string. + * Shows only one unit with appropriate rounding. + * + * Examples: + * - 3600 -> "1 hour" + * - 7200 -> "2 hours" + * - 86400 -> "1 day" + * - 604800 -> "7 days" + * - 2592000 -> "1 month" (30 days) + * - 31536000 -> "12 months" (365 days) + */ +export const humanizeDuration = (seconds: bigint | null): string => { + if (seconds === null) return "-"; + + const secs = Number(seconds); + + if (secs === 0) return "0 seconds"; + + const MINUTE = 60; + const HOUR = 3600; + const DAY = 86400; + const MONTH = 30 * DAY; // 30 days + + // For values >= 30 days, show months + if (secs >= MONTH) { + const months = Math.round(secs / MONTH); + return months === 1 ? "1 month" : `${months} months`; + } + + // For values >= 1 day, show days + if (secs >= DAY) { + const days = Math.round(secs / DAY); + return days === 1 ? "1 day" : `${days} days`; + } + + // For values >= 1 hour, show hours + if (secs >= HOUR) { + const hours = Math.round(secs / HOUR); + return hours === 1 ? "1 hour" : `${hours} hours`; + } + + // For values >= 1 minute, show minutes + if (secs >= MINUTE) { + const minutes = Math.round(secs / MINUTE); + return minutes === 1 ? "1 minute" : `${minutes} minutes`; + } + + // For values < 1 minute, show seconds + return secs === 1 ? "1 second" : `${secs} seconds`; +}; + +/** + * Hook to access Canon Guard configuration. + * Delegates to the context provider for shared state. + */ +export const useCanonGuardConfig = (): CanonGuardConfig => { + return useCanonGuardConfigContext(); +}; diff --git a/src/hooks/useNavigateWithParams.ts b/src/hooks/useNavigateWithParams.ts new file mode 100644 index 0000000..e7f45ad --- /dev/null +++ b/src/hooks/useNavigateWithParams.ts @@ -0,0 +1,47 @@ +import { useCallback } from "react"; +import { useNavigate, useSearchParams, NavigateOptions } from "react-router-dom"; + +interface NavigateWithParamsOptions extends NavigateOptions { + /** Additional search params to add to the URL */ + additionalParams?: Record; +} + +/** + * Hook that provides navigation while preserving safeAddress, chainId, and guardAddress query params. + * This allows users to share URLs that include the Safe context (including detached mode). + */ +export const useNavigateWithParams = () => { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + + const navigateWithParams = useCallback( + (to: string, options?: NavigateWithParamsOptions) => { + const safeAddress = searchParams.get("safeAddress"); + const chainId = searchParams.get("chainId"); + const guardAddress = searchParams.get("guardAddress"); + + // Build the search params string + const params = new URLSearchParams(); + if (safeAddress) params.set("safeAddress", safeAddress); + if (chainId) params.set("chainId", chainId); + if (guardAddress) params.set("guardAddress", guardAddress); + + // Add any additional params + if (options?.additionalParams) { + for (const [key, value] of Object.entries(options.additionalParams)) { + params.set(key, value); + } + } + + const search = params.toString(); + const fullPath = search ? `${to}?${search}` : to; + + // Extract navigateOptions without additionalParams (which is only used for URL building) + const navigateOptions = options ? { state: options.state, replace: options.replace } : undefined; + navigate(fullPath, navigateOptions); + }, + [navigate, searchParams], + ); + + return navigateWithParams; +}; diff --git a/src/hooks/useServices.ts b/src/hooks/useServices.ts new file mode 100644 index 0000000..cd46fa4 --- /dev/null +++ b/src/hooks/useServices.ts @@ -0,0 +1,21 @@ +import { useStateContext } from "./useStateContext"; + +export const useSafeService = () => { + const { services } = useStateContext(); + return services.safeService; +}; + +export const useCanonGuardService = () => { + const { services } = useStateContext(); + return services.canonGuardService; +}; + +export const useClientService = () => { + const { services } = useStateContext(); + return services.clientService; +}; + +export const useQueueService = () => { + const { services } = useStateContext(); + return services.queueService; +}; diff --git a/src/hooks/useTransactionExecutor.ts b/src/hooks/useTransactionExecutor.ts new file mode 100644 index 0000000..e8b6c60 --- /dev/null +++ b/src/hooks/useTransactionExecutor.ts @@ -0,0 +1,1129 @@ +import { useState, useCallback } from "react"; +import { Address, Hash, Hex, parseUnits, parseEther, decodeEventLog, erc20Abi } from "viem"; +import { useWriteContract, useConfig } from "wagmi"; +import { waitForTransactionReceipt, readContract } from "wagmi/actions"; +import { + simpleTransfersFactoryAbi, + arbitraryActionsFactoryAbi, + allowanceClaimorFactoryAbi, + cappedTokenTransfersHubFactoryAbi, + canonGuardRegistryAbi, + canonGuardAbi, + preApproveActionFactoryAbi, + safeAbi, + changeSafeGuardActionFactoryAbi, +} from "~/abis/canonGuard"; +import { cappedTokenTransfersHubAbi } from "~/abis/canonGuard"; +import type { + TransferFormData, + ArbitraryActionFormData, + ClaimAllowanceFormData, + CappedTransferHubFormData, + HubChildFormData, +} from "~/components/NewAction/steps"; +import { + SIMPLE_TRANSFERS_FACTORY, + ARBITRARY_ACTIONS_FACTORY, + ALLOWANCE_CLAIMOR_FACTORY, + CAPPED_TOKEN_TRANSFERS_HUB_FACTORY, + CANON_GUARD_REGISTRY, + PRE_APPROVE_ACTION_FACTORY, + CHANGE_SAFE_GUARD_ACTION_FACTORY, +} from "~/constants/canonGuard"; +import { EPOCH_TIME_MULTIPLIERS } from "~/utils/timeUnits"; + +/** + * Transaction execution states + */ +export type ExecutionStatus = "idle" | "pending" | "confirming" | "success" | "error"; + +/** + * Result of a deploy transaction + */ +export interface DeployResult { + txHash: Hash; + deployedAddress: Address; +} + +/** + * Result of a registry record transaction + */ +export interface RecordResult { + txHash: Hash; +} + +/** + * Result of a queue transaction + */ +export interface QueueResult { + txHash: Hash; +} + +/** + * Result of a sign transaction (Safe approveHash) + */ +export interface SignResult { + txHash: Hash; + safeTxHash: Hash; +} + +/** + * Result of deploying a pre-approval action + */ +export interface DeployPreApprovalResult { + txHash: Hash; + preApprovalAddress: Address; +} + +/** + * Hook for executing Canon Guard transaction steps + * Supports: Deploy Contract, Save to Registry, Queue, Sign, Pre-Approval + */ +export function useTransactionExecutor() { + const config = useConfig(); + const { writeContractAsync } = useWriteContract(); + + const [status, setStatus] = useState("idle"); + const [error, setError] = useState(null); + const [txHash, setTxHash] = useState(null); + + /** + * Fetch token decimals from the ERC20 contract + */ + const getTokenDecimals = useCallback( + async (tokenAddress: Address): Promise => { + try { + const decimals = await readContract(config, { + address: tokenAddress, + abi: erc20Abi, + functionName: "decimals", + }); + return decimals; + } catch (err) { + console.warn(`Failed to fetch decimals for ${tokenAddress}, defaulting to 18:`, err); + return 18; // Default to 18 if call fails (some tokens don't implement decimals) + } + }, + [config], + ); + + /** + * Execute the Deploy Contract step for SimpleTransfers + * Calls SimpleTransfersFactory.createSimpleTransfers() and returns the deployed address + */ + const executeDeployTransfer = useCallback( + async (formData: TransferFormData): Promise => { + setStatus("pending"); + setError(null); + setTxHash(null); + + try { + // Build the transfer action array from form data + const transferActions = await Promise.all( + formData.transfers.map(async (transfer) => { + const tokenAddress = transfer.tokenAddress as Address; + // Fetch the token's decimals from the ERC20 contract + const decimals = await getTokenDecimals(tokenAddress); + console.log(`Token ${tokenAddress} has ${decimals} decimals`); + + return { + token: tokenAddress, + to: transfer.recipientAddress as Address, + amount: parseUnits(transfer.amount || "0", decimals), + }; + }), + ); + + console.log("[useTransactionExecutor] Deploying transfers:", transferActions); + + // Execute the contract write + const hash = await writeContractAsync({ + address: SIMPLE_TRANSFERS_FACTORY, + abi: simpleTransfersFactoryAbi, + functionName: "createSimpleTransfers", + args: [transferActions], + }); + + setTxHash(hash); + setStatus("confirming"); + + // Wait for transaction confirmation + const receipt = await waitForTransactionReceipt(config, { hash }); + + if (receipt.status === "reverted") { + throw new Error("Transaction reverted"); + } + + // Parse the SimpleTransfersCreated event to get the deployed address + // The event is: SimpleTransfersCreated(address indexed _simpleTransfers) + console.log("[useTransactionExecutor] Parsing logs:", receipt.logs); + const deployedAddress = parseDeployedAddress(receipt.logs); + + if (!deployedAddress) { + console.error("[useTransactionExecutor] Failed to parse deployed address from logs"); + throw new Error("Could not find deployed address in transaction logs"); + } + + console.log("[useTransactionExecutor] Deploy successful:", { txHash: hash, deployedAddress }); + setStatus("success"); + return { txHash: hash, deployedAddress }; + } catch (err) { + const error = err instanceof Error ? err : new Error("Transaction failed"); + setError(error); + setStatus("error"); + console.error("Deploy transaction failed:", error); + return null; + } + }, + [config, writeContractAsync, getTokenDecimals], + ); + + /** + * Execute the Deploy Contract step for ArbitraryActions + * Calls ArbitraryActionsFactory.createArbitraryActions() and returns the deployed address + */ + const executeDeployArbitraryAction = useCallback( + async (formData: ArbitraryActionFormData): Promise => { + setStatus("pending"); + setError(null); + setTxHash(null); + + try { + // Build array of ArbitraryAction structs from form data + // Signature is now optional - pass empty string if not provided + const arbitraryActions = formData.actions.map((action) => { + // Parse value - handle empty/undefined as 0 + let valueWei: bigint; + if (!action.value || action.value.trim() === "") { + valueWei = 0n; + } else { + // If user entered a decimal value, parse as ether; if whole number in wei, use directly + const valueStr = action.value.trim(); + if (valueStr.includes(".")) { + valueWei = parseEther(valueStr); + } else { + valueWei = BigInt(valueStr); + } + } + + return { + target: action.target as Address, + signature: action.signature || "", // Optional signature + data: (action.data || "0x") as Hex, // Full calldata including selector + value: valueWei, + }; + }); + + console.log("[useTransactionExecutor] Deploying ArbitraryActions:", arbitraryActions); + + // Execute the contract write + const hash = await writeContractAsync({ + address: ARBITRARY_ACTIONS_FACTORY, + abi: arbitraryActionsFactoryAbi, + functionName: "createArbitraryActions", + args: [arbitraryActions], + }); + + setTxHash(hash); + setStatus("confirming"); + + // Wait for transaction confirmation + const receipt = await waitForTransactionReceipt(config, { hash }); + + if (receipt.status === "reverted") { + throw new Error("Transaction reverted"); + } + + // Parse the ArbitraryActionsCreated event to get the deployed address + console.log("[useTransactionExecutor] Parsing ArbitraryActions logs:", receipt.logs); + const deployedAddress = parseArbitraryActionsAddress(receipt.logs); + + if (!deployedAddress) { + console.error("[useTransactionExecutor] Failed to parse deployed address from logs"); + throw new Error("Could not find deployed address in transaction logs"); + } + + console.log("[useTransactionExecutor] Deploy ArbitraryAction successful:", { txHash: hash, deployedAddress }); + setStatus("success"); + return { txHash: hash, deployedAddress }; + } catch (err) { + const error = err instanceof Error ? err : new Error("Transaction failed"); + setError(error); + setStatus("error"); + console.error("Deploy ArbitraryAction failed:", error); + return null; + } + }, + [config, writeContractAsync], + ); + + /** + * Execute the Deploy Contract step for AllowanceClaimor + * Calls AllowanceClaimorFactory.createAllowanceClaimor() and returns the deployed address + */ + const executeDeployClaimAllowance = useCallback( + async (formData: ClaimAllowanceFormData): Promise => { + setStatus("pending"); + setError(null); + setTxHash(null); + + try { + console.log("[useTransactionExecutor] Deploying AllowanceClaimor:", { + token: formData.token, + tokenOwner: formData.tokenOwner, + tokenRecipient: formData.tokenRecipient, + }); + + // Execute the contract write + const hash = await writeContractAsync({ + address: ALLOWANCE_CLAIMOR_FACTORY, + abi: allowanceClaimorFactoryAbi, + functionName: "createAllowanceClaimor", + args: [formData.token as Address, formData.tokenOwner as Address, formData.tokenRecipient as Address], + }); + + setTxHash(hash); + setStatus("confirming"); + + // Wait for transaction confirmation + const receipt = await waitForTransactionReceipt(config, { hash }); + + if (receipt.status === "reverted") { + throw new Error("Transaction reverted"); + } + + // Parse the AllowanceClaimorCreated event to get the deployed address + console.log("[useTransactionExecutor] Parsing AllowanceClaimor logs:", receipt.logs); + const deployedAddress = parseAllowanceClaimorAddress(receipt.logs); + + if (!deployedAddress) { + console.error("[useTransactionExecutor] Failed to parse deployed address from logs"); + throw new Error("Could not find deployed address in transaction logs"); + } + + console.log("[useTransactionExecutor] Deploy AllowanceClaimor successful:", { txHash: hash, deployedAddress }); + setStatus("success"); + return { txHash: hash, deployedAddress }; + } catch (err) { + const error = err instanceof Error ? err : new Error("Transaction failed"); + setError(error); + setStatus("error"); + console.error("Deploy AllowanceClaimor failed:", error); + return null; + } + }, + [config, writeContractAsync], + ); + + /** + * Execute the Deploy Hub Child step + * Calls CappedTokenTransfersHub.createNewActionsBuilder(token, amount) and returns the deployed address + */ + const executeDeployHubChild = useCallback( + async (hubAddress: Address, formData: HubChildFormData): Promise => { + setStatus("pending"); + setError(null); + setTxHash(null); + + try { + const tokenAddress = formData.token as Address; + + // Fetch the token's decimals from the ERC20 contract + const decimals = await getTokenDecimals(tokenAddress); + console.log(`Token ${tokenAddress} has ${decimals} decimals`); + + // Parse amount with correct decimals + const amount = parseUnits(formData.amount || "0", decimals); + + console.log("[useTransactionExecutor] Deploying Hub Child:", { + hubAddress, + token: tokenAddress, + amount: amount.toString(), + }); + + // Execute the contract write - call createNewActionsBuilder on the hub + const hash = await writeContractAsync({ + address: hubAddress, + abi: cappedTokenTransfersHubAbi, + functionName: "createNewActionsBuilder", + args: [tokenAddress, amount], + }); + + setTxHash(hash); + setStatus("confirming"); + + // Wait for transaction confirmation + const receipt = await waitForTransactionReceipt(config, { hash }); + + if (receipt.status === "reverted") { + throw new Error("Transaction reverted"); + } + + // Parse the CappedTokenTransfersCreated event to get the deployed address + console.log("[useTransactionExecutor] Parsing Hub Child logs:", receipt.logs); + const deployedAddress = parseHubChildAddress(receipt.logs); + + if (!deployedAddress) { + console.error("[useTransactionExecutor] Failed to parse deployed address from logs"); + throw new Error("Could not find deployed address in transaction logs"); + } + + console.log("[useTransactionExecutor] Deploy Hub Child successful:", { txHash: hash, deployedAddress }); + setStatus("success"); + return { txHash: hash, deployedAddress }; + } catch (err) { + const error = err instanceof Error ? err : new Error("Transaction failed"); + setError(error); + setStatus("error"); + console.error("Deploy Hub Child failed:", error); + return null; + } + }, + [config, writeContractAsync, getTokenDecimals], + ); + + /** + * Execute the Deploy Contract step for CappedTokenTransfersHub + * Calls CappedTokenTransfersHubFactory.createCappedTokenTransfersHub() and returns the deployed address + */ + const executeDeployCappedTransferHub = useCallback( + async (formData: CappedTransferHubFormData, safeAddress: Address): Promise => { + setStatus("pending"); + setError(null); + setTxHash(null); + + try { + // Calculate epoch length in seconds + const epochLengthValue = parseFloat(formData.epochLength) || 0; + const epochLengthSeconds = BigInt(Math.floor(epochLengthValue * EPOCH_TIME_MULTIPLIERS[formData.epochUnit])); + + // Prepare tokens and caps arrays + // For each token, fetch its decimals and parse the cap amount correctly + const tokens: Address[] = []; + const caps: bigint[] = []; + + for (const token of formData.tokens) { + const tokenAddress = token.address as Address; + tokens.push(tokenAddress); + + // Fetch token decimals + const decimals = await getTokenDecimals(tokenAddress); + console.log(`Token ${tokenAddress} has ${decimals} decimals`); + + // Parse cap with correct decimals + const cap = parseUnits(token.amount || "0", decimals); + caps.push(cap); + } + + console.log("[useTransactionExecutor] Deploying CappedTokenTransfersHub:", { + safe: safeAddress, + recipient: formData.recipientAddress, + tokens, + caps: caps.map((c) => c.toString()), + epochLength: epochLengthSeconds.toString(), + }); + + // Execute the contract write + const hash = await writeContractAsync({ + address: CAPPED_TOKEN_TRANSFERS_HUB_FACTORY, + abi: cappedTokenTransfersHubFactoryAbi, + functionName: "createCappedTokenTransfersHub", + args: [safeAddress, formData.recipientAddress as Address, tokens, caps, epochLengthSeconds], + }); + + setTxHash(hash); + setStatus("confirming"); + + // Wait for transaction confirmation + const receipt = await waitForTransactionReceipt(config, { hash }); + + if (receipt.status === "reverted") { + throw new Error("Transaction reverted"); + } + + // Parse the CappedTokenTransfersHubCreated event to get the deployed address + console.log("[useTransactionExecutor] Parsing CappedTokenTransfersHub logs:", receipt.logs); + const deployedAddress = parseCappedTokenTransfersHubAddress(receipt.logs); + + if (!deployedAddress) { + console.error("[useTransactionExecutor] Failed to parse deployed address from logs"); + throw new Error("Could not find deployed address in transaction logs"); + } + + console.log("[useTransactionExecutor] Deploy CappedTokenTransfersHub successful:", { + txHash: hash, + deployedAddress, + }); + setStatus("success"); + return { txHash: hash, deployedAddress }; + } catch (err) { + const error = err instanceof Error ? err : new Error("Transaction failed"); + setError(error); + setStatus("error"); + console.error("Deploy CappedTokenTransfersHub failed:", error); + return null; + } + }, + [config, writeContractAsync, getTokenDecimals], + ); + + /** + * Execute the Deploy Contract step for ChangeSafeGuardAction + * Calls ChangeSafeGuardActionFactory.createChangeSafeGuardAction(newGuardAddress) and returns the deployed address + * Used to attach or detach Canon Guard from a Safe + * @param newGuardAddress - The guard address to set. Use address(0) to detach, or a valid Canon Guard address to attach. + */ + const executeDeployChangeSafeGuardAction = useCallback( + async (newGuardAddress?: Address): Promise => { + setStatus("pending"); + setError(null); + setTxHash(null); + + const targetAddress = newGuardAddress ?? ("0x0000000000000000000000000000000000000000" as Address); + const action = newGuardAddress ? "attach" : "detach"; + + try { + console.log(`[useTransactionExecutor] Deploying ChangeSafeGuardAction to ${action} guard:`, targetAddress); + + // Execute the contract write + const hash = await writeContractAsync({ + address: CHANGE_SAFE_GUARD_ACTION_FACTORY, + abi: changeSafeGuardActionFactoryAbi, + functionName: "createChangeSafeGuardAction", + args: [targetAddress], + }); + + setTxHash(hash); + setStatus("confirming"); + + const receipt = await waitForTransactionReceipt(config, { hash }); + + if (receipt.status === "reverted") { + throw new Error("Transaction reverted"); + } + + // Parse the ChangeSafeGuardActionCreated event to get the deployed address + console.log("[useTransactionExecutor] Parsing ChangeSafeGuardAction logs:", receipt.logs); + const deployedAddress = parseChangeSafeGuardActionAddress(receipt.logs); + + if (!deployedAddress) { + console.error("[useTransactionExecutor] Failed to parse deployed address from logs"); + throw new Error("Could not find deployed address in transaction logs"); + } + + console.log("[useTransactionExecutor] Deploy ChangeSafeGuardAction successful:", { + txHash: hash, + deployedAddress, + }); + setStatus("success"); + return { txHash: hash, deployedAddress }; + } catch (err) { + const error = err instanceof Error ? err : new Error("Transaction failed"); + setError(error); + setStatus("error"); + console.error("Deploy ChangeSafeGuardAction failed:", error); + return null; + } + }, + [config, writeContractAsync], + ); + + /** + * Execute the Save to Registry step + * Calls CanonGuardRegistry.record() to save the deployed action with its label + */ + const executeRecordToRegistry = useCallback( + async (guardAddress: Address, deployedActionAddress: Address, label: string): Promise => { + setStatus("pending"); + setError(null); + setTxHash(null); + + try { + console.log("[useTransactionExecutor] Recording to registry:", { + guardAddress, + deployedActionAddress, + label, + }); + + // Execute the contract write + // record(address _canonGuard, address[] _entities, string[] _labels) + const hash = await writeContractAsync({ + address: CANON_GUARD_REGISTRY, + abi: canonGuardRegistryAbi, + functionName: "record", + args: [ + guardAddress, + [deployedActionAddress], // entities array with single item + [label], // labels array with single item + ], + }); + + setTxHash(hash); + setStatus("confirming"); + + // Wait for transaction confirmation + const receipt = await waitForTransactionReceipt(config, { hash }); + + if (receipt.status === "reverted") { + throw new Error("Transaction reverted"); + } + + console.log("[useTransactionExecutor] Registry record successful:", { txHash: hash }); + setStatus("success"); + return { txHash: hash }; + } catch (err) { + const error = err instanceof Error ? err : new Error("Registry record failed"); + setError(error); + setStatus("error"); + console.error("Registry record transaction failed:", error); + return null; + } + }, + [config, writeContractAsync], + ); + + /** + * Execute the Queue Transaction step + * Calls canonGuard.queueTransaction(actionBuilderAddress) + */ + const executeQueueTransaction = useCallback( + async (guardAddress: Address, actionBuilderAddress: Address): Promise => { + setStatus("pending"); + setError(null); + setTxHash(null); + + try { + console.log("[useTransactionExecutor] Queueing transaction:", { + guardAddress, + actionBuilderAddress, + }); + + const hash = await writeContractAsync({ + address: guardAddress, + abi: canonGuardAbi, + functionName: "queueTransaction", + args: [actionBuilderAddress], + }); + + setTxHash(hash); + setStatus("confirming"); + + const receipt = await waitForTransactionReceipt(config, { hash }); + + if (receipt.status === "reverted") { + throw new Error("Queue transaction reverted"); + } + + console.log("[useTransactionExecutor] Queue successful:", { txHash: hash }); + setStatus("success"); + return { txHash: hash }; + } catch (err) { + const error = err instanceof Error ? err : new Error("Queue transaction failed"); + setError(error); + setStatus("error"); + console.error("Queue transaction failed:", error); + return null; + } + }, + [config, writeContractAsync], + ); + + /** + * Execute the Sign Transaction step + * 1. Get the safeTxHash from canonGuard.getSafeTransactionHash(actionBuilderAddress, nonce?) + * 2. Call safe.approveHash(safeTxHash) + * + * @param nonce - Optional nonce to sign at. If provided, uses getSafeTransactionHash(actionBuilder, nonce). + * If not provided, uses getSafeTransactionHash(actionBuilder) which auto-detects current nonce. + */ + const executeSignTransaction = useCallback( + async ( + safeAddress: Address, + guardAddress: Address, + actionBuilderAddress: Address, + nonce?: number, + ): Promise => { + setStatus("pending"); + setError(null); + setTxHash(null); + + try { + console.log("[useTransactionExecutor] Signing transaction:", { + safeAddress, + guardAddress, + actionBuilderAddress, + nonce, + }); + + // Step 1: Get the Safe transaction hash from the Canon Guard + // Use nonce-specific method if provided, otherwise auto-detect + const safeTxHash = + nonce !== undefined + ? ((await readContract(config, { + address: guardAddress, + abi: canonGuardAbi, + functionName: "getSafeTransactionHash", + args: [actionBuilderAddress, BigInt(nonce)], + })) as Hash) + : ((await readContract(config, { + address: guardAddress, + abi: canonGuardAbi, + functionName: "getSafeTransactionHash", + args: [actionBuilderAddress], + })) as Hash); + + console.log("[useTransactionExecutor] Got safeTxHash:", safeTxHash, "for nonce:", nonce); + + // Step 2: Approve the hash in the Safe + const hash = await writeContractAsync({ + address: safeAddress, + abi: safeAbi, + functionName: "approveHash", + args: [safeTxHash], + }); + + setTxHash(hash); + setStatus("confirming"); + + const receipt = await waitForTransactionReceipt(config, { hash }); + + if (receipt.status === "reverted") { + throw new Error("Sign transaction reverted"); + } + + console.log("[useTransactionExecutor] Sign successful:", { txHash: hash, safeTxHash, nonce }); + setStatus("success"); + return { txHash: hash, safeTxHash }; + } catch (err) { + const error = err instanceof Error ? err : new Error("Sign transaction failed"); + setError(error); + setStatus("error"); + console.error("Sign transaction failed:", error); + return null; + } + }, + [config, writeContractAsync], + ); + + /** + * Execute the Deploy Pre-Approval step + * Calls PreApproveActionFactory.createPreApproveAction(actionBuilderAddress, approvalDuration) + */ + const executeDeployPreApproval = useCallback( + async (actionBuilderAddress: Address, approvalDuration: bigint): Promise => { + setStatus("pending"); + setError(null); + setTxHash(null); + + try { + console.log("[useTransactionExecutor] Deploying pre-approval:", { + actionBuilderAddress, + approvalDuration: approvalDuration.toString(), + }); + + const hash = await writeContractAsync({ + address: PRE_APPROVE_ACTION_FACTORY, + abi: preApproveActionFactoryAbi, + functionName: "createPreApproveAction", + args: [actionBuilderAddress, approvalDuration], + }); + + setTxHash(hash); + setStatus("confirming"); + + const receipt = await waitForTransactionReceipt(config, { hash }); + + if (receipt.status === "reverted") { + throw new Error("Deploy pre-approval reverted"); + } + + // Parse the PreApproveActionCreated event to get the deployed address + console.log("[useTransactionExecutor] Parsing pre-approval logs:", receipt.logs); + const preApprovalAddress = parsePreApprovalAddress(receipt.logs); + + if (!preApprovalAddress) { + console.error("[useTransactionExecutor] Failed to parse pre-approval address from logs"); + throw new Error("Could not find pre-approval address in transaction logs"); + } + + console.log("[useTransactionExecutor] Deploy pre-approval successful:", { txHash: hash, preApprovalAddress }); + setStatus("success"); + return { txHash: hash, preApprovalAddress }; + } catch (err) { + const error = err instanceof Error ? err : new Error("Deploy pre-approval failed"); + setError(error); + setStatus("error"); + console.error("Deploy pre-approval failed:", error); + return null; + } + }, + [config, writeContractAsync], + ); + + /** + * Execute a queued Canon Guard transaction + * Calls canonGuard.executeTransaction(actionBuilder) + */ + const executeCanonTransaction = useCallback( + async (guardAddress: Address, actionBuilderAddress: Address): Promise => { + setStatus("pending"); + setError(null); + setTxHash(null); + + try { + console.log(`Executing Canon Guard transaction for action builder: ${actionBuilderAddress}`); + console.log(`Canon Guard address: ${guardAddress}`); + + // Call executeTransaction on Canon Guard + const hash = await writeContractAsync({ + address: guardAddress, + abi: canonGuardAbi, + functionName: "executeTransaction", + args: [actionBuilderAddress], + }); + + console.log(`Execute transaction submitted: ${hash}`); + setTxHash(hash); + setStatus("confirming"); + + // Wait for transaction confirmation + const receipt = await waitForTransactionReceipt(config, { + hash, + confirmations: 1, + }); + + if (receipt.status === "reverted") { + throw new Error("Execute transaction reverted"); + } + + console.log(`Execute transaction confirmed: ${hash}`); + setStatus("success"); + return hash; + } catch (err) { + const error = err instanceof Error ? err : new Error(String(err)); + setError(error); + setStatus("error"); + console.error("Execute Canon Guard transaction failed:", error); + return null; + } + }, + [config, writeContractAsync], + ); + + /** + * Execute the Remove from Registry step + * Calls CanonGuardRegistry.remove(guardAddress, entityAddresses) + */ + const executeRemoveFromRegistry = useCallback( + async (guardAddress: Address, entityAddresses: Address[]): Promise => { + setStatus("pending"); + setError(null); + setTxHash(null); + + try { + console.log("[useTransactionExecutor] Removing from registry:", { + guardAddress, + entityAddresses, + }); + + const hash = await writeContractAsync({ + address: CANON_GUARD_REGISTRY, + abi: canonGuardRegistryAbi, + functionName: "remove", + args: [guardAddress, entityAddresses], + }); + + setTxHash(hash); + setStatus("confirming"); + + const receipt = await waitForTransactionReceipt(config, { hash }); + + if (receipt.status === "reverted") { + throw new Error("Remove from registry transaction reverted"); + } + + console.log("[useTransactionExecutor] Remove from registry successful:", { txHash: hash }); + setStatus("success"); + return hash; + } catch (err) { + const error = err instanceof Error ? err : new Error("Remove from registry failed"); + setError(error); + setStatus("error"); + console.error("Remove from registry failed:", error); + return null; + } + }, + [config, writeContractAsync], + ); + + /** + * Execute the Cancel Enqueued Transaction step + * Calls CanonGuard.cancelEnqueuedTransaction(actionBuilderAddress) + * Only the proposer can cancel a non-expired transaction + */ + const executeCancelTransaction = useCallback( + async (guardAddress: Address, actionBuilderAddress: Address): Promise => { + setStatus("pending"); + setError(null); + setTxHash(null); + + try { + console.log("[useTransactionExecutor] Cancelling enqueued transaction:", { + guardAddress, + actionBuilderAddress, + }); + + const hash = await writeContractAsync({ + address: guardAddress, + abi: canonGuardAbi, + functionName: "cancelEnqueuedTransaction", + args: [actionBuilderAddress], + }); + + setTxHash(hash); + setStatus("confirming"); + + const receipt = await waitForTransactionReceipt(config, { hash }); + + if (receipt.status === "reverted") { + throw new Error("Cancel enqueued transaction reverted"); + } + + console.log("[useTransactionExecutor] Cancel enqueued transaction successful:", { txHash: hash }); + setStatus("success"); + return hash; + } catch (err) { + const error = err instanceof Error ? err : new Error("Cancel enqueued transaction failed"); + setError(error); + setStatus("error"); + console.error("Cancel enqueued transaction failed:", error); + return null; + } + }, + [config, writeContractAsync], + ); + + /** + * Reset the executor state + */ + const reset = useCallback(() => { + setStatus("idle"); + setError(null); + setTxHash(null); + }, []); + + return { + // State + status, + error, + txHash, + isExecuting: status === "pending" || status === "confirming", + + // Actions + executeDeployTransfer, + executeDeployArbitraryAction, + executeDeployClaimAllowance, + executeDeployHubChild, + executeDeployCappedTransferHub, + executeDeployChangeSafeGuardAction, + executeRecordToRegistry, + executeQueueTransaction, + executeSignTransaction, + executeDeployPreApproval, + executeCanonTransaction, + executeRemoveFromRegistry, + executeCancelTransaction, + reset, + }; +} + +/** + * Parse the deployed address from transaction logs + * Looks for the SimpleTransfersCreated event + */ +function parseDeployedAddress( + logs: readonly { data: `0x${string}`; topics: readonly `0x${string}`[] }[], +): Address | null { + for (const log of logs) { + try { + const decoded = decodeEventLog({ + abi: simpleTransfersFactoryAbi, + data: log.data, + topics: log.topics, + }); + + if (decoded.eventName === "SimpleTransfersCreated") { + // The event has: SimpleTransfersCreated(address indexed _simpleTransfers) + return (decoded.args as { _simpleTransfers: Address })._simpleTransfers; + } + } catch { + // Not the event we're looking for, continue + continue; + } + } + return null; +} + +/** + * Parse the pre-approval address from transaction logs + * Looks for the PreApproveActionCreated event + */ +function parsePreApprovalAddress( + logs: readonly { data: `0x${string}`; topics: readonly `0x${string}`[] }[], +): Address | null { + for (const log of logs) { + try { + const decoded = decodeEventLog({ + abi: preApproveActionFactoryAbi, + data: log.data, + topics: log.topics, + }); + + if (decoded.eventName === "PreApproveActionCreated") { + // The event has: PreApproveActionCreated(address indexed _preApproveAction, address indexed _actionsBuilder, uint256 _approvalDuration) + return (decoded.args as { _preApproveAction: Address })._preApproveAction; + } + } catch { + // Not the event we're looking for, continue + continue; + } + } + return null; +} + +/** + * Parse the deployed ArbitraryActions address from transaction logs + * Looks for the ArbitraryActionsCreated event + */ +function parseArbitraryActionsAddress( + logs: readonly { data: `0x${string}`; topics: readonly `0x${string}`[] }[], +): Address | null { + for (const log of logs) { + try { + const decoded = decodeEventLog({ + abi: arbitraryActionsFactoryAbi, + data: log.data, + topics: log.topics, + }); + + if (decoded.eventName === "ArbitraryActionsCreated") { + // The event has: ArbitraryActionsCreated(address indexed _arbitraryActions) + return (decoded.args as { _arbitraryActions: Address })._arbitraryActions; + } + } catch { + // Not the event we're looking for, continue + continue; + } + } + return null; +} + +/** + * Parse the deployed AllowanceClaimor address from transaction logs + * Looks for the AllowanceClaimorCreated event + */ +function parseAllowanceClaimorAddress( + logs: readonly { data: `0x${string}`; topics: readonly `0x${string}`[] }[], +): Address | null { + for (const log of logs) { + try { + const decoded = decodeEventLog({ + abi: allowanceClaimorFactoryAbi, + data: log.data, + topics: log.topics, + }); + + if (decoded.eventName === "AllowanceClaimorCreated") { + // The event has: AllowanceClaimorCreated(address indexed _allowanceClaimor, address indexed _token, address indexed _tokenOwner, address _tokenRecipient) + return (decoded.args as { _allowanceClaimor: Address })._allowanceClaimor; + } + } catch { + // Not the event we're looking for, continue + continue; + } + } + return null; +} + +/** + * Parse the deployed CappedTokenTransfersHub address from transaction logs + * Looks for the CappedTokenTransfersHubCreated event + */ +function parseCappedTokenTransfersHubAddress( + logs: readonly { data: `0x${string}`; topics: readonly `0x${string}`[] }[], +): Address | null { + for (const log of logs) { + try { + const decoded = decodeEventLog({ + abi: cappedTokenTransfersHubFactoryAbi, + data: log.data, + topics: log.topics, + }); + + if (decoded.eventName === "CappedTokenTransfersHubCreated") { + // The event has: CappedTokenTransfersHubCreated(address indexed _cappedTokenTransfersHub, address indexed _safe, address indexed _recipient) + return (decoded.args as { _cappedTokenTransfersHub: Address })._cappedTokenTransfersHub; + } + } catch { + // Not the event we're looking for, continue + continue; + } + } + return null; +} + +/** + * Parse the deployed Hub Child (CappedTokenTransfers) address from transaction logs + * Looks for the CappedTokenTransfersCreated event emitted by the hub + */ +function parseHubChildAddress( + logs: readonly { data: `0x${string}`; topics: readonly `0x${string}`[] }[], +): Address | null { + for (const log of logs) { + try { + const decoded = decodeEventLog({ + abi: cappedTokenTransfersHubAbi, + data: log.data, + topics: log.topics, + }); + + if (decoded.eventName === "CappedTokenTransfersCreated") { + // The event has: CappedTokenTransfersCreated(address indexed _actionsBuilder, address _token, uint256 _amount) + return (decoded.args as { _actionsBuilder: Address })._actionsBuilder; + } + } catch { + // Not the event we're looking for, continue + continue; + } + } + return null; +} + +/** + * Parse the deployed ChangeSafeGuardAction address from transaction logs + * Looks for the ChangeSafeGuardActionCreated event + */ +function parseChangeSafeGuardActionAddress( + logs: readonly { data: `0x${string}`; topics: readonly `0x${string}`[] }[], +): Address | null { + for (const log of logs) { + try { + const decoded = decodeEventLog({ + abi: changeSafeGuardActionFactoryAbi, + data: log.data, + topics: log.topics, + }); + + if (decoded.eventName === "ChangeSafeGuardActionCreated") { + // The event has: ChangeSafeGuardActionCreated(address indexed _changeSafeGuardAction, address indexed _safeGuard) + return (decoded.args as { _changeSafeGuardAction: Address })._changeSafeGuardAction; + } + } catch { + // Not the event we're looking for, continue + continue; + } + } + return null; +} + +export type UseTransactionExecutorReturn = ReturnType; diff --git a/src/hooks/useWallet.ts b/src/hooks/useWallet.ts new file mode 100644 index 0000000..3743a8f --- /dev/null +++ b/src/hooks/useWallet.ts @@ -0,0 +1,83 @@ +import { useCallback } from "react"; +import { useConnectModal } from "@rainbow-me/rainbowkit"; +import { useAccount, useDisconnect, useSwitchChain } from "wagmi"; +import { SupportedChainId, isSupportedChain } from "~/config/chains"; + +/** + * Centralized hook for wallet connection management + * Wraps wagmi and RainbowKit hooks into a single interface + */ +export function useWallet() { + const { address, isConnected, isConnecting, chainId } = useAccount(); + const { disconnect } = useDisconnect(); + const { switchChainAsync, isPending: isSwitchingChain } = useSwitchChain(); + const { openConnectModal } = useConnectModal(); + + /** + * Switch to a specific chain + * Returns true if successful, false otherwise + */ + const switchToChain = useCallback( + async (targetChainId: SupportedChainId): Promise => { + if (!isConnected) { + console.warn("Cannot switch chain: wallet not connected"); + return false; + } + + if (chainId === targetChainId) { + return true; // Already on correct chain + } + + try { + await switchChainAsync({ chainId: targetChainId }); + return true; + } catch (error) { + console.error("Failed to switch chain:", error); + return false; + } + }, + [isConnected, chainId, switchChainAsync], + ); + + /** + * Check if wallet is on the correct chain + */ + const isOnCorrectChain = useCallback( + (targetChainId: SupportedChainId): boolean => { + return chainId === targetChainId; + }, + [chainId], + ); + + /** + * Check if current chain is a supported chain + */ + const isOnSupportedChain = chainId ? isSupportedChain(chainId) : false; + + /** + * Open the RainbowKit connect modal + */ + const connect = useCallback(() => { + openConnectModal?.(); + }, [openConnectModal]); + + return { + // Connection state + address, + isConnected, + isConnecting, + chainId, + isOnSupportedChain, + + // Actions + connect, + disconnect, + switchToChain, + isOnCorrectChain, + + // Loading states + isSwitchingChain, + }; +} + +export type UseWalletReturn = ReturnType; diff --git a/src/pages/SafeVault.tsx b/src/pages/SafeVault.tsx index 67439e8..2f6b364 100644 --- a/src/pages/SafeVault.tsx +++ b/src/pages/SafeVault.tsx @@ -1,167 +1,294 @@ -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useRef } from "react"; import { Box, Typography, CircularProgress, styled } from "@mui/material"; -import { Address } from "viem"; -import { QueueSection } from "~/components/QueueSection"; -import { SafeSidebar } from "~/components/SafeSidebar"; +import { flushSync } from "react-dom"; +import { useSearchParams, useNavigate } from "react-router-dom"; +import { Address, isAddress } from "viem"; +import { CanonGuardApp } from "~/components/CanonGuardApp"; +import { DetachedGuardInput } from "~/components/DetachedGuardInput"; +import { ErrorState } from "~/components/ErrorState"; +import { GuardSetupWizard } from "~/components/GuardSetupWizard"; +import { NoGuardChoiceScreen } from "~/components/NoGuardChoiceScreen"; import { VaultSetupModal } from "~/components/VaultSetupModal"; -import { SafePageContainer, SafeMainContent } from "~/components/shared/StyledComponents"; +import { SupportedChainId, parseChainId, getRpcUrlForChain, getViemChain } from "~/config/chains"; import { useStateContext } from "~/hooks/useStateContext"; -import { canonGuardService } from "~/services/canonGuardService"; -import { VaultData, TabType } from "~/types/canon-guard"; - -const TAB_CONTENT_MAP = { - [TabType.QUEUE]: (vaultData: VaultData) => ( - tx.approversCount < tx.requiredApprovals)} - /> - ), - [TabType.PRE_APPROVED]: () => Pre-approved actions coming soon..., - [TabType.HISTORY]: () => History coming soon..., - [TabType.CONFIGURATION]: () => Configuration coming soon..., - [TabType.ACTIONS]: () => Action creation coming soon..., -}; +import { ClientService, SafeService, CanonGuardValidationService } from "~/services"; +import { SafeInfo } from "~/types"; -interface VaultContentProps { - vaultData: VaultData; - activeTab: TabType; -} +type ViewState = "setup" | "loading" | "error" | "ready" | "no-guard-choice" | "detached-input" | "deploy-wizard"; -const VaultContent = ({ vaultData, activeTab }: VaultContentProps) => { - if (!vaultData.vaultInfo.hasCanonGuard) { - return ( - - This address is not a Canon Vault, please set it up and try again. - - ); - } +export const SafeVault = () => { + const { setSafeAddress, setChainId, setGuardAddress, setIsDetached, clearConfig } = useStateContext(); + const [searchParams, setSearchParams] = useSearchParams(); + const navigate = useNavigate(); - const renderTabContent = TAB_CONTENT_MAP[activeTab]; - return renderTabContent ? renderTabContent(vaultData) : null; -}; + const [safeInfo, setSafeInfo] = useState(null); + const [viewState, setViewState] = useState("setup"); -interface SafeVaultProps { - safeData?: VaultData; -} - -export const SafeVault = ({ safeData }: SafeVaultProps) => { - const { vaultAddress, rpcUrl, isVaultConfigured, setVaultAddress, setRpcUrl, loading, setLoading } = - useStateContext(); - - const [currentVaultData, setCurrentVaultData] = useState(safeData || null); - const [activeTab, setActiveTab] = useState(TabType.QUEUE); - const [sidebarCollapsed, setSidebarCollapsed] = useState(true); - - const loadVaultData = useCallback(async () => { - if (!vaultAddress || !rpcUrl) return; - - try { - setLoading(true); - const vaultData = await canonGuardService.getVaultData(vaultAddress); - setCurrentVaultData(vaultData); - } catch (error) { - console.error("Failed to load vault data:", error); - setCurrentVaultData(null); - } finally { - setLoading(false); - } - }, [vaultAddress, rpcUrl, setLoading]); + // Track if we've initialized from URL params + const initializedRef = useRef(false); + // Initialize from URL params on mount only useEffect(() => { - if (isVaultConfigured) { - loadVaultData(); + if (initializedRef.current) return; + + const safeAddressParam = searchParams.get("safeAddress"); + const chainIdParam = searchParams.get("chainId"); + const guardAddressParam = searchParams.get("guardAddress"); + + if (safeAddressParam && isAddress(safeAddressParam)) { + const parsedChainId = parseChainId(chainIdParam); + + if (parsedChainId) { + initializedRef.current = true; + setSafeAddress(safeAddressParam as Address); + setChainId(parsedChainId); + setViewState("loading"); + + // Create a fresh service for the correct chain to avoid stale closure issues + const loadWithCorrectChain = async () => { + try { + const rpcUrl = getRpcUrlForChain(parsedChainId); + const chain = getViemChain(parsedChainId); + const clientService = new ClientService(rpcUrl, chain); + const freshSafeService = new SafeService(clientService); + + const info = await freshSafeService.getSafeInfo(safeAddressParam as Address); + setSafeInfo(info); + + // Case D: URL has guardAddress param - validate and use detached mode + if (guardAddressParam && isAddress(guardAddressParam) && !info.hasGuard) { + const validationService = new CanonGuardValidationService(clientService.getClient()); + const isValid = await validationService.isValidCanonGuard(guardAddressParam as Address); + + if (isValid) { + setGuardAddress(guardAddressParam as Address); + setIsDetached(true); + setViewState("ready"); + return; + } + // If invalid, fall through to normal flow + } + + // Case A: Safe has valid attached guard + if (info.hasGuard && info.isValidCanonGuard && info.guardAddress) { + setGuardAddress(info.guardAddress); + setIsDetached(false); + setViewState("ready"); + return; + } + + // Case B: Safe has invalid guard (not Canon Guard) + if (info.hasGuard && !info.isValidCanonGuard) { + setViewState("error"); + return; + } + + // Case C: Safe has no guard - show choice screen + if (!info.hasGuard) { + setViewState("no-guard-choice"); + return; + } + + setViewState("ready"); + } catch (error) { + console.error("Failed to load Safe info:", error); + setSafeInfo(null); + setViewState("error"); + } + }; + + loadWithCorrectChain(); + } } - }, [isVaultConfigured, loadVaultData]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleSetupSubmit = useCallback( + async (address: Address, selectedChainId: SupportedChainId) => { + // Create a fresh service for the selected chain to avoid stale closure issues + try { + const rpcUrl = getRpcUrlForChain(selectedChainId); + const chain = getViemChain(selectedChainId); + const clientService = new ClientService(rpcUrl, chain); + const freshSafeService = new SafeService(clientService); + + const info = await freshSafeService.getSafeInfo(address); + + // Only update state after successful fetch - VaultSetupModal shows loading on button + setSafeAddress(address); + setChainId(selectedChainId); + setSearchParams({ + safeAddress: address, + chainId: String(selectedChainId), + }); + setSafeInfo(info); + + // Case A: Safe has valid attached guard + if (info.hasGuard && info.isValidCanonGuard && info.guardAddress) { + setGuardAddress(info.guardAddress); + setIsDetached(false); + setViewState("ready"); + return; + } + + // Case B: Safe has invalid guard (not Canon Guard) + if (info.hasGuard && !info.isValidCanonGuard) { + setViewState("error"); + return; + } + + // Case C: Safe has no guard - show choice screen + if (!info.hasGuard) { + setViewState("no-guard-choice"); + return; + } + + setViewState("ready"); + } catch (error) { + console.error("Failed to load Safe info:", error); + setSafeInfo(null); + setViewState("error"); + } + }, + [setSafeAddress, setChainId, setSearchParams, setGuardAddress, setIsDetached], + ); + + const handleClearConfig = useCallback(() => { + // Reset everything and go back to setup + initializedRef.current = false; + + // Use flushSync to ensure state updates happen synchronously + flushSync(() => { + clearConfig(); + setSafeInfo(null); + setViewState("setup"); + }); + + // Navigate to root using React Router to ensure proper state sync + navigate("/", { replace: true }); + }, [clearConfig, navigate]); + + // Handle choice: Deploy New Guard + const handleDeployNew = useCallback(() => { + setViewState("deploy-wizard"); + }, []); - const handleSetupSubmit = (address: Address, rpc: string) => { - setVaultAddress(address); - setRpcUrl(rpc); - }; + // Handle choice: Use Existing Guard (detached mode) + const handleUseExisting = useCallback(() => { + setViewState("detached-input"); + }, []); - if (!isVaultConfigured) { + // Handle detached guard input continue + const handleDetachedContinue = useCallback( + (guardAddr: Address) => { + if (!safeInfo) return; + + // Save to URL params + setSearchParams({ + safeAddress: safeInfo.address, + chainId: String(safeInfo.chainId), + guardAddress: guardAddr, + }); + + // Set in context + setGuardAddress(guardAddr); + setIsDetached(true); + setViewState("ready"); + }, + [safeInfo, setSearchParams, setGuardAddress, setIsDetached], + ); + + // Handle back from choice/input screens + const handleBackToChoice = useCallback(() => { + setViewState("no-guard-choice"); + }, []); + + // Show setup modal + if (viewState === "setup") { return ; } - if (loading) { + // Show loading state + if (viewState === "loading") { return ( - - Loading vault data... + + Loading Safe info... ); } - if (!currentVaultData) { + // Handle error - couldn't connect to Safe or invalid guard + if (viewState === "error" || !safeInfo) { + // Check if it's an invalid guard error + if (safeInfo?.hasGuard && !safeInfo.isValidCanonGuard) { + return ( + + ); + } + return ( - - Failed to load vault data, please try again. - + ); } - if (!currentVaultData.vaultInfo.hasCanonGuard) { + // Case C: Safe has no guard - show choice screen + if (viewState === "no-guard-choice") { return ( - - This address is not a Canon Vault, please set it up and try again. - + ); } - return ( - - {currentVaultData && ( - <> - setSidebarCollapsed(!sidebarCollapsed)} - /> - - - - - )} - - ); + // Deploy new guard wizard + if (viewState === "deploy-wizard") { + return ( + + ); + } + + // Detached guard input + if (viewState === "detached-input") { + return ( + + ); + } + + // Everything is good - show the main Canon Guard App UI + return ; }; -const LoadingContainer = styled(Box)(() => ({ +const LoadingContainer = styled(Box)(({ theme }) => ({ display: "flex", alignItems: "center", justifyContent: "center", height: "100vh", flexDirection: "column", - gap: 16, + gap: theme.spacing(2), })); const LoadingText = styled(Typography)(({ theme }) => ({ - variant: "h6", - color: theme.palette.text.secondary, -})); - -const ErrorContainer = styled(Box)(() => ({ - display: "flex", - alignItems: "center", - justifyContent: "center", - height: "100vh", - padding: 32, -})); - -const ErrorMessage = styled(Typography)(({ theme }) => ({ - variant: "h6", - color: theme.palette.error.main, - textAlign: "center", - maxWidth: 600, -})); - -const ComingSoonMessage = styled(Typography)(({ theme }) => ({ - display: "flex", - alignItems: "center", - justifyContent: "center", - height: "400px", - fontSize: "1.25rem", color: theme.palette.text.secondary, - fontStyle: "italic", })); diff --git a/src/pages/TestPage.tsx b/src/pages/TestPage.tsx new file mode 100644 index 0000000..2f2120e --- /dev/null +++ b/src/pages/TestPage.tsx @@ -0,0 +1,304 @@ +/** + * Test Page for Component Development + * + * This page shows various components in isolation for easy testing and iteration. + * Access at: /test + */ + +import { useState } from "react"; +import { Box, Typography, styled, Divider, Button } from "@mui/material"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import type { CappedTransferHubFormData } from "~/components/NewAction/steps"; +import { CappedTransferHubFormStep } from "~/components/NewAction/steps/CappedTransferHubFormStep"; +import { NonceSelector } from "~/components/NonceSelector"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import type { QueueItem } from "~/services/queueService"; + +// Mock queue items for testing +const mockQueueItems: QueueItem[] = [ + { + actionBuilderAddress: "0x1111111111111111111111111111111111111111", + label: "Empty USDC Transfer", + factoryLabel: "Transfer", + nonce: 2, + approversCount: 2, + threshold: 3, + executableAt: new Date(Date.now() + 3600000), // 1 hour from now + expiresAt: new Date(Date.now() + 86400000), // 24 hours from now + }, + { + actionBuilderAddress: "0x2222222222222222222222222222222222222222", + label: "Deposit to Vault", + factoryLabel: "Arbitrary Action", + nonce: 2, // Same nonce - will show the one with more sigs + approversCount: 1, + threshold: 3, + executableAt: new Date(Date.now() + 7200000), + expiresAt: new Date(Date.now() + 86400000), + }, + { + actionBuilderAddress: "0x3333333333333333333333333333333333333333", + label: "Claim Rewards", + factoryLabel: "Claim Allowance", + nonce: 3, + approversCount: 1, + threshold: 3, + executableAt: new Date(Date.now() + 1800000), + expiresAt: new Date(Date.now() + 86400000), + }, +]; + +export const TestPage = () => { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + + // State for interactive nonce selectors + const [selectedNonce1, setSelectedNonce1] = useState(4); + const [selectedNonce2, setSelectedNonce2] = useState(0); + const [selectedNonce3, setSelectedNonce3] = useState(2); + + // State for CappedTransferHubFormStep + const [hubFormData, setHubFormData] = useState({ + title: "", + recipientAddress: "", + epochLength: "10", + epochUnit: "weeks", + tokens: [ + { address: "", amount: "" }, + { address: "", amount: "" }, + ], + }); + + // Get safeAddress and chainId from URL params (to preserve in navigation) + const safeAddress = searchParams.get("safeAddress") || "0x9c798F32A328292b296d9B4AD4aEbC69ed012554"; + const chainId = searchParams.get("chainId") || "10"; + + // Test navigation to sign flow + const handleTestSignFlow = (hasExistingSignatures: boolean) => { + const state = { + actionBuilderAddress: "0x1111111111111111111111111111111111111111", + label: hasExistingSignatures ? "Existing Signed Transfer" : "New Unsigned Transfer", + factoryLabel: "Transfer", + nonce: hasExistingSignatures ? 2 : 5, + approversCount: hasExistingSignatures ? 2 : 0, + threshold: 3, + }; + navigate(`/queue/sign?safeAddress=${safeAddress}&chainId=${chainId}`, { state }); + }; + + return ( + + Component Test Page + Use this page to test and iterate on components in isolation + + {/* CappedTransferHubFormStep Test */} +
+ CappedTransferHubFormStep Component + Testing the Hub form with multiple tokens and proper divider styling. + + console.log("Continue clicked", hubFormData)} + onBack={() => console.log("Back clicked")} + onNavigateToCreate={() => console.log("Navigate to create")} + onChangeHub={() => console.log("Change hub")} + /> + +
+ + + +
+ Queue Sign Flow Test + + Navigate to Sign Flow + + Test the signing flow from queue items. The first button simulates a new unsigned item (editable nonce), the + second simulates an item with existing signatures (locked nonce). + + + handleTestSignFlow(false)}>Sign New Item (editable nonce) + handleTestSignFlow(true)}>Sign Existing Item (locked nonce) + + +
+ + + +
+ NonceSelector Component + + {/* Scenario 1: With queue items */} + + With Queue Items (recommended = 4) + + Shows nonces with existing queue items. #2 has "Empty USDC Transfer" (picked over "Deposit to Vault" because + it has more signatures). #3 has "Claim Rewards". + + + Selected: #{selectedNonce1} + + + + + {/* Scenario 2: Empty queue */} + + Empty Queue (recommended = current = 0) + + When there are no items in the queue, the recommended nonce equals the current Safe nonce. + + + Selected: #{selectedNonce2} + + + + + {/* Scenario 3: Read-only mode */} + + Read-Only Mode (for signing existing queue items) + + When a user clicks "Sign" on an existing queue item, the nonce is locked and shown as read-only text. + + + + + + + {/* Scenario 4: High nonce range */} + + High Nonce Values + Testing with higher nonce values to ensure proper display. + {}} + queueItems={[ + { + actionBuilderAddress: "0x4444444444444444444444444444444444444444", + label: "Large Transfer #152", + factoryLabel: "Transfer", + nonce: 152, + approversCount: 3, + threshold: 3, + executableAt: new Date(), + expiresAt: new Date(Date.now() + 86400000), + }, + ]} + /> + +
+
+ ); +}; + +// Styled Components +const Container = styled(Box)({ + display: "flex", + flexDirection: "column", + padding: "40px 60px", + minHeight: "100vh", + backgroundColor: canonHeaderTokens.background.layer0, +}); + +const Title = styled(Typography)({ + fontSize: "32px", + fontWeight: 600, + color: canonHeaderTokens.foreground.accent0, + marginBottom: "8px", +}); + +const Subtitle = styled(Typography)({ + fontSize: "16px", + color: canonHeaderTokens.foreground.accent10, + marginBottom: "40px", +}); + +const Section = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "24px", +}); + +const SectionTitle = styled(Typography)({ + fontSize: "24px", + fontWeight: 600, + color: canonHeaderTokens.foreground.accent0, + marginBottom: "16px", +}); + +const TestCase = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "12px", + padding: "24px", + backgroundColor: canonHeaderTokens.background.layer1, + borderRadius: "12px", +}); + +const TestLabel = styled(Typography)({ + fontSize: "16px", + fontWeight: 600, + color: canonHeaderTokens.foreground.accent0, +}); + +const TestDescription = styled(Typography)({ + fontSize: "14px", + color: canonHeaderTokens.foreground.accent10, + marginBottom: "8px", +}); + +const SelectedValue = styled(Typography)({ + fontSize: "13px", + fontFamily: "monospace", + color: canonHeaderTokens.brand.green, + marginTop: "8px", +}); + +const ButtonRow = styled(Box)({ + display: "flex", + gap: "16px", + marginTop: "8px", +}); + +const TestButton = styled(Button)({ + backgroundColor: canonHeaderTokens.brand.green, + color: "#000", + padding: "12px 24px", + borderRadius: "8px", + fontWeight: 600, + fontSize: "14px", + textTransform: "none", + "&:hover": { + backgroundColor: "#8ae58a", + }, +}); + +const HubFormContainer = styled(Box)({ + backgroundColor: canonHeaderTokens.background.layer0, + borderRadius: "12px", + overflow: "hidden", + width: "100%", +}); + +export default TestPage; diff --git a/src/pages/TestSigningFlow.tsx b/src/pages/TestSigningFlow.tsx new file mode 100644 index 0000000..e768abd --- /dev/null +++ b/src/pages/TestSigningFlow.tsx @@ -0,0 +1,192 @@ +import { useState, useEffect } from "react"; +import { Box, styled } from "@mui/material"; +import { useSearchParams } from "react-router-dom"; +import { optimism } from "viem/chains"; +import { Header } from "~/components/Header"; +import { SigningFlowStep } from "~/components/NewAction/steps"; +import type { TransferFormData } from "~/components/NewAction/steps"; +import { canonHeaderTokens } from "~/config/themes/safeTheme"; +import type { TransactionStep } from "~/services/transactionBuilderService"; + +// Mock Safe address for testing +const MOCK_SAFE_ADDRESS = "0x9c798F32A328292b296d9B4AD4aEbC69ed012554"; + +// Mock form data for testing +const MOCK_FORM_DATA: TransferFormData = { + title: "Bankless Transfer 2025-2026", + tokenAddress: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + recipientAddress: "0x4a9dE4AeA32B0B3e2711b8EEB85B5E209cB65EB5", + amount: "1000000", +}; + +// Mock transaction steps for testing (7 steps to match Figma) +const createMockSteps = (): TransactionStep[] => [ + { + id: "deploy-action", + title: "Deploy Contract", + description: "Deploy the transfer action builder contract", + status: "pending", + to: "0x55402f682b3FbD58eaE1d6E990FF8D086B2F9F61", + data: "0x096b81919b8fad769c5f9afa23efbbc5699812e21d94dc5fed8db69de67d1b6f", + }, + { + id: "save-canon-list", + title: "Save to Canon List", + description: "Save the action builder to Canon Guard Registry", + status: "pending", + to: "0x1d6f006964fBDf260B06cA38283Ec952B51f4f84", + data: "0xa1b2c3d4e5f6789012345678901234567890abcdef", + }, + { + id: "add-to-queue", + title: "Add to Queue", + description: "Add transaction to Canon Guard queue", + status: "pending", + to: "0x4E4536447B3f4adf2B62a1212f27Bd1B077b135D", + data: "0x1234567890abcdef1234567890abcdef1234567890abcdef", + }, + { + id: "sign-transaction", + title: "Sign", + description: "Sign the transaction in Safe", + status: "pending", + to: "0x9c798F32A328292b296d9B4AD4aEbC69ed012554", + data: "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + }, + { + id: "pre-approval-deploy", + title: "Pre-Approval: Deploy Contract", + description: "Deploy the pre-approval action", + status: "pending", + to: "0x55402f682b3FbD58eaE1d6E990FF8D086B2F9F61", + data: "0x5566778899aabbccddeeff00112233445566778899aabbcc", + }, + { + id: "pre-approval-queue", + title: "Pre-Approval: Add to Queue", + description: "Add pre-approval to Canon Guard queue", + status: "pending", + to: "0x4E4536447B3f4adf2B62a1212f27Bd1B077b135D", + data: "0xaabbccddeeff00112233445566778899aabbccddeeff0011", + }, + { + id: "pre-approval-sign", + title: "Pre-Approval: Sign", + description: "Sign the pre-approval in Safe", + status: "pending", + to: "0x9c798F32A328292b296d9B4AD4aEbC69ed012554", + data: "0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", + }, +]; + +export const TestSigningFlow = () => { + const [searchParams] = useSearchParams(); + const showComplete = searchParams.get("complete") === "true"; + + // Create steps with all signed if showing complete state + const createInitialSteps = () => { + const mockSteps = createMockSteps(); + if (showComplete) { + return mockSteps.map((step) => ({ ...step, status: "signed" as const })); + } + return mockSteps; + }; + + const [steps, setSteps] = useState(createInitialSteps); + const [currentStepIndex, setCurrentStepIndex] = useState(showComplete ? 7 : 0); + + // Update when URL changes + useEffect(() => { + if (showComplete) { + setSteps(createMockSteps().map((step) => ({ ...step, status: "signed" as const }))); + setCurrentStepIndex(7); + } else { + setSteps(createMockSteps()); + setCurrentStepIndex(0); + } + }, [showComplete]); + + const isComplete = steps.every((s) => s.status === "signed"); + + const handleSimulateSign = () => { + if (currentStepIndex >= steps.length) return; + + // Step 1: Set current step to "waiting" + const waitingSteps = [...steps]; + waitingSteps[currentStepIndex] = { + ...waitingSteps[currentStepIndex], + status: "waiting", + }; + setSteps(waitingSteps); + + // Step 2: After 3 seconds, mark as signed and advance + setTimeout(() => { + setSteps((prev) => { + const updatedSteps = [...prev]; + updatedSteps[currentStepIndex] = { + ...updatedSteps[currentStepIndex], + status: "signed", + }; + // Set next step to pending (ready to sign) + const nextIndex = currentStepIndex + 1; + if (nextIndex < updatedSteps.length) { + updatedSteps[nextIndex] = { + ...updatedSteps[nextIndex], + status: "pending", + }; + } + return updatedSteps; + }); + setCurrentStepIndex((prev) => prev + 1); + }, 3000); + }; + + const handleBack = () => { + // Reset for testing + setSteps(createMockSteps()); + setCurrentStepIndex(0); + }; + + const handleNavigateToCreate = () => { + // Reset for testing + setSteps(createMockSteps()); + setCurrentStepIndex(0); + }; + + const handleClearConfig = () => { + // For test route, just reset the flow + setSteps(createMockSteps()); + setCurrentStepIndex(0); + }; + + return ( + +
+ + + + + ); +}; + +const PageContainer = styled(Box)({ + display: "flex", + flexDirection: "column", + minHeight: "100vh", + width: "100%", + backgroundColor: canonHeaderTokens.background.layer0, +}); + +const MainContent = styled(Box)({ + flex: 1, + overflow: "auto", + backgroundColor: canonHeaderTokens.background.layer0, +}); diff --git a/src/pages/index.ts b/src/pages/index.ts index 34633db..11a950c 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -1,2 +1,4 @@ export * from "./Home"; export * from "./SafeVault"; +export * from "./TestSigningFlow"; +export * from "./TestPage"; diff --git a/src/providers/StateProvider.tsx b/src/providers/StateProvider.tsx index 782ec91..9c66dd7 100644 --- a/src/providers/StateProvider.tsx +++ b/src/providers/StateProvider.tsx @@ -1,22 +1,54 @@ import { createContext, useState } from "react"; import { Address } from "viem"; +import { SupportedChainId, DEFAULT_CHAIN_ID, getRpcUrlForChain, getViemChain } from "~/config/chains"; +import { ClientService, SafeService, CanonGuardService, QueueService } from "../services"; -type ContextType = { - loading: boolean; - setLoading: (val: boolean) => void; +interface ServiceInstances { + clientService: ClientService; + safeService: SafeService; + canonGuardService: CanonGuardService; + queueService: QueueService; +} + +const createServiceInstances = (chainId: SupportedChainId): ServiceInstances => { + const rpcUrl = getRpcUrlForChain(chainId); + const chain = getViemChain(chainId); + + const clientService = new ClientService(rpcUrl, chain); + const safeService = new SafeService(clientService); + const canonGuardService = new CanonGuardService(clientService); + const queueService = new QueueService(clientService); + + return { + clientService, + safeService, + canonGuardService, + queueService, + }; +}; + +const initialServices = createServiceInstances(DEFAULT_CHAIN_ID); +type ContextType = { isError: boolean; setIsError: (val: boolean) => void; - vaultAddress: Address | null; - setVaultAddress: (address: Address) => void; + safeAddress: Address | null; + setSafeAddress: (address: Address) => void; + + guardAddress: Address | null; + setGuardAddress: (address: Address) => void; - rpcUrl: string | null; - setRpcUrl: (url: string) => void; + // Whether the Canon Guard is in detached mode (not attached to the Safe) + isDetached: boolean; + setIsDetached: (val: boolean) => void; - isVaultConfigured: boolean; + chainId: SupportedChainId | null; + setChainId: (chainId: SupportedChainId) => void; - clearVaultConfig: () => void; + services: ServiceInstances; + + clearConfig: () => void; }; interface StateProps { @@ -26,39 +58,57 @@ interface StateProps { export const StateContext = createContext({} as ContextType); export const StateProvider = ({ children }: StateProps) => { - const [loading, setLoading] = useState(false); const [isError, setIsError] = useState(false); - const [vaultAddress, setVaultAddressState] = useState
(null); - const [rpcUrl, setRpcUrlState] = useState(null); + const [safeAddress, setSafeAddressState] = useState
(null); + const [guardAddress, setGuardAddressState] = useState
(null); + const [isDetached, setIsDetachedState] = useState(false); + const [chainId, setChainIdState] = useState(null); + const [services, setServices] = useState(initialServices); + + const updateServicesForChain = (newChainId: SupportedChainId) => { + const newServices = createServiceInstances(newChainId); + setServices(newServices); + }; + + const setSafeAddress = (address: Address) => { + setSafeAddressState(address); + }; - const setVaultAddress = (address: Address) => { - setVaultAddressState(address); + const setGuardAddress = (address: Address) => { + setGuardAddressState(address); }; - const setRpcUrl = (url: string) => { - setRpcUrlState(url); + const setIsDetached = (val: boolean) => { + setIsDetachedState(val); }; - const isVaultConfigured = Boolean(vaultAddress && rpcUrl); + const setChainId = (newChainId: SupportedChainId) => { + setChainIdState(newChainId); + updateServicesForChain(newChainId); + }; - const clearVaultConfig = () => { - setVaultAddressState(null); - setRpcUrlState(null); + const clearConfig = () => { + setSafeAddressState(null); + setGuardAddressState(null); + setIsDetachedState(false); + setChainIdState(null); }; return ( <>{children} diff --git a/src/providers/WalletConnectProvider.tsx b/src/providers/WalletConnectProvider.tsx new file mode 100644 index 0000000..32ba55c --- /dev/null +++ b/src/providers/WalletConnectProvider.tsx @@ -0,0 +1,398 @@ +/** + * WalletConnect Provider + * Acts as a WalletConnect receiver/wallet to accept transaction requests from dApps + */ + +import { createContext, useContext, useState, useEffect, useCallback, useRef, type ReactNode } from "react"; +import SignClient from "@walletconnect/sign-client"; +import { getSdkError } from "@walletconnect/utils"; +import { useStateContext } from "~/hooks/useStateContext"; +import type { SessionTypes, SignClientTypes } from "@walletconnect/types"; + +// Get project ID from environment +const PROJECT_ID = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID || ""; + +// App metadata for WalletConnect +const APP_METADATA: SignClientTypes.Metadata = { + name: "Canon Guard", + description: "Canon Guard - Safe Transaction Guard", + url: typeof window !== "undefined" ? window.location.origin : "https://canon.defi.sucks", + icons: ["https://canon.defi.sucks/icon.png"], +}; + +// Transaction request from WalletConnect +export interface WalletConnectTransaction { + from: string; + to: string; + data: string; + value: string; + gas?: string; + gasPrice?: string; +} + +// Parsed transaction ready for ArbitraryAction form +export interface ParsedWalletConnectTransaction { + target: string; + data: string; // Full calldata including selector + value: string; + requestId?: number; + topic?: string; + dappName?: string; // Name of the connected dApp +} + +interface WalletConnectContextType { + // Connection state + isInitialized: boolean; + isPairing: boolean; + pairingError: string | null; + sessions: SessionTypes.Struct[]; + + // Actions + pairWithUri: (uri: string) => Promise; + disconnect: (topic: string) => Promise; + disconnectAll: () => Promise; + clearPairingError: () => void; + + // Modal state + isModalOpen: boolean; + openModal: () => void; + closeModal: () => void; + + // Pending transaction (set when a dApp sends a tx request) + pendingTransaction: ParsedWalletConnectTransaction | null; + clearPendingTransaction: () => void; +} + +const WalletConnectContext = createContext(null); + +export const useWalletConnect = () => { + const context = useContext(WalletConnectContext); + if (!context) { + throw new Error("useWalletConnect must be used within WalletConnectProvider"); + } + return context; +}; + +interface WalletConnectProviderProps { + children: ReactNode; +} + +export const WalletConnectProvider = ({ children }: WalletConnectProviderProps) => { + const { chainId, safeAddress } = useStateContext(); + + const [signClient, setSignClient] = useState(null); + const [isInitialized, setIsInitialized] = useState(false); + const [isPairing, setIsPairing] = useState(false); + const [pairingError, setPairingError] = useState(null); + const [sessions, setSessions] = useState([]); + const [isModalOpen, setIsModalOpen] = useState(false); + const [pendingTransaction, setPendingTransaction] = useState(null); + + // Use refs to access current values in event handlers without re-registering + const chainIdRef = useRef(chainId); + const safeAddressRef = useRef(safeAddress); + + // Keep refs in sync + useEffect(() => { + chainIdRef.current = chainId; + safeAddressRef.current = safeAddress; + }, [chainId, safeAddress]); + + // Initialize SignClient + useEffect(() => { + if (!PROJECT_ID) { + console.warn("[WalletConnect] No project ID configured"); + return; + } + + let mounted = true; + + const init = async () => { + try { + const client = await SignClient.init({ + projectId: PROJECT_ID, + metadata: APP_METADATA, + }); + + if (!mounted) return; + + // Set up event listeners BEFORE setting state to ensure they're ready + client.on("session_proposal", async (proposal) => { + console.log("[WalletConnect] Session proposal received:", proposal); + + const { id, params } = proposal; + const { requiredNamespaces, optionalNamespaces } = params; + + try { + // Build namespaces based on required and optional namespaces + const namespaces: SessionTypes.Namespaces = {}; + const currentChainId = chainIdRef.current || 1; + const currentSafeAddress = safeAddressRef.current || "0x0000000000000000000000000000000000000000"; + const currentChain = `eip155:${currentChainId}`; + const currentAccount = `${currentChain}:${currentSafeAddress}`; + + // Process required namespaces + if (requiredNamespaces && Object.keys(requiredNamespaces).length > 0) { + for (const [key, value] of Object.entries(requiredNamespaces)) { + if (key === "eip155" || key.startsWith("eip155:")) { + const chains = value.chains || [currentChain]; + const accounts = chains.map((chain) => `${chain}:${currentSafeAddress}`); + + namespaces[key] = { + accounts, + methods: value.methods || [], + events: value.events || [], + chains, + }; + } + } + } + + // Process optional namespaces if no required ones + if (Object.keys(namespaces).length === 0 && optionalNamespaces) { + for (const [key, value] of Object.entries(optionalNamespaces)) { + if (key === "eip155" || key.startsWith("eip155:")) { + const chains = value.chains || [currentChain]; + const accounts = chains.map((chain) => `${chain}:${currentSafeAddress}`); + + namespaces[key] = { + accounts, + methods: value.methods || [], + events: value.events || [], + chains, + }; + } + } + } + + // If still empty, create a default namespace + if (Object.keys(namespaces).length === 0) { + namespaces["eip155"] = { + accounts: [currentAccount], + methods: ["eth_sendTransaction", "personal_sign", "eth_signTypedData", "eth_signTypedData_v4"], + events: ["chainChanged", "accountsChanged"], + chains: [currentChain], + }; + } + + console.log("[WalletConnect] Approving with namespaces:", namespaces); + + const session = await client.approve({ + id, + namespaces, + }); + + console.log("[WalletConnect] Session approved:", session); + + // Wait for session to be acknowledged + await session.acknowledged(); + console.log("[WalletConnect] Session acknowledged"); + + // Refresh sessions from client to get complete data + const updatedSessions = client.session.getAll(); + setSessions(updatedSessions); + setPairingError(null); + } catch (error) { + console.error("[WalletConnect] Failed to approve session:", error); + setPairingError("Failed to approve session"); + try { + await client.reject({ + id, + reason: getSdkError("USER_REJECTED"), + }); + } catch (rejectError) { + console.error("[WalletConnect] Failed to reject session:", rejectError); + } + } + }); + + client.on("session_request", async (event) => { + const { topic, params, id } = event; + const { request } = params; + + console.log("[WalletConnect] Session request:", request.method, params); + + // Get dApp name from session + const session = client.session.get(topic); + const dappName = session?.peer?.metadata?.name || "Unknown App"; + + if (request.method === "eth_sendTransaction") { + const tx = request.params[0] as WalletConnectTransaction; + + // Validate the transaction is for our Safe + const currentSafe = safeAddressRef.current; + if (currentSafe && tx.from.toLowerCase() !== currentSafe.toLowerCase()) { + console.warn("[WalletConnect] Transaction from address doesn't match Safe:", tx.from, currentSafe); + } + + // Parse the transaction - pass full calldata directly + const parsed: ParsedWalletConnectTransaction = { + target: tx.to, + data: tx.data || "0x", // Full calldata including selector + value: tx.value || "0", + requestId: id, + topic, + dappName, + }; + + setPendingTransaction(parsed); + setIsModalOpen(false); + + // Reject the request in WalletConnect (we handle it through Canon Guard flow) + try { + await client.respond({ + topic, + response: { + id, + jsonrpc: "2.0", + error: getSdkError("USER_REJECTED"), + }, + }); + } catch (error) { + console.error("[WalletConnect] Failed to respond to request:", error); + } + } else { + // Reject unsupported methods + try { + await client.respond({ + topic, + response: { + id, + jsonrpc: "2.0", + error: getSdkError("UNSUPPORTED_METHODS"), + }, + }); + } catch (error) { + console.error("[WalletConnect] Failed to reject unsupported method:", error); + } + } + }); + + client.on("session_delete", (event) => { + console.log("[WalletConnect] Session deleted:", event.topic); + setSessions((prev) => prev.filter((s) => s.topic !== event.topic)); + }); + + setSignClient(client); + setIsInitialized(true); + + // Load existing sessions + const existingSessions = client.session.getAll(); + setSessions(existingSessions); + + console.log("[WalletConnect] Initialized with", existingSessions.length, "existing sessions"); + } catch (error) { + console.error("[WalletConnect] Failed to initialize:", error); + } + }; + + init(); + + return () => { + mounted = false; + }; + }, []); + + // Pair with URI from dApp + const pairWithUri = useCallback( + async (uri: string) => { + if (!signClient) { + setPairingError("WalletConnect not initialized"); + return; + } + + if (!uri.startsWith("wc:")) { + setPairingError("Invalid WalletConnect URI"); + return; + } + + setIsPairing(true); + setPairingError(null); + + try { + await signClient.pair({ uri }); + console.log("[WalletConnect] Pairing initiated with URI"); + // Session approval happens in session_proposal handler + } catch (error) { + console.error("[WalletConnect] Failed to pair:", error); + setPairingError(error instanceof Error ? error.message : "Failed to connect"); + } finally { + setIsPairing(false); + } + }, + [signClient], + ); + + // Disconnect a specific session + const disconnect = useCallback( + async (topic: string) => { + if (!signClient) return; + + // First remove from local state + setSessions((prev) => prev.filter((s) => s.topic !== topic)); + + // Check if session exists before trying to disconnect + const sessionExists = signClient.session.keys.includes(topic); + if (!sessionExists) { + console.log("[WalletConnect] Session already removed:", topic); + return; + } + + try { + await signClient.disconnect({ + topic, + reason: getSdkError("USER_DISCONNECTED"), + }); + console.log("[WalletConnect] Disconnected session:", topic); + } catch { + // Session might have been removed by the dApp already + console.log("[WalletConnect] Session disconnect handled:", topic); + } + }, + [signClient], + ); + + // Disconnect all sessions + const disconnectAll = useCallback(async () => { + if (!signClient) return; + + for (const session of sessions) { + await disconnect(session.topic); + } + }, [signClient, sessions, disconnect]); + + const openModal = useCallback(() => { + setIsModalOpen(true); + setPairingError(null); + }, []); + + const closeModal = useCallback(() => { + setIsModalOpen(false); + }, []); + + const clearPairingError = useCallback(() => { + setPairingError(null); + }, []); + + const clearPendingTransaction = useCallback(() => { + setPendingTransaction(null); + }, []); + + const value: WalletConnectContextType = { + isInitialized, + isPairing, + pairingError, + sessions, + pairWithUri, + disconnect, + disconnectAll, + clearPairingError, + isModalOpen, + openModal, + closeModal, + pendingTransaction, + clearPendingTransaction, + }; + + return {children}; +}; diff --git a/src/providers/index.tsx b/src/providers/index.tsx index 5815ec5..82775b9 100644 --- a/src/providers/index.tsx +++ b/src/providers/index.tsx @@ -1,8 +1,10 @@ import type { ReactNode } from "react"; import { RouterProvider } from "react-router-dom"; +import { WalletConnectModal } from "~/components/WalletConnect"; import { router } from "~/router"; import { StateProvider } from "./StateProvider"; import { ThemeProvider } from "./ThemeProvider"; +import { WalletConnectProvider } from "./WalletConnectProvider"; import { WalletProvider } from "./WalletProvider"; type Props = { @@ -14,8 +16,11 @@ export const Providers = ({ children }: Props) => { - - {children} + + + + {children} + diff --git a/src/router/index.tsx b/src/router/index.tsx index 97326bf..933ab02 100644 --- a/src/router/index.tsx +++ b/src/router/index.tsx @@ -1,11 +1,58 @@ -import { createBrowserRouter } from "react-router-dom"; +import { createBrowserRouter, Navigate } from "react-router-dom"; import { AppLayout } from "~/containers"; -import { SafeVault } from "~/pages"; +import { SafeVault, TestPage } from "~/pages"; +// Lazy load the route components to avoid circular dependencies +// These will be rendered inside CanonGuardApp via Outlet export const router = createBrowserRouter([ { path: "/", element: , - children: [{ index: true, path: "/", element: }], + children: [ + // Landing page - Safe selection + { index: true, element: }, + // Main app routes (all render SafeVault which handles auth and shows CanonGuardApp) + { path: "queue", element: }, + { path: "queue/sign", element: }, + { path: "queue-action", element: }, + { path: "canon-list", element: }, + { path: "settings", element: }, + { path: "settings/attach", element: }, + { path: "settings/detach", element: }, + { + path: "create", + element: , + children: [ + { index: true, element: null }, // Main create view + { + path: "action", + element: null, + children: [ + { index: true, element: null }, // Select factory + { path: "transfer", element: null }, // Transfer form (review is internal state) + { path: "arbitrary-action", element: null }, // Arbitrary action form + { path: "claim-allowance", element: null }, // Claim allowance form + { path: "turn-off-emergency", element: null }, // Turn off emergency mode signing flow + ], + }, + { + path: "hub", + element: null, + children: [ + { index: true, element: null }, // Select hub type + { path: "capped-transfer", element: null }, // Capped transfer hub form + ], + }, + { + path: "hub-child/:hubAddress", + element: null, // Deploy child from hub + }, + ], + }, + // Test route for design iteration (temporary) + { path: "test", element: }, + // Catch-all redirect to home + { path: "*", element: }, + ], }, ]); diff --git a/src/services/canonGuardService.ts b/src/services/canonGuardService.ts index d0ddac0..84d28f8 100644 --- a/src/services/canonGuardService.ts +++ b/src/services/canonGuardService.ts @@ -1,174 +1,351 @@ -import { Address } from "viem"; -import { optimism } from "viem/chains"; -import { OPTIMISM_MAINNET_RPC } from "../constants/addresses"; +/** + * Canon Guard Service - Handles all Canon Guard entrypoint contract operations + * + * Responsibilities: + * - Fetch queued transactions + * - Classify actions by factory type and determine transaction states + * - Aggregate complete Canon Guard data with execution history + */ + +import { Address, PublicClient, Hash, Hex } from "viem"; +import { canonGuardEntrypointAbi, actionBuilderAbi } from "../abis"; +import { + getFactoryType, + getFactoryLabel, + getFactoryLabelByType, + KNOWN_FACTORY_MAPPINGS, +} from "../constants/canonGuard"; import { QueuedTransaction, - ExecutedTransaction, PreApprovedItem, - CanonGuardConfiguration, - ActionDetails, - VaultData, - ActionFactoryType, + CanonGuardData, QueuedTransactionState, PreApprovedItemType, -} from "../types/canon-guard"; + ActionFactoryType, +} from "../types"; +import { parseMulticallResults } from "../utils/multicall"; import { ClientService } from "./clientService"; -import { SafeService } from "./safeService"; - -const tempClientService = new ClientService(OPTIMISM_MAINNET_RPC, optimism); -const safeService = new SafeService(tempClientService); - -export const FACTORY_LABELS: Record = { - "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984": "Simple Actions Factory", - "0xA0b86a33E6441097C3be01cF8BA5c2C70A3c8B24": "Simple Transfers Factory", - "0x6B175474E89094C44Da98b954EedeAC495271d0F": "Capped Token Transfers Factory", -}; - -class CanonGuardService { - async getQueuedTransactions(_safe: Address): Promise { - void _safe; - return [ - { - actionBuilder: { - address: "0xaddress", - factoryType: ActionFactoryType.SIMPLE_ACTIONS, - factoryAddress: "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", - createdAt: new Date(Date.now() - 2 * 60 * 60 * 1000), - isApproved: false, - }, - state: QueuedTransactionState.QUEUED, - queuedAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago - executableAt: new Date(Date.now() + 22 * 60 * 60 * 1000), // 22 hours from now - expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days from now - safeTxHash: "0xaddress", - approversCount: 1, - requiredApprovals: 2, - approvers: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"], - }, - { - actionBuilder: { - address: "0xefgh5678901234567890abcd5678901234567890", - factoryType: ActionFactoryType.SIMPLE_TRANSFERS, - factoryAddress: "0xA0b86a33E6441097C3be01cF8BA5c2C70A3c8B24", - createdAt: new Date(Date.now() - 1 * 60 * 60 * 1000), - isApproved: true, - approvalExpiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), - }, - state: QueuedTransactionState.EXECUTABLE, - queuedAt: new Date(Date.now() - 1 * 60 * 60 * 1000), // 1 hour ago - executableAt: new Date(Date.now() - 30 * 60 * 1000), // 30 minutes ago (executable now) - expiresAt: new Date(Date.now() + 6 * 24 * 60 * 60 * 1000), // 6 days from now - safeTxHash: "0xaddress", - approversCount: 2, - requiredApprovals: 2, - approvers: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"], - }, - ]; - } - async getExecutionHistory(_safe: Address): Promise { - void _safe; - return [ - { - actionBuilder: { - address: "0xaddress", - factoryType: ActionFactoryType.CAPPED_TOKEN_TRANSFERS, - factoryAddress: "0x6B175474E89094C44Da98b954EedeAC495271d0F", - createdAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000), - isApproved: false, - }, - safeTxHash: "0xaddress", - executedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), // 2 days ago - executedBy: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - approvers: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"], - gasUsed: 150000, - txHash: "0xaddress", - }, - ]; +const SECONDS_TO_MILLISECONDS = 1000; +const ONE_HOUR_IN_MILLISECONDS = 60 * 60 * 1000; +const ONE_DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1000; +const ZERO_HASH = `0x${"0".repeat(64)}`; + +const FUNCTION_SELECTORS = { + ERC20_TRANSFER: "0x" + "a9059cbb", + ERC20_TRANSFER_FROM: "0x" + "23b872dd", + ERC20_APPROVE: "0x" + "095ea7b3", + CUSTOM_APPROVAL: "0x" + "d77c9b49", +} as const; + +interface ActionDetails { + actionsData: Hex; + executableAt: bigint; + expiresAt: bigint; + safeTxHash: Hash; + approvalExpiry: bigint; + approvers: Address[]; +} + +interface FactoryClassification { + [actionAddress: Address]: { + factoryType: ActionFactoryType; + factoryLabel: string; + actionBuilderAddress: Address; + }; +} + +export class CanonGuardService { + private clientService: ClientService; + + constructor(clientService: ClientService) { + this.clientService = clientService; } - async getPreApprovedItems(_safe: Address): Promise { - void _safe; - return [ - { - address: "0xA0b86a33E6441097C3be01cF8BA5c2C70A3c8B24", - type: PreApprovedItemType.BUILDER, - factoryType: ActionFactoryType.SIMPLE_TRANSFERS, - approvedAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // 7 days ago - expiresAt: new Date(Date.now() + 23 * 24 * 60 * 60 * 1000), // 23 days from now - approvalDuration: 30 * 24 * 60 * 60, // 30 days in seconds - }, - { - address: "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", - type: PreApprovedItemType.HUB, - approvedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), // 2 days ago - expiresAt: new Date(Date.now() + 28 * 24 * 60 * 60 * 1000), // 28 days from now - approvalDuration: 30 * 24 * 60 * 60, // 30 days in seconds - }, - ]; + private get client(): PublicClient { + return this.clientService.getClient(); } - async getGuardConfiguration(safe: Address): Promise { + async getCanonGuardData( + entrypointAddress: Address, + safeThreshold: number, + ): Promise> { + const allActionAddresses = await this.fetchAllActionAddresses(entrypointAddress); + + const actionDetailsMap = await this.fetchActionDetails(entrypointAddress, allActionAddresses); + + const factoryClassifications = await this.classifyActionsByFactory(allActionAddresses); + + const { queuedTransactions, preApprovedItems } = this.buildFinalActionObjects( + allActionAddresses, + actionDetailsMap, + factoryClassifications, + safeThreshold, + ); + return { - vaultAddress: safe, - entrypointAddress: "0x1234567890123456789012345678901234567890", - shortTxExecutionDelay: 3600, // 1 hour - longTxExecutionDelay: 86400, // 24 hours - txExpiryDelay: 604800, // 7 days - maxApprovalDuration: 2592000, // 30 days - emergencyTriggerAddress: "0xabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd", - emergencyCallerAddress: "0xefefefefefefefefefefefefefefefefefefef", - isEmergencyMode: false, + queuedTransactions, + preApprovedItems, + executionHistory: [], }; } - async getActionDetails(actionBuilder: Address): Promise { - return { - actionBuilder, - target: "0xA0b86a33E6441097C3be01cF8BA5c2C70A3c8B24", - value: "0", - calldata: - "0x095ea7b3000000000000000000000000a0b86a33e6441097c3be01cf8ba5c2c70a3c8b24000000000000000000000000000000000000000000000000de0b6b3a7640000", - fnSignature: "approve(address,uint256)", - decodedParams: { - spender: "0xa0b86a33e6441097c3be01cf8ba5c2c70a3c8b24", - amount: "1000000000000000000", - }, - }; + private async fetchAllActionAddresses(entrypointAddress: Address): Promise { + try { + const queuedResult = await this.client.readContract({ + address: entrypointAddress, + abi: [ + { + type: "function", + name: "getQueuedActionBuilders", + inputs: [], + outputs: [{ name: "_queuedActionBuilders", type: "address[]" }], + stateMutability: "view", + }, + ], + functionName: "getQueuedActionBuilders", + }); + + const queued = Array.isArray(queuedResult) ? queuedResult : []; + return queued; + } catch (error) { + console.error("Failed to fetch action addresses:", error); + return []; + } } - async isActionPreApproved(_safe: Address, actionHash: string): Promise { - return actionHash.includes("efgh"); + private async fetchActionDetails( + entrypointAddress: Address, + actionAddresses: Address[], + ): Promise> { + if (actionAddresses.length === 0) return new Map(); + + try { + const safeNonce = await this.getSafeNonce(entrypointAddress); + const detailsMap = new Map(); + + // We fetch all the details in one multicall + const contracts = actionAddresses.flatMap((address) => [ + { + address: entrypointAddress, + abi: canonGuardEntrypointAbi, + functionName: "queuedTransactions", + args: [address], + }, + { + address: entrypointAddress, + abi: canonGuardEntrypointAbi, + functionName: "getSafeTransactionHash", + args: [address], + }, + { + address: entrypointAddress, + abi: canonGuardEntrypointAbi, + functionName: "approvalExpiries", + args: [address], + }, + { + address: entrypointAddress, + abi: canonGuardEntrypointAbi, + functionName: "getApprovedHashSigners", + args: [address, BigInt(safeNonce)], + }, + ]); + + const results = await this.client.multicall({ contracts }); + const values = parseMulticallResults(results); + + // Now we iterate over the results, knowing that types will repeat every 4 items + for (let i = 0; i < actionAddresses.length; i++) { + const actionAddress = actionAddresses[i]; + const baseIndex = i * 4; + + const queuedTransactionResult = values[baseIndex] as [Hex, bigint, bigint]; + const safeTxHash = values[baseIndex + 1] as Hash; + const approvalExpiry = values[baseIndex + 2] as bigint; + const approvers = values[baseIndex + 3] as Address[]; + + if (!queuedTransactionResult) continue; + + const [actionsData, executableAt, expiresAt] = queuedTransactionResult; + + detailsMap.set(actionAddress, { + actionsData, + executableAt, + expiresAt, + safeTxHash, + approvalExpiry, + approvers, + }); + } + + return detailsMap; + } catch (error) { + console.error("Failed to fetch action details:", error); + return new Map(); + } } - async getTimeToExecution(actionHash: string): Promise { - if (actionHash.includes("efgh")) { - return 1 * 60 * 60 * 1000; // 1 hour + private async identifyActionBuilderFactory(actionBuilderAddress: Address): Promise<{ + factoryType: ActionFactoryType; + factoryLabel: string; + }> { + const directFactoryMatch = KNOWN_FACTORY_MAPPINGS[actionBuilderAddress]; + if (directFactoryMatch) { + return { factoryType: directFactoryMatch.type, factoryLabel: directFactoryMatch.label }; + } + + let actionsResult; + try { + actionsResult = await this.client.readContract({ + address: actionBuilderAddress, + abi: actionBuilderAbi, + functionName: "getActions", + }); + } catch { + actionsResult = null; + } + + if (!actionsResult || !Array.isArray(actionsResult) || actionsResult.length === 0) { + return { factoryType: ActionFactoryType.UNKNOWN, factoryLabel: getFactoryLabel(actionBuilderAddress) }; + } + + const firstAction = actionsResult[0]; + if (!firstAction || typeof firstAction !== "object" || !("data" in firstAction)) { + return { factoryType: ActionFactoryType.UNKNOWN, factoryLabel: getFactoryLabel(actionBuilderAddress) }; } - return 22 * 60 * 60 * 1000; // 22 hours + + const data = (firstAction as { data: string }).data.toLowerCase(); + + if (data.startsWith(FUNCTION_SELECTORS.ERC20_TRANSFER) || data.startsWith(FUNCTION_SELECTORS.ERC20_TRANSFER_FROM)) { + return { + factoryType: ActionFactoryType.SIMPLE_TRANSFERS, + factoryLabel: getFactoryLabelByType(ActionFactoryType.SIMPLE_TRANSFERS), + }; + } + + if (data.startsWith(FUNCTION_SELECTORS.ERC20_APPROVE) || data.startsWith(FUNCTION_SELECTORS.CUSTOM_APPROVAL)) { + return { + factoryType: ActionFactoryType.APPROVE_ACTION, + factoryLabel: getFactoryLabelByType(ActionFactoryType.APPROVE_ACTION), + }; + } + + return { factoryType: ActionFactoryType.UNKNOWN, factoryLabel: getFactoryLabel(actionBuilderAddress) }; } - async getApprovalCount(_safe: Address, nonce: number): Promise { - return nonce === 5 ? 1 : 0; + private async classifyActionsByFactory(actionBuilderAddresses: Address[]): Promise { + if (actionBuilderAddresses.length === 0) return {}; + + const classification: FactoryClassification = {}; + + for (const actionBuilderAddress of actionBuilderAddresses) { + const factoryInfo = await this.identifyActionBuilderFactory(actionBuilderAddress); + + classification[actionBuilderAddress] = { + factoryType: factoryInfo.factoryType, + factoryLabel: factoryInfo.factoryLabel, + actionBuilderAddress: actionBuilderAddress, + }; + } + // TODO: Research using blockchain events to track factory deployments for proper classification + + return classification; } - async getVaultData(safe: Address): Promise { - const [vaultInfo, configuration, queuedTransactions, executionHistory, preApprovedItems] = await Promise.all([ - safeService.getVaultInfo(safe), - this.getGuardConfiguration(safe), - this.getQueuedTransactions(safe), - this.getExecutionHistory(safe), - this.getPreApprovedItems(safe), - ]); + private buildFinalActionObjects( + allActionAddresses: Address[], + actionDetailsMap: Map, + factoryClassifications: FactoryClassification, + safeThreshold: number, + ): { queuedTransactions: QueuedTransaction[]; preApprovedItems: PreApprovedItem[] } { + const now = Date.now() / SECONDS_TO_MILLISECONDS; + const queuedTransactions: QueuedTransaction[] = []; + const preApprovedItems: PreApprovedItem[] = []; - return { - vaultInfo, - configuration, - queuedTransactions, - preApprovedItems, - executionHistory, - }; + for (const [actionAddress, details] of actionDetailsMap.entries()) { + const { executableAt, expiresAt, safeTxHash, approvalExpiry, approvers } = details; + + const executableTimestamp = Number(executableAt); + const expiresTimestamp = Number(expiresAt); + + let state: QueuedTransactionState; + if (expiresTimestamp < now) { + state = QueuedTransactionState.EXPIRED; + } else if (executableTimestamp <= now) { + state = QueuedTransactionState.EXECUTABLE; + } else { + state = QueuedTransactionState.QUEUED; + } + + const classification = factoryClassifications[actionAddress]; + const factoryType = + classification?.factoryType || getFactoryType(actionAddress) || ActionFactoryType.ARBITRARY_ACTIONS; + const factoryLabel = classification?.factoryLabel || getFactoryLabel(actionAddress); + const actionBuilderAddress = classification?.actionBuilderAddress || actionAddress; + const isApproved = Number(approvalExpiry) > now; + const actionBuilder = { + address: actionAddress, + factoryType, + actionBuilderAddress, + factoryLabel, + // TODO: Get real creation timestamp from blockchain events instead of assuming 1 day before executable + createdAt: new Date(executableTimestamp * SECONDS_TO_MILLISECONDS - ONE_DAY_IN_MILLISECONDS), + isApproved, + approvalExpiresAt: isApproved ? new Date(Number(approvalExpiry) * SECONDS_TO_MILLISECONDS) : undefined, + }; + + if (allActionAddresses.includes(actionAddress)) { + queuedTransactions.push({ + actionBuilder, + state, + // TODO: Get real queued timestamp from blockchain events instead of assuming 1 hour before executable + queuedAt: new Date(executableTimestamp * SECONDS_TO_MILLISECONDS - ONE_HOUR_IN_MILLISECONDS), + executableAt: new Date(executableTimestamp * SECONDS_TO_MILLISECONDS), + expiresAt: new Date(expiresTimestamp * SECONDS_TO_MILLISECONDS), + safeTxHash, + approversCount: approvers.length, + requiredApprovals: safeThreshold, + approvers, + }); + } + + const hasPartialApprovals = approvers.length > 0 && approvers.length < safeThreshold; + const hasValidNonce = safeTxHash && safeTxHash !== ZERO_HASH; + + if (hasPartialApprovals && hasValidNonce) { + preApprovedItems.push({ + address: actionAddress, + type: PreApprovedItemType.BUILDER, + factoryType, + // TODO: Get real approval timestamp from blockchain events instead of assuming 1 day before executable + approvedAt: new Date(executableTimestamp * SECONDS_TO_MILLISECONDS - ONE_DAY_IN_MILLISECONDS), + expiresAt: new Date(expiresTimestamp * SECONDS_TO_MILLISECONDS), + approvalDuration: Math.max(0, expiresTimestamp - now), + safeTxHash, + approversCount: approvers.length, + requiredApprovals: safeThreshold, + approvers, + }); + } + } + + queuedTransactions.sort((a, b) => b.queuedAt.getTime() - a.queuedAt.getTime()); + preApprovedItems.sort((a, b) => b.approvedAt.getTime() - a.approvedAt.getTime()); + + return { queuedTransactions, preApprovedItems }; } -} -export const canonGuardService = new CanonGuardService(); + private async getSafeNonce(entrypointAddress: Address): Promise { + try { + const result = await this.client.readContract({ + address: entrypointAddress, + abi: canonGuardEntrypointAbi, + functionName: "getSafeNonce", + }); + return typeof result === "bigint" ? Number(result) : 0; + } catch { + return 0; + } + } +} diff --git a/src/services/canonGuardValidationService.ts b/src/services/canonGuardValidationService.ts new file mode 100644 index 0000000..dca277e --- /dev/null +++ b/src/services/canonGuardValidationService.ts @@ -0,0 +1,103 @@ +/** + * Canon Guard Validation Service + * + * Provides centralized validation logic to determine if an address + * is a legitimate Canon Guard deployed from a known factory. + * + * 3-step validation: + * 1. Call PARENT() on the guard - if it fails, not a Canon Guard + * 2. Check returned factory address against known factories + * 3. Call factory.isChild(guardAddress) to confirm deployment + */ + +import { Address, PublicClient, zeroAddress } from "viem"; +import { actionBuilderParentAbi, canonGuardFactoryAbi } from "../abis/canonGuard"; +import { KNOWN_CANON_GUARD_FACTORIES } from "../config/factories"; + +export interface ValidationResult { + isValid: boolean; + factoryAddress?: Address; + error?: string; +} + +export class CanonGuardValidationService { + private client: PublicClient; + + constructor(client: PublicClient) { + this.client = client; + } + + /** + * Validate an address is a legitimate Canon Guard using 3-step check: + * 1. Call PARENT() - must not fail + * 2. PARENT() result must be a known factory address + * 3. Factory.isChild(guardAddress) must return true + */ + async isValidCanonGuard(guardAddress: Address): Promise { + const result = await this.validateCanonGuard(guardAddress); + return result.isValid; + } + + /** + * Full validation with detailed results + */ + async validateCanonGuard(guardAddress: Address): Promise { + if (!guardAddress || guardAddress === zeroAddress) { + return { isValid: false, error: "Invalid or zero address" }; + } + + // Step 1: Try to call PARENT() on the guard + let factoryAddress: Address; + try { + factoryAddress = (await this.client.readContract({ + address: guardAddress, + abi: actionBuilderParentAbi, + functionName: "PARENT", + })) as Address; + } catch { + return { + isValid: false, + error: "This address is not a supported Canon Guard", + }; + } + + // Step 2: Check if the factory address is in our known factories list + const isKnownFactory = KNOWN_CANON_GUARD_FACTORIES.some( + (known) => known.toLowerCase() === factoryAddress.toLowerCase(), + ); + + if (!isKnownFactory) { + return { + isValid: false, + factoryAddress, + error: `PARENT() returned unknown factory: ${factoryAddress}`, + }; + } + + // Step 3: Verify the guard is actually a child of the factory + try { + const isChild = await this.client.readContract({ + address: factoryAddress, + abi: canonGuardFactoryAbi, + functionName: "isChild", + args: [guardAddress], + }); + + if (!isChild) { + return { + isValid: false, + factoryAddress, + error: "Factory does not recognize this address as a child", + }; + } + + return { isValid: true, factoryAddress }; + } catch { + return { + isValid: false, + factoryAddress, + error: "Failed to verify isChild() on factory", + }; + } + } +} diff --git a/src/services/fourByteValidation.ts b/src/services/fourByteValidation.ts new file mode 100644 index 0000000..5a72a20 --- /dev/null +++ b/src/services/fourByteValidation.ts @@ -0,0 +1,38 @@ +/** + * Signature validation utilities + * Separated to handle viem imports cleanly + */ + +import { keccak256, toBytes, slice } from "viem"; + +/** + * Compute the 4-byte selector from a function signature + * @param signature - Function signature (e.g., "transfer(address,uint256)") + * @returns The 4-byte selector (e.g., "0xa9059cbb") + */ +export const computeSelectorFromSignature = (signature: string): string => { + try { + const hash = keccak256(toBytes(signature)); + return slice(hash, 0, 4); + } catch { + return ""; + } +}; + +/** + * Validate that a signature matches an expected selector + * @param signature - Function signature to validate (e.g., "transfer(address,uint256)") + * @param expectedSelector - Expected 4-byte selector (e.g., "0xa9059cbb") + * @returns true if the signature produces the expected selector + */ +export const validateSignatureMatchesSelector = (signature: string, expectedSelector: string): boolean => { + if (!signature || !expectedSelector) { + return false; + } + + const computedSelector = computeSelectorFromSignature(signature); + const normalizedExpected = expectedSelector.toLowerCase(); + const normalizedComputed = computedSelector.toLowerCase(); + + return normalizedExpected === normalizedComputed; +}; diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 0000000..cf578b2 --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1,10 @@ +export { ClientService } from "./clientService"; +export { SafeService } from "./safeService"; +export { CanonGuardService } from "./canonGuardService"; +export { RegistryService } from "./registryService"; +export type { RegisteredEntity } from "./registryService"; +export { QueueService } from "./queueService"; +export type { QueueItem } from "./queueService"; +export * from "./transactionBuilderService"; +export { CanonGuardValidationService } from "./canonGuardValidationService"; +export type { ValidationResult } from "./canonGuardValidationService"; diff --git a/src/services/queueService.ts b/src/services/queueService.ts new file mode 100644 index 0000000..e9a0201 --- /dev/null +++ b/src/services/queueService.ts @@ -0,0 +1,839 @@ +/** + * Queue Service - Fetches and processes queued transactions from Canon Guard + * + * Responsibilities: + * - Fetch queued action builders from Canon Guard + * - Scan nonces (currentNonce to currentNonce + queueSize + buffer) to find approvals + * - Create queue items for each action builder with their actual signed nonce + * - Determine factory types and fetch labels from registry + */ + +import { Address, Hash, Hex, PublicClient } from "viem"; +import { + canonGuardAbi, + safeAbi, + canonGuardRegistryAbi, + actionBuilderParentAbi, + actionHubChildAbi, + preApproveActionAbi, + changeSafeGuardActionAbi, +} from "../abis/canonGuard"; +import { CANON_GUARD_REGISTRY, KNOWN_FACTORY_MAPPINGS, getHubFactoryType } from "../constants/canonGuard"; +import { ActionFactoryType } from "../types"; +import { HUB_FACTORY_DISPLAY_NAMES } from "../utils/factoryDisplay"; +import { ClientService } from "./clientService"; + +// Number of extra nonces to scan beyond queue size +const NONCE_SCAN_BUFFER = 10; + +// Pre-deployed UnsetEmergencyModeAction contract - has a hardcoded label +const UNSET_EMERGENCY_MODE_ACTION: Address = "0x68e54338e31C7A8B7c46a2BB8Fd73f3a0606A506"; + +/** + * Represents a queue item - an action builder at a specific nonce with approvals + */ +export interface QueueItem { + actionBuilderAddress: Address; + nonce: number; + currentNonce: number; + isAtCurrentNonce: boolean; + + // Transaction info + proposer: Address; + actionsData: Hex; + executableAt: Date; + expiresAt: Date; + isPreApproved: boolean; + + // Approval info + safeTxHash: Hash; + approvers: Address[]; + approversCount: number; + threshold: number; + + // Factory info + factoryType: ActionFactoryType; + factoryLabel: string; + + // Hub child info (for actions that are children of a hub) + isHubChild: boolean; + hubAddress?: Address; + hubType?: string; // e.g., "Capped Transfer" + hubLabel?: string; // Label from registry, or "Untitled Hub" + + // Label from registry (if available) + label: string; + + // Computed state + isFullySigned: boolean; + isExecutable: boolean; + isExpired: boolean; + hasExecutionDelay: boolean; + executionDelayRemaining: number; // seconds remaining +} + +export class QueueService { + private clientService: ClientService; + + constructor(clientService: ClientService) { + this.clientService = clientService; + } + + private get client(): PublicClient { + return this.clientService.getClient(); + } + + /** + * Fetch all queue items with forward nonce scanning + * Scans from currentNonce to currentNonce + queueSize + NONCE_SCAN_BUFFER + * to find signatures at any nonce + */ + async getQueueItems(guardAddress: Address, safeAddress: Address): Promise { + // Step 1: Get queued action builders + const actionBuilders = await this.getQueuedActionBuilders(guardAddress); + if (actionBuilders.length === 0) return []; + + // Step 2: Get current Safe nonce and threshold + const [currentNonce, threshold] = await Promise.all([ + this.getSafeNonce(guardAddress), + this.getSafeThreshold(safeAddress), + ]); + + // Step 3: Calculate nonce scan range (forward scanning) + // Scan from currentNonce to currentNonce + queueSize + buffer + const endNonce = currentNonce + actionBuilders.length + NONCE_SCAN_BUFFER; + + console.log( + `[QueueService] Scanning nonces from ${currentNonce} to ${endNonce} for ${actionBuilders.length} action builders`, + ); + + // Step 4: Batch fetch all data in parallel + const [transactionInfoMap, factoryTypeMap, approvalsByActionAndNonce] = await Promise.all([ + this.batchFetchTransactionInfo(guardAddress, actionBuilders), + this.batchFetchFactoryTypes(actionBuilders, guardAddress), + this.batchScanAllNonces(guardAddress, actionBuilders, currentNonce, endNonce), + ]); + + // Step 5: Identify pre-approve actions and fetch labels + const preApproveActions = actionBuilders.filter( + (addr) => factoryTypeMap.get(addr)?.type === ActionFactoryType.APPROVE_ACTION, + ); + const regularActions = actionBuilders.filter( + (addr) => factoryTypeMap.get(addr)?.type !== ActionFactoryType.APPROVE_ACTION, + ); + + const underlyingActionMap = await this.batchFetchUnderlyingActions(preApproveActions); + const underlyingAddresses = Array.from(underlyingActionMap.values()); + const allAddressesToFetchLabels = [...regularActions, ...underlyingAddresses]; + const labelsMap = await this.batchFetchLabels(guardAddress, allAddressesToFetchLabels); + + // Step 5b: For CHANGE_SAFE_GUARD actions without a label, check if they're removing the guard + const unlabeledChangeSafeGuardActions = actionBuilders.filter((addr) => { + const factoryInfo = factoryTypeMap.get(addr); + const label = labelsMap.get(addr); + return factoryInfo?.type === ActionFactoryType.CHANGE_SAFE_GUARD && !label; + }); + const safeGuardAddressMap = await this.batchFetchSafeGuardAddresses(unlabeledChangeSafeGuardActions); + + // Step 6: Pre-compute best nonces for all action builders + const bestNonceMap = new Map(); + for (const actionBuilder of actionBuilders) { + const approvalsByNonce = approvalsByActionAndNonce.get(actionBuilder) || new Map(); + bestNonceMap.set(actionBuilder, this.findBestNonce(approvalsByNonce, currentNonce)); + } + + // Step 7: Batch fetch all safeTxHashes in a single multicall + const safeTxHashMap = await this.batchFetchSafeTxHashes(guardAddress, actionBuilders, bestNonceMap); + + // Step 8: Build queue items + const queueItems: QueueItem[] = []; + const now = Date.now(); + + for (const actionBuilder of actionBuilders) { + const txInfo = transactionInfoMap.get(actionBuilder); + if (!txInfo) continue; + + // Get factory info + const isEmergencyModeAction = actionBuilder.toLowerCase() === UNSET_EMERGENCY_MODE_ACTION.toLowerCase(); + const factoryInfo = isEmergencyModeAction + ? { type: ActionFactoryType.UNKNOWN, label: "Emergency" } + : factoryTypeMap.get(actionBuilder) || { + type: ActionFactoryType.UNKNOWN, + label: "Unknown", + }; + + // Build label + let label: string; + if (isEmergencyModeAction) { + label = "Turn Off Emergency Mode"; + } else if (factoryInfo.type === ActionFactoryType.APPROVE_ACTION) { + const underlyingAction = underlyingActionMap.get(actionBuilder); + const underlyingLabel = underlyingAction ? labelsMap.get(underlyingAction) : ""; + label = `Pre-Approval | ${underlyingLabel || "Untitled Transaction"}`; + } else if ( + factoryInfo.type === ActionFactoryType.CHANGE_SAFE_GUARD && + !labelsMap.get(actionBuilder) && + safeGuardAddressMap.get(actionBuilder) === "0x0000000000000000000000000000000000000000" + ) { + label = "Remove Safe Guard"; + } else if ( + factoryInfo.type === ActionFactoryType.CHANGE_SAFE_GUARD && + !labelsMap.get(actionBuilder) && + safeGuardAddressMap.get(actionBuilder) !== "0x0000000000000000000000000000000000000000" + ) { + label = "Add Guard to Safe"; + } else { + label = labelsMap.get(actionBuilder) || ""; + } + + // Get pre-computed best nonce and safeTxHash + const { bestNonce, approvers } = bestNonceMap.get(actionBuilder) || { bestNonce: currentNonce, approvers: [] }; + const safeTxHash = + safeTxHashMap.get(actionBuilder) || + ("0x0000000000000000000000000000000000000000000000000000000000000000" as Hash); + + console.log( + `[QueueService] Action builder ${actionBuilder}: ${JSON.stringify({ + label, + currentNonce, + bestNonce, + approversCount: approvers.length, + })}`, + ); + const executableAtMs = Number(txInfo.executableAt) * 1000; + const expiresAtMs = Number(txInfo.expiresAt) * 1000; + + const isFullySigned = approvers.length >= threshold; + const isExpired = expiresAtMs < now; + const hasExecutionDelay = executableAtMs > now; + const executionDelayRemaining = Math.max(0, Math.floor((executableAtMs - now) / 1000)); + + // Executable only if: fully signed, not expired, delay passed, AND at current nonce + const isExecutable = isFullySigned && !isExpired && !hasExecutionDelay && bestNonce === currentNonce; + + const queueItem: QueueItem = { + actionBuilderAddress: actionBuilder, + nonce: bestNonce, + currentNonce, + isAtCurrentNonce: bestNonce === currentNonce, + proposer: txInfo.proposer, + actionsData: txInfo.actionsData, + executableAt: new Date(executableAtMs), + expiresAt: new Date(expiresAtMs), + isPreApproved: txInfo.isPreApproved, + safeTxHash, + approvers, + approversCount: approvers.length, + threshold, + factoryType: factoryInfo.type, + factoryLabel: factoryInfo.label, + isHubChild: factoryInfo.isHubChild, + hubAddress: factoryInfo.hubAddress, + hubType: factoryInfo.hubType, + hubLabel: factoryInfo.hubLabel, + label, + isFullySigned, + isExecutable, + isExpired, + hasExecutionDelay, + executionDelayRemaining, + }; + + console.log( + `[QueueService] Adding queue item: ${JSON.stringify({ + label, + nonce: bestNonce, + approversCount: approvers.length, + threshold, + isFullySigned, + isAtCurrentNonce: bestNonce === currentNonce, + })}`, + ); + + queueItems.push(queueItem); + } + + // Sort: items needing signatures first (warning state), then by label + return queueItems.sort((a, b) => { + const aIsWarning = a.approversCount === 0; + const bIsWarning = b.approversCount === 0; + if (aIsWarning && !bIsWarning) return -1; + if (!aIsWarning && bIsWarning) return 1; + return (a.label || "").localeCompare(b.label || ""); + }); + } + + /** + * Find the best nonce for an action builder + * - If no signatures found → return currentNonce with empty approvers (unsigned state) + * - If signatures found → return nonce with most signatures (earliest nonce as tiebreaker) + */ + private findBestNonce( + approvalsByNonce: Map, + currentNonce: number, + ): { bestNonce: number; approvers: Address[] } { + let bestNonce = currentNonce; + let bestApprovers: Address[] = []; + let maxSignatures = 0; + + for (const [nonce, approvers] of approvalsByNonce) { + if (approvers.length > maxSignatures) { + maxSignatures = approvers.length; + bestNonce = nonce; + bestApprovers = approvers; + } else if (approvers.length === maxSignatures && approvers.length > 0 && nonce < bestNonce) { + // Same number of signatures, prefer earlier nonce + bestNonce = nonce; + bestApprovers = approvers; + } + } + + return { bestNonce, approvers: bestApprovers }; + } + + /** + * Batch scan all nonces for all action builders in a single multicall + * Returns Map> + */ + private async batchScanAllNonces( + guardAddress: Address, + actionBuilders: Address[], + startNonce: number, + endNonce: number, + ): Promise>> { + const result = new Map>(); + + // Initialize result map + for (const actionBuilder of actionBuilders) { + result.set(actionBuilder, new Map()); + } + + if (actionBuilders.length === 0) return result; + + // Build all nonces to scan + const nonces: number[] = []; + for (let nonce = startNonce; nonce <= endNonce; nonce++) { + nonces.push(nonce); + } + + // Build single multicall for ALL action builders x ALL nonces + const contracts: { + address: Address; + abi: typeof canonGuardAbi; + functionName: "getApprovedHashSigners"; + args: [Address, bigint]; + }[] = []; + + // Track which index corresponds to which (actionBuilder, nonce) pair + const indexMap: { actionBuilder: Address; nonce: number }[] = []; + + for (const actionBuilder of actionBuilders) { + for (const nonce of nonces) { + contracts.push({ + address: guardAddress, + abi: canonGuardAbi, + functionName: "getApprovedHashSigners", + args: [actionBuilder, BigInt(nonce)], + }); + indexMap.push({ actionBuilder, nonce }); + } + } + + console.log( + `[QueueService] Executing batch multicall for ${contracts.length} calls (${actionBuilders.length} actions x ${nonces.length} nonces)`, + ); + + try { + const results = await this.client.multicall({ contracts }); + + for (let i = 0; i < results.length; i++) { + const { actionBuilder, nonce } = indexMap[i]; + const callResult = results[i]; + + const actionMap = result.get(actionBuilder)!; + + if (callResult.status === "success" && callResult.result) { + const approvers = callResult.result as Address[]; + actionMap.set(nonce, approvers); + + // Log non-empty approvers for debugging + if (approvers.length > 0) { + console.log(`[QueueService] Found ${approvers.length} approvers for ${actionBuilder} at nonce ${nonce}`); + } + } else { + actionMap.set(nonce, []); + } + } + } catch (error) { + console.error("[QueueService] Failed to batch scan nonces:", error); + } + + return result; + } + + /** + * Get count of items in queue (for navbar) + */ + async getQueueCount(guardAddress: Address): Promise { + const actionBuilders = await this.getQueuedActionBuilders(guardAddress); + return actionBuilders.length; + } + + private async getQueuedActionBuilders(guardAddress: Address): Promise { + try { + const result = await this.client.readContract({ + address: guardAddress, + abi: canonGuardAbi, + functionName: "getQueuedActionBuilders", + }); + return (result as Address[]) || []; + } catch (error) { + console.error("Failed to fetch queued action builders:", error); + return []; + } + } + + private async getSafeNonce(guardAddress: Address): Promise { + try { + console.log("[QueueService] Fetching Safe nonce from guard:", guardAddress); + const result = await this.client.readContract({ + address: guardAddress, + abi: canonGuardAbi, + functionName: "getSafeNonce", + }); + const nonce = Number(result); + console.log("[QueueService] Safe nonce fetched:", nonce); + return nonce; + } catch (error) { + console.error("[QueueService] Failed to fetch Safe nonce:", error); + return 0; + } + } + + private async getSafeThreshold(safeAddress: Address): Promise { + try { + const result = await this.client.readContract({ + address: safeAddress, + abi: safeAbi, + functionName: "getThreshold", + }); + return Number(result); + } catch (error) { + console.error("Failed to fetch Safe threshold:", error); + return 1; + } + } + + private async batchFetchTransactionInfo( + guardAddress: Address, + actionBuilders: Address[], + ): Promise< + Map< + Address, + { + proposer: Address; + actionsData: Hex; + executableAt: bigint; + expiresAt: bigint; + isPreApproved: boolean; + } + > + > { + const map = new Map< + Address, + { + proposer: Address; + actionsData: Hex; + executableAt: bigint; + expiresAt: bigint; + isPreApproved: boolean; + } + >(); + + if (actionBuilders.length === 0) return map; + + const contracts = actionBuilders.map((address) => ({ + address: guardAddress, + abi: canonGuardAbi, + functionName: "transactionsInfo" as const, + args: [address], + })); + + try { + const results = await this.client.multicall({ contracts }); + + for (let i = 0; i < actionBuilders.length; i++) { + const result = results[i]; + if (result.status === "success" && result.result) { + const [proposer, actionsData, executableAt, expiresAt, isPreApproved] = result.result as [ + Address, + Hex, + bigint, + bigint, + boolean, + ]; + map.set(actionBuilders[i], { + proposer, + actionsData, + executableAt, + expiresAt, + isPreApproved, + }); + } + } + } catch (error) { + console.error("Failed to batch fetch transaction info:", error); + } + + return map; + } + + /** + * Factory info including optional hub child information + */ + private async batchFetchFactoryTypes( + actionBuilders: Address[], + guardAddress: Address, + ): Promise< + Map< + Address, + { + type: ActionFactoryType; + label: string; + isHubChild: boolean; + hubAddress?: Address; + hubType?: string; + hubLabel?: string; + } + > + > { + const map = new Map< + Address, + { + type: ActionFactoryType; + label: string; + isHubChild: boolean; + hubAddress?: Address; + hubType?: string; + hubLabel?: string; + } + >(); + + if (actionBuilders.length === 0) return map; + + // First check known factory mappings (action builder might be a factory) + for (const address of actionBuilders) { + if (KNOWN_FACTORY_MAPPINGS[address]) { + map.set(address, { ...KNOWN_FACTORY_MAPPINGS[address], isHubChild: false }); + } + } + + // For unknown ones, call PARENT() to get factory address + const unknownBuilders = actionBuilders.filter((addr) => !map.has(addr)); + + if (unknownBuilders.length === 0) return map; + + const parentContracts = unknownBuilders.map((address) => ({ + address, + abi: actionBuilderParentAbi, + functionName: "PARENT" as const, + })); + + // Track which builders have unknown parents (potential hub children) + const potentialHubChildren: { actionBuilder: Address; parentAddress: Address }[] = []; + + try { + const parentResults = await this.client.multicall({ contracts: parentContracts }); + + for (let i = 0; i < unknownBuilders.length; i++) { + const result = parentResults[i]; + if (result.status === "success" && result.result) { + const parentFactory = result.result as Address; + const factoryMapping = KNOWN_FACTORY_MAPPINGS[parentFactory]; + if (factoryMapping) { + map.set(unknownBuilders[i], { ...factoryMapping, isHubChild: false }); + } else { + // Parent is not a known factory - might be a hub child + potentialHubChildren.push({ actionBuilder: unknownBuilders[i], parentAddress: parentFactory }); + } + } else { + map.set(unknownBuilders[i], { + type: ActionFactoryType.UNKNOWN, + label: "Unknown", + isHubChild: false, + }); + } + } + } catch (error) { + console.error("Failed to batch fetch factory types:", error); + // Set all unknown builders to UNKNOWN + for (const builder of unknownBuilders) { + if (!map.has(builder)) { + map.set(builder, { type: ActionFactoryType.UNKNOWN, label: "Unknown", isHubChild: false }); + } + } + return map; + } + + // For potential hub children, try calling HUB() to confirm they are hub children + if (potentialHubChildren.length > 0) { + const hubContracts = potentialHubChildren.map(({ actionBuilder }) => ({ + address: actionBuilder, + abi: actionHubChildAbi, + functionName: "HUB" as const, + })); + + try { + const hubResults = await this.client.multicall({ contracts: hubContracts }); + + const confirmedHubChildren: { actionBuilder: Address; hubAddress: Address }[] = []; + + for (let i = 0; i < potentialHubChildren.length; i++) { + const { actionBuilder } = potentialHubChildren[i]; + const hubResult = hubResults[i]; + + if (hubResult.status === "success" && hubResult.result) { + // This is a hub child - store hub address for further processing + const hubAddress = hubResult.result as Address; + confirmedHubChildren.push({ actionBuilder, hubAddress }); + } else { + // Not a hub child - mark as unknown + map.set(actionBuilder, { + type: ActionFactoryType.UNKNOWN, + label: "Unknown", + isHubChild: false, + }); + } + } + + // For confirmed hub children, get hub factory types by calling PARENT() on the hubs + if (confirmedHubChildren.length > 0) { + const uniqueHubAddresses = [...new Set(confirmedHubChildren.map((c) => c.hubAddress))]; + + // Call PARENT() on each hub to get hub factory type + const hubParentContracts = uniqueHubAddresses.map((hubAddress) => ({ + address: hubAddress, + abi: actionBuilderParentAbi, + functionName: "PARENT" as const, + })); + + // Also fetch hub labels from registry + const hubLabelContracts = uniqueHubAddresses.map((hubAddress) => ({ + address: CANON_GUARD_REGISTRY, + abi: canonGuardRegistryAbi, + functionName: "entityLabel" as const, + args: [guardAddress, hubAddress], + })); + + const [hubParentResults, hubLabelResults] = await Promise.all([ + this.client.multicall({ contracts: hubParentContracts }), + this.client.multicall({ contracts: hubLabelContracts }), + ]); + + // Build maps for hub info + const hubFactoryMap = new Map(); + const hubLabelMap = new Map(); + + for (let i = 0; i < uniqueHubAddresses.length; i++) { + const hubAddress = uniqueHubAddresses[i]; + + // Get hub factory type + const parentResult = hubParentResults[i]; + if (parentResult.status === "success" && parentResult.result) { + const hubFactoryAddress = parentResult.result as Address; + const hubFactoryType = getHubFactoryType(hubFactoryAddress); + if (hubFactoryType) { + hubFactoryMap.set(hubAddress, { + type: hubFactoryType, + displayName: HUB_FACTORY_DISPLAY_NAMES[hubFactoryType] || "Unknown Hub", + }); + } else { + hubFactoryMap.set(hubAddress, { + type: ActionFactoryType.UNKNOWN, + displayName: "Unknown Hub", + }); + } + } + + // Get hub label + const labelResult = hubLabelResults[i]; + if (labelResult.status === "success" && labelResult.result) { + const edition = labelResult.result as { label: string; lastEditedAt: bigint }; + hubLabelMap.set(hubAddress, edition.label || ""); + } + } + + // Now populate the map for each confirmed hub child + for (const { actionBuilder, hubAddress } of confirmedHubChildren) { + const hubFactoryInfo = hubFactoryMap.get(hubAddress); + const hubLabel = hubLabelMap.get(hubAddress) || ""; + + map.set(actionBuilder, { + type: hubFactoryInfo?.type ?? ActionFactoryType.CAPPED_TOKEN_TRANSFERS_HUB_CHILD, + label: hubFactoryInfo?.displayName ?? "Capped Transfer", + isHubChild: true, + hubAddress, + hubType: hubFactoryInfo?.displayName ?? "Capped Transfer", + hubLabel: hubLabel || "Untitled Hub", + }); + } + } + } catch (error) { + console.error("Failed to detect hub children:", error); + // Fall back to UNKNOWN for potential hub children + for (const { actionBuilder } of potentialHubChildren) { + if (!map.has(actionBuilder)) { + map.set(actionBuilder, { + type: ActionFactoryType.UNKNOWN, + label: "Unknown", + isHubChild: false, + }); + } + } + } + } + + return map; + } + + private async batchFetchLabels(guardAddress: Address, actionBuilders: Address[]): Promise> { + const map = new Map(); + + if (actionBuilders.length === 0) return map; + + const contracts = actionBuilders.map((address) => ({ + address: CANON_GUARD_REGISTRY, + abi: canonGuardRegistryAbi, + functionName: "entityLabel" as const, + args: [guardAddress, address], + })); + + try { + const results = await this.client.multicall({ contracts }); + + for (let i = 0; i < actionBuilders.length; i++) { + const result = results[i]; + if (result.status === "success" && result.result) { + const edition = result.result as { label: string; lastEditedAt: bigint }; + map.set(actionBuilders[i], edition.label || ""); + } + } + } catch (error) { + console.error("Failed to batch fetch labels:", error); + } + + return map; + } + + /** + * For pre-approve actions, fetch the underlying action builder address + * by calling ACTIONS_BUILDER() on each pre-approve action contract + */ + private async batchFetchUnderlyingActions(preApproveActions: Address[]): Promise> { + const map = new Map(); + + if (preApproveActions.length === 0) return map; + + const contracts = preApproveActions.map((address) => ({ + address, + abi: preApproveActionAbi, + functionName: "ACTIONS_BUILDER" as const, + })); + + try { + const results = await this.client.multicall({ contracts }); + + for (let i = 0; i < preApproveActions.length; i++) { + const result = results[i]; + if (result.status === "success" && result.result) { + const underlyingAction = result.result as Address; + map.set(preApproveActions[i], underlyingAction); + } + } + } catch (error) { + console.error("Failed to batch fetch underlying actions:", error); + } + + return map; + } + + /** + * Batch fetch safeTxHashes for all action builders at their best nonces + * in a single multicall + */ + private async batchFetchSafeTxHashes( + guardAddress: Address, + actionBuilders: Address[], + bestNonceMap: Map, + ): Promise> { + const map = new Map(); + + if (actionBuilders.length === 0) return map; + + const contracts = actionBuilders.map((actionBuilder) => { + const { bestNonce } = bestNonceMap.get(actionBuilder) || { bestNonce: 0 }; + return { + address: guardAddress, + abi: canonGuardAbi, + functionName: "getSafeTransactionHash" as const, + args: [actionBuilder, BigInt(bestNonce)], + }; + }); + + try { + const results = await this.client.multicall({ contracts }); + + for (let i = 0; i < actionBuilders.length; i++) { + const result = results[i]; + if (result.status === "success" && result.result) { + map.set(actionBuilders[i], result.result as Hash); + } else { + map.set(actionBuilders[i], "0x0000000000000000000000000000000000000000000000000000000000000000" as Hash); + } + } + } catch (error) { + console.error("Failed to batch fetch safeTxHashes:", error); + } + + return map; + } + + /** + * Get the current Safe nonce (public method for external use) + * This is useful for nonce selection in signing flows + */ + async getCurrentSafeNonce(guardAddress: Address): Promise { + return this.getSafeNonce(guardAddress); + } + + /** + * Batch fetch SAFE_GUARD() addresses from ChangeSafeGuardAction contracts + * Used to determine if a CHANGE_SAFE_GUARD action is removing the guard (address(0)) + */ + private async batchFetchSafeGuardAddresses(actionBuilders: Address[]): Promise> { + const map = new Map(); + + if (actionBuilders.length === 0) return map; + + const contracts = actionBuilders.map((address) => ({ + address, + abi: changeSafeGuardActionAbi, + functionName: "SAFE_GUARD" as const, + })); + + try { + const results = await this.client.multicall({ contracts }); + + for (let i = 0; i < actionBuilders.length; i++) { + const result = results[i]; + if (result.status === "success" && result.result) { + map.set(actionBuilders[i], result.result as Address); + } + } + } catch (error) { + console.error("Failed to batch fetch SAFE_GUARD addresses:", error); + } + + return map; + } +} diff --git a/src/services/registryService.ts b/src/services/registryService.ts new file mode 100644 index 0000000..1b74be9 --- /dev/null +++ b/src/services/registryService.ts @@ -0,0 +1,338 @@ +/** + * Registry Service - Handles fetching and processing registered entities from CanonGuardRegistry + * + * Uses viem's multicall to batch PARENT() and approvalExpiries() calls for efficiency + */ + +import { Address, PublicClient, erc20Abi } from "viem"; +import { + canonGuardRegistryAbi, + actionBuilderParentAbi, + approvalExpiriesAbi, + cappedTokenTransfersHubAbi, +} from "../abis/canonGuard"; +import { CANON_GUARD_REGISTRY, KNOWN_FACTORY_MAPPINGS, isHubFactory, getHubFactoryType } from "../constants/canonGuard"; +import { ActionFactoryType, HubFactoryType, CappedTokenTransfersHubInfo, HubTokenConfig } from "../types/canon-guard"; +import { ClientService } from "./clientService"; + +export interface RegisteredEntity { + address: Address; + label: string; + factoryType: ActionFactoryType; + factoryLabel: string; + isFastPath: boolean; + lastEditedAt: Date; + // Hub-related fields + isHub: boolean; + hubType?: HubFactoryType; + parentHubAddress?: Address; // For children: the hub address this child belongs to + childrenCount?: number; // For hubs: number of children +} + +interface RegistryEntity { + entity: Address; + edition: { + label: string; + lastEditedAt: bigint; + }; +} + +export class RegistryService { + private clientService: ClientService; + + constructor(clientService: ClientService) { + this.clientService = clientService; + } + + private get client(): PublicClient { + return this.clientService.getClient(); + } + + /** + * Fetch all registered entities from the CanonGuardRegistry + * Uses multicall to batch PARENT() and approvalExpiries() calls + */ + async getRegisteredEntities( + guardAddress: Address, + entrypointAddress: Address, + offset: number = 0, + limit: number = 100, + ): Promise { + try { + // Step 1: Fetch entities from registry + const entities = await this.fetchEntitiesFromRegistry(guardAddress, offset, limit); + + if (entities.length === 0) { + return []; + } + + // Step 2: Batch fetch PARENT() and approvalExpiries() using multicall + const enrichedEntities = await this.enrichEntitiesWithMulticall(entities, entrypointAddress); + + return enrichedEntities; + } catch (error) { + console.error("Failed to fetch registered entities:", error); + return []; + } + } + + /** + * Fetch raw entities from the registry contract + */ + private async fetchEntitiesFromRegistry( + guardAddress: Address, + offset: number, + limit: number, + ): Promise { + try { + const result = await this.client.readContract({ + address: CANON_GUARD_REGISTRY, + abi: canonGuardRegistryAbi, + functionName: "read", + args: [guardAddress, BigInt(offset), BigInt(limit)], + }); + + // Type assertion - result should be an array of entities + const entities = result as RegistryEntity[]; + return entities; + } catch (error) { + console.error("Failed to read from registry:", error); + return []; + } + } + + /** + * Enrich entities with factory type and fast-path status using a single multicall + * This batches 2N calls (N PARENT() + N approvalExpiries()) into 1 RPC request + */ + private async enrichEntitiesWithMulticall( + entities: RegistryEntity[], + entrypointAddress: Address, + ): Promise { + // Build the multicall contracts array + // For each entity, we need: PARENT() and approvalExpiries() + // Results will be interleaved: [parent1, approval1, parent2, approval2, ...] + const contracts = entities.flatMap((entity) => [ + { + address: entity.entity, + abi: actionBuilderParentAbi, + functionName: "PARENT" as const, + }, + { + address: entrypointAddress, + abi: approvalExpiriesAbi, + functionName: "approvalExpiries" as const, + args: [entity.entity], + }, + ]); + + // Execute multicall + const results = await this.client.multicall({ + contracts, + allowFailure: true, + }); + + const now = Math.floor(Date.now() / 1000); + const enrichedEntities: RegisteredEntity[] = []; + + // Process results - they come back interleaved + for (let i = 0; i < entities.length; i++) { + const entity = entities[i]; + const parentResult = results[i * 2]; + const approvalResult = results[i * 2 + 1]; + + // Determine factory type and hub status from PARENT() result + let factoryType = ActionFactoryType.UNKNOWN; + let factoryLabel = "Unknown"; + let isHub = false; + let hubType: HubFactoryType | undefined; + let parentHubAddress: Address | undefined; + + if (parentResult.status === "success" && parentResult.result) { + const parentAddress = parentResult.result as Address; + + // Check if this is a hub (parent is a hub factory) + if (isHubFactory(parentAddress)) { + isHub = true; + hubType = getHubFactoryType(parentAddress) ?? undefined; + factoryType = ActionFactoryType.CAPPED_TOKEN_TRANSFERS; + factoryLabel = "Capped Token Transfers Hub"; + } else { + // Check if parent is a known factory + const mapping = KNOWN_FACTORY_MAPPINGS[parentAddress]; + if (mapping) { + factoryType = mapping.type; + factoryLabel = mapping.label; + } else { + // Parent is not a known factory - could be a hub (this entity is a child) + // We'll check later if any hub contains this as a child + parentHubAddress = parentAddress; + } + } + } + + // Determine fast-path status from approvalExpiries() + // Fast-path if approval is not expired (and not 0) + let isFastPath = false; + if (approvalResult.status === "success" && approvalResult.result) { + const expiryTimestamp = Number(approvalResult.result as bigint); + isFastPath = expiryTimestamp > 0 && expiryTimestamp > now; + } + + enrichedEntities.push({ + address: entity.entity, + label: entity.edition.label || "Untitled Transaction", + factoryType, + factoryLabel, + isFastPath, + lastEditedAt: new Date(Number(entity.edition.lastEditedAt) * 1000), + isHub, + hubType, + parentHubAddress, + }); + } + + // Second pass: identify hub children and count children for hubs + // Build a map of hub addresses to their entities + const hubAddresses = enrichedEntities.filter((e) => e.isHub).map((e) => e.address); + + // For entities that have a parentHubAddress, check if it's a hub in our list + for (const entity of enrichedEntities) { + if (entity.parentHubAddress && hubAddresses.includes(entity.parentHubAddress)) { + // This is a child of a hub - update its factory type + entity.factoryType = ActionFactoryType.CAPPED_TOKEN_TRANSFERS; + entity.factoryLabel = "Capped Transfer"; + } + } + + // Count children for each hub + for (const hub of enrichedEntities.filter((e) => e.isHub)) { + hub.childrenCount = enrichedEntities.filter((e) => e.parentHubAddress === hub.address).length; + } + + return enrichedEntities; + } + + /** + * Get the total count of entities for a guard address + */ + async getTotalEntities(guardAddress: Address): Promise { + try { + const result = await this.client.readContract({ + address: CANON_GUARD_REGISTRY, + abi: canonGuardRegistryAbi, + functionName: "totalEntities", + args: [guardAddress], + }); + + return Number(result as bigint); + } catch (error) { + console.error("Failed to get total entities:", error); + return 0; + } + } + + /** + * Fetch hub configuration for a CappedTokenTransfersHub + * Includes tokens, caps, cap remaining, recipient, and epoch length + */ + async getHubConfiguration(hubAddress: Address): Promise { + try { + // First, fetch basic hub info and token list + const [recipient, epochLength, tokens] = await Promise.all([ + this.client.readContract({ + address: hubAddress, + abi: cappedTokenTransfersHubAbi, + functionName: "RECIPIENT", + }) as Promise
, + this.client.readContract({ + address: hubAddress, + abi: cappedTokenTransfersHubAbi, + functionName: "EPOCH_LENGTH", + }) as Promise, + this.client.readContract({ + address: hubAddress, + abi: cappedTokenTransfersHubAbi, + functionName: "tokens", + }) as Promise, + ]); + + if (!tokens || tokens.length === 0) { + return { + address: hubAddress, + recipient, + epochLength, + tokens: [], + }; + } + + // Fetch cap, capLeft, and decimals for each token using multicall + const tokenContracts = tokens.flatMap((token) => [ + { + address: hubAddress, + abi: cappedTokenTransfersHubAbi, + functionName: "cap" as const, + args: [token], + }, + { + address: hubAddress, + abi: cappedTokenTransfersHubAbi, + functionName: "capLeft" as const, + args: [token], + }, + { + address: token, + abi: erc20Abi, + functionName: "decimals" as const, + }, + ]); + + const tokenResults = await this.client.multicall({ + contracts: tokenContracts, + allowFailure: true, + }); + + const tokenConfigs: HubTokenConfig[] = tokens.map((tokenAddress, index) => { + const capResult = tokenResults[index * 3]; + const capLeftResult = tokenResults[index * 3 + 1]; + const decimalsResult = tokenResults[index * 3 + 2]; + + return { + address: tokenAddress, + cap: capResult.status === "success" ? (capResult.result as bigint) : 0n, + capLeft: capLeftResult.status === "success" ? (capLeftResult.result as bigint) : 0n, + decimals: decimalsResult.status === "success" ? (decimalsResult.result as number) : 18, + }; + }); + + return { + address: hubAddress, + recipient, + epochLength, + tokens: tokenConfigs, + }; + } catch (error) { + console.error("Failed to fetch hub configuration:", error); + return null; + } + } + + /** + * Check if an address is a child of a specific hub + */ + async isHubChild(hubAddress: Address, childAddress: Address): Promise { + try { + const result = await this.client.readContract({ + address: hubAddress, + abi: cappedTokenTransfersHubAbi, + functionName: "isHubChild", + args: [childAddress], + }); + + return result as boolean; + } catch (error) { + console.error("Failed to check if address is hub child:", error); + return false; + } + } +} diff --git a/src/services/safeService.ts b/src/services/safeService.ts index dc83569..3975f09 100644 --- a/src/services/safeService.ts +++ b/src/services/safeService.ts @@ -4,12 +4,13 @@ * Responsibilities: * - Read Safe configuration (owners, threshold, nonce) * - Detect guard contracts via storage slot reading + * - Validate Canon Guard deployment via factory check */ -import { Address, keccak256, toHex, PublicClient } from "viem"; -import { ZERO_ADDRESS } from "~/utils/hex"; +import { Address, keccak256, toHex, PublicClient, getAddress, zeroAddress } from "viem"; import { safeAbi } from "../abis/safe"; -import { VaultInfo } from "../types"; +import { SafeInfo } from "../types"; +import { CanonGuardValidationService } from "./canonGuardValidationService"; import { ClientService } from "./clientService"; export class SafeService { @@ -42,10 +43,13 @@ export class SafeService { if (!rawGuardData) return null; // Extract the address from the last 20 bytes (40 hex chars) of the storage slot - const guardAddress = `0x${rawGuardData.slice(-40)}` as Address; + const rawAddress = `0x${rawGuardData.slice(-40)}` as Address; + + // Normalize to checksummed address + const guardAddress = getAddress(rawAddress); // If the extracted address is the zero address, no guard is set - if (guardAddress === ZERO_ADDRESS) return null; + if (guardAddress === zeroAddress) return null; return guardAddress; } catch (error) { @@ -55,17 +59,33 @@ export class SafeService { } /** - * Validate Canon Guard entrypoint + * Validate that a guard address was deployed from a supported CanonGuardFactory + * This ensures the guard is a legitimate Canon Guard and not an arbitrary contract. + * + * Uses 3-step validation: + * 1. Call PARENT() on the guard - must not fail + * 2. PARENT() result must be a known factory address + * 3. Factory.isChild(guardAddress) must return true */ - async isValidCanonGuardEntrypoint(): Promise { - // TODO: Implement this by checking if the guard address is a valid Canon Guard entrypoint - return true; + async isValidCanonGuard(guardAddress: Address): Promise { + if (!guardAddress || guardAddress === zeroAddress) { + return false; + } + + try { + const validationService = new CanonGuardValidationService(this.client); + return await validationService.isValidCanonGuard(guardAddress); + } catch (error) { + console.error("Failed to validate Canon Guard:", error); + return false; + } } /** - * Get complete vault information using multicall for efficiency + * Get complete Safe information using multicall for efficiency + * Returns info about the Safe and its guard status */ - async getVaultInfo(safe: Address): Promise { + async getSafeInfo(safe: Address): Promise { const [multicallResults, guardAddress] = await Promise.all([ this.client.multicall({ contracts: [ @@ -105,6 +125,15 @@ export class SafeService { const threshold = Number(thresholdResult.result); const nonce = Number(nonceResult.result); const chain = this.clientService.getChain(); + + // Determine guard status + const hasGuard = guardAddress !== null; + let isValidCanonGuard = false; + + if (hasGuard && guardAddress) { + isValidCanonGuard = await this.isValidCanonGuard(guardAddress); + } + return { address: safe, chainId: chain.id, @@ -112,10 +141,17 @@ export class SafeService { threshold, owners, totalOwners: owners.length, - // TODO: Verify that the guard address is a valid Canon Guard entrypoint - hasCanonGuard: guardAddress !== null, + hasGuard, + isValidCanonGuard, guardAddress: guardAddress || undefined, nonce, }; } + + /** + * @deprecated Use getSafeInfo instead + */ + async getVaultInfo(safe: Address): Promise { + return this.getSafeInfo(safe); + } } diff --git a/src/services/transactionBuilderService.ts b/src/services/transactionBuilderService.ts new file mode 100644 index 0000000..ea5a06f --- /dev/null +++ b/src/services/transactionBuilderService.ts @@ -0,0 +1,750 @@ +/** + * Transaction Builder Service - Builds transaction steps for Canon Guard operations + * + * Responsibilities: + * - Build transaction metadata based on user selections (deploy, propose, pre-approve) + * - Encode contract calls for deployment, queuing, and signing + * - Provide transaction data for wallet signing + */ + +import { Address, Hex, encodeFunctionData, parseUnits } from "viem"; +import { + simpleTransfersFactoryAbi, + arbitraryActionsFactoryAbi, + allowanceClaimorFactoryAbi, + cappedTokenTransfersHubFactoryAbi, + canonGuardRegistryAbi, + canonGuardEntrypointAbi, + preApproveActionFactoryAbi, + safeAbi, +} from "../abis/canonGuard"; +import { cappedTokenTransfersHubAbi } from "../abis/canonGuard"; +import { + CANON_GUARD_REGISTRY, + PRE_APPROVE_ACTION_FACTORY, + SIMPLE_TRANSFERS_FACTORY, + ARBITRARY_ACTIONS_FACTORY, + ALLOWANCE_CLAIMOR_FACTORY, + CAPPED_TOKEN_TRANSFERS_HUB_FACTORY, +} from "../constants/canonGuard"; +import { EPOCH_TIME_MULTIPLIERS } from "../utils/timeUnits"; +import type { + TransferFormData, + ArbitraryActionFormData, + ClaimAllowanceFormData, + CappedTransferHubFormData, + HubChildFormData, +} from "../components/NewAction/steps"; + +// Transaction step status +export type TransactionStepStatus = "pending" | "waiting" | "signed" | "error"; + +// Transaction step definition +export interface TransactionStep { + id: string; + title: string; + description: string; + status: TransactionStepStatus; + to: Address; + data: Hex; + value?: bigint; +} + +// Options for building transaction steps +export interface BuildTransactionStepsOptions { + formData: TransferFormData; + safeAddress: Address; + guardAddress: Address; + proposeTransaction: boolean; + proposePreApproval: boolean; + approvalDurationSeconds?: bigint; // User-provided duration in seconds +} + +// Result of building transaction steps +export interface TransactionStepsResult { + steps: TransactionStep[]; + actionBuilderAddress?: Address; // Predicted address from CREATE2 +} + +// Default pre-approval duration (1 hour = 3600 seconds) - used only if not specified +const DEFAULT_PRE_APPROVAL_DURATION = 3600n; + +/** + * Builds the list of transaction steps based on checkbox selections + */ +export function buildTransactionSteps(options: BuildTransactionStepsOptions): TransactionStepsResult { + const { formData, safeAddress, guardAddress, proposeTransaction, proposePreApproval, approvalDurationSeconds } = + options; + + // Use the user-provided duration or fall back to default + const preApprovalDuration = approvalDurationSeconds ?? DEFAULT_PRE_APPROVAL_DURATION; + + const steps: TransactionStep[] = []; + + // Build array of transfer actions from form data + const transferActions = formData.transfers.map((transfer) => ({ + token: transfer.tokenAddress as Address, + to: transfer.recipientAddress as Address, + amount: parseUnits(transfer.amount || "0", 18), + })); + + // 1. MANDATORY: Deploy Action Builder + // This deploys a SimpleTransfers action via the factory + // The factory takes an array of TransferAction tuples: [{token, to, amount}] + const deployData = encodeFunctionData({ + abi: simpleTransfersFactoryAbi, + functionName: "createSimpleTransfers", + args: [transferActions], + }); + + steps.push({ + id: "deploy-action", + title: "Deploy Transfer Action", + description: "Deploy the transfer action builder contract", + status: "pending", + to: SIMPLE_TRANSFERS_FACTORY, + data: deployData, + }); + + // 2. MANDATORY: Record in Registry + // For now, we use a placeholder address since we can't predict CREATE2 address client-side + // In a real implementation, this would need to be done after deployment or use CREATE2 prediction + // record(address _canonGuard, address[] _entities, string[] _labels) + const recordData = encodeFunctionData({ + abi: canonGuardRegistryAbi, + functionName: "record", + args: [ + guardAddress, // _canonGuard + ["0x0000000000000000000000000000000000000000" as Address], // _entities - placeholder array + [formData.title || "Untitled Transfer"], // _labels array + ], + }); + + steps.push({ + id: "record-registry", + title: "Save to Canon List", + description: "Save the action for future use", + status: "pending", + to: CANON_GUARD_REGISTRY, + data: recordData, + }); + + // 3. OPTIONAL: Propose Transaction (queue + sign) + if (proposeTransaction) { + // Queue the action in the guard + const queueData = encodeFunctionData({ + abi: canonGuardEntrypointAbi, + functionName: "queueTransaction", + args: [ + "0x0000000000000000000000000000000000000000" as Address, // _actionsBuilder - placeholder + ], + }); + + steps.push({ + id: "queue-action", + title: "Queue Transaction", + description: "Add the transaction to the Canon Guard queue", + status: "pending", + to: guardAddress, + data: queueData, + }); + + // Sign the transaction in the Safe + // For Safe signing, we need to call approveHash with the safeTxHash + // This requires computing the hash, which depends on the action being queued + // For now, we use a placeholder + const approveHashData = encodeFunctionData({ + abi: safeAbi, + functionName: "approveHash", + args: [ + "0x0000000000000000000000000000000000000000000000000000000000000000" as Hex, // hash - placeholder + ], + }); + + steps.push({ + id: "sign-safe-tx", + title: "Sign Transaction", + description: "Sign the transaction in your Safe wallet", + status: "pending", + to: safeAddress, + data: approveHashData, + }); + } + + // 4. OPTIONAL: Propose Pre-Approval + if (proposePreApproval) { + // Deploy the pre-approve action + // createPreApproveAction takes: _actionsBuilder, _approvalDuration + const deployPreApproveData = encodeFunctionData({ + abi: preApproveActionFactoryAbi, + functionName: "createPreApproveAction", + args: [ + "0x0000000000000000000000000000000000000000" as Address, // _actionsBuilder - placeholder (the action to approve) + preApprovalDuration, // _approvalDuration (user-provided duration) + ], + }); + + steps.push({ + id: "deploy-preapprove", + title: "Deploy Pre-Approval", + description: "Deploy the pre-approval action contract", + status: "pending", + to: PRE_APPROVE_ACTION_FACTORY, + data: deployPreApproveData, + }); + + // Queue the pre-approve action + const queuePreApproveData = encodeFunctionData({ + abi: canonGuardEntrypointAbi, + functionName: "queueTransaction", + args: [ + "0x0000000000000000000000000000000000000000" as Address, // _preApproveAction - placeholder + ], + }); + + steps.push({ + id: "queue-preapprove", + title: "Queue Pre-Approval", + description: "Add the pre-approval to the Canon Guard queue", + status: "pending", + to: guardAddress, + data: queuePreApproveData, + }); + + // Sign the pre-approve transaction + const signPreApproveData = encodeFunctionData({ + abi: safeAbi, + functionName: "approveHash", + args: [ + "0x0000000000000000000000000000000000000000000000000000000000000000" as Hex, // hash - placeholder + ], + }); + + steps.push({ + id: "sign-preapprove", + title: "Sign Pre-Approval", + description: "Sign the pre-approval transaction in your Safe wallet", + status: "pending", + to: safeAddress, + data: signPreApproveData, + }); + } + + return { steps }; +} + +// Options for building Arbitrary Action transaction steps +export interface BuildArbitraryActionStepsOptions { + formData: ArbitraryActionFormData; + safeAddress: Address; + guardAddress: Address; + proposeTransaction: boolean; + proposePreApproval: boolean; + approvalDurationSeconds?: bigint; + skipRegistry?: boolean; // Skip saving to Canon List (e.g., for WalletConnect transactions) +} + +/** + * Builds the list of transaction steps for Arbitrary Action + */ +export function buildArbitraryActionSteps(options: BuildArbitraryActionStepsOptions): TransactionStepsResult { + const { + formData, + safeAddress, + guardAddress, + proposeTransaction, + proposePreApproval, + approvalDurationSeconds, + skipRegistry, + } = options; + + const preApprovalDuration = approvalDurationSeconds ?? DEFAULT_PRE_APPROVAL_DURATION; + const steps: TransactionStep[] = []; + + // Build array of arbitrary actions from form data + // Signature is now optional - pass empty string if not provided + const arbitraryActions = formData.actions.map((action) => ({ + target: action.target as Address, + signature: action.signature || "", // Optional signature + data: (action.data || "0x") as Hex, // Full calldata including selector + value: action.value && action.value.trim() !== "" ? BigInt(action.value) : 0n, + })); + + // 1. MANDATORY: Deploy Arbitrary Actions + const deployData = encodeFunctionData({ + abi: arbitraryActionsFactoryAbi, + functionName: "createArbitraryActions", + args: [arbitraryActions], + }); + + steps.push({ + id: "deploy-arbitrary-action", + title: "Deploy Arbitrary Action", + description: "Deploy the arbitrary action builder contract", + status: "pending", + to: ARBITRARY_ACTIONS_FACTORY, + data: deployData, + }); + + // 2. OPTIONAL: Record in Registry (skipped for WalletConnect transactions) + if (!skipRegistry) { + const recordData = encodeFunctionData({ + abi: canonGuardRegistryAbi, + functionName: "record", + args: [ + guardAddress, + ["0x0000000000000000000000000000000000000000" as Address], + [formData.title || "Untitled Arbitrary Action"], + ], + }); + + steps.push({ + id: "record-registry", + title: "Save to Canon List", + description: "Save the action for future use", + status: "pending", + to: CANON_GUARD_REGISTRY, + data: recordData, + }); + } + + // 3. OPTIONAL: Propose Transaction (queue + sign) + if (proposeTransaction) { + const queueData = encodeFunctionData({ + abi: canonGuardEntrypointAbi, + functionName: "queueTransaction", + args: ["0x0000000000000000000000000000000000000000" as Address], + }); + + steps.push({ + id: "queue-action", + title: "Queue Transaction", + description: "Add the transaction to the Canon Guard queue", + status: "pending", + to: guardAddress, + data: queueData, + }); + + const approveHashData = encodeFunctionData({ + abi: safeAbi, + functionName: "approveHash", + args: ["0x0000000000000000000000000000000000000000000000000000000000000000" as Hex], + }); + + steps.push({ + id: "sign-safe-tx", + title: "Sign Transaction", + description: "Sign the transaction in your Safe wallet", + status: "pending", + to: safeAddress, + data: approveHashData, + }); + } + + // 4. OPTIONAL: Propose Pre-Approval + if (proposePreApproval) { + const deployPreApproveData = encodeFunctionData({ + abi: preApproveActionFactoryAbi, + functionName: "createPreApproveAction", + args: ["0x0000000000000000000000000000000000000000" as Address, preApprovalDuration], + }); + + steps.push({ + id: "deploy-preapprove", + title: "Deploy Pre-Approval", + description: "Deploy the pre-approval action contract", + status: "pending", + to: PRE_APPROVE_ACTION_FACTORY, + data: deployPreApproveData, + }); + + const queuePreApproveData = encodeFunctionData({ + abi: canonGuardEntrypointAbi, + functionName: "queueTransaction", + args: ["0x0000000000000000000000000000000000000000" as Address], + }); + + steps.push({ + id: "queue-preapprove", + title: "Queue Pre-Approval", + description: "Add the pre-approval to the Canon Guard queue", + status: "pending", + to: guardAddress, + data: queuePreApproveData, + }); + + const signPreApproveData = encodeFunctionData({ + abi: safeAbi, + functionName: "approveHash", + args: ["0x0000000000000000000000000000000000000000000000000000000000000000" as Hex], + }); + + steps.push({ + id: "sign-preapprove", + title: "Sign Pre-Approval", + description: "Sign the pre-approval transaction in your Safe wallet", + status: "pending", + to: safeAddress, + data: signPreApproveData, + }); + } + + return { steps }; +} + +// Options for building Claim Allowance transaction steps +export interface BuildClaimAllowanceStepsOptions { + formData: ClaimAllowanceFormData; + safeAddress: Address; + guardAddress: Address; + proposeTransaction: boolean; + proposePreApproval: boolean; + approvalDurationSeconds?: bigint; +} + +/** + * Builds the list of transaction steps for Claim Allowance + */ +export function buildClaimAllowanceSteps(options: BuildClaimAllowanceStepsOptions): TransactionStepsResult { + const { formData, safeAddress, guardAddress, proposeTransaction, proposePreApproval, approvalDurationSeconds } = + options; + + const preApprovalDuration = approvalDurationSeconds ?? DEFAULT_PRE_APPROVAL_DURATION; + const steps: TransactionStep[] = []; + + // 1. MANDATORY: Deploy Claim Allowance + const deployData = encodeFunctionData({ + abi: allowanceClaimorFactoryAbi, + functionName: "createAllowanceClaimor", + args: [formData.token as Address, formData.tokenOwner as Address, formData.tokenRecipient as Address], + }); + + steps.push({ + id: "deploy-claim-allowance", + title: "Deploy Claim Allowance", + description: "Deploy the allowance claimor action builder contract", + status: "pending", + to: ALLOWANCE_CLAIMOR_FACTORY, + data: deployData, + }); + + // 2. MANDATORY: Record in Registry + const recordData = encodeFunctionData({ + abi: canonGuardRegistryAbi, + functionName: "record", + args: [ + guardAddress, + ["0x0000000000000000000000000000000000000000" as Address], + [formData.title || "Untitled Claim Allowance"], + ], + }); + + steps.push({ + id: "record-registry", + title: "Save to Canon List", + description: "Save the action for future use", + status: "pending", + to: CANON_GUARD_REGISTRY, + data: recordData, + }); + + // 3. OPTIONAL: Propose Transaction (queue + sign) + if (proposeTransaction) { + const queueData = encodeFunctionData({ + abi: canonGuardEntrypointAbi, + functionName: "queueTransaction", + args: ["0x0000000000000000000000000000000000000000" as Address], + }); + + steps.push({ + id: "queue-action", + title: "Queue Transaction", + description: "Add the transaction to the Canon Guard queue", + status: "pending", + to: guardAddress, + data: queueData, + }); + + const approveHashData = encodeFunctionData({ + abi: safeAbi, + functionName: "approveHash", + args: ["0x0000000000000000000000000000000000000000000000000000000000000000" as Hex], + }); + + steps.push({ + id: "sign-safe-tx", + title: "Sign Transaction", + description: "Sign the transaction in your Safe wallet", + status: "pending", + to: safeAddress, + data: approveHashData, + }); + } + + // 4. OPTIONAL: Propose Pre-Approval + if (proposePreApproval) { + const deployPreApproveData = encodeFunctionData({ + abi: preApproveActionFactoryAbi, + functionName: "createPreApproveAction", + args: ["0x0000000000000000000000000000000000000000" as Address, preApprovalDuration], + }); + + steps.push({ + id: "deploy-preapprove", + title: "Deploy Pre-Approval", + description: "Deploy the pre-approval action contract", + status: "pending", + to: PRE_APPROVE_ACTION_FACTORY, + data: deployPreApproveData, + }); + + const queuePreApproveData = encodeFunctionData({ + abi: canonGuardEntrypointAbi, + functionName: "queueTransaction", + args: ["0x0000000000000000000000000000000000000000" as Address], + }); + + steps.push({ + id: "queue-preapprove", + title: "Queue Pre-Approval", + description: "Add the pre-approval to the Canon Guard queue", + status: "pending", + to: guardAddress, + data: queuePreApproveData, + }); + + const signPreApproveData = encodeFunctionData({ + abi: safeAbi, + functionName: "approveHash", + args: ["0x0000000000000000000000000000000000000000000000000000000000000000" as Hex], + }); + + steps.push({ + id: "sign-preapprove", + title: "Sign Pre-Approval", + description: "Sign the pre-approval transaction in your Safe wallet", + status: "pending", + to: safeAddress, + data: signPreApproveData, + }); + } + + return { steps }; +} + +// Options for building Capped Transfer Hub transaction steps +export interface BuildCappedTransferHubStepsOptions { + formData: CappedTransferHubFormData; + safeAddress: Address; + guardAddress: Address; + proposePreApproval: boolean; + approvalDurationSeconds?: bigint; +} + +/** + * Builds the list of transaction steps for Capped Token Transfers Hub + */ +export function buildCappedTransferHubSteps(options: BuildCappedTransferHubStepsOptions): TransactionStepsResult { + const { formData, safeAddress, guardAddress, proposePreApproval, approvalDurationSeconds } = options; + + const preApprovalDuration = approvalDurationSeconds ?? DEFAULT_PRE_APPROVAL_DURATION; + const steps: TransactionStep[] = []; + + // Calculate epoch length in seconds + const epochLengthValue = parseFloat(formData.epochLength) || 0; + const epochLengthSeconds = BigInt(Math.floor(epochLengthValue * EPOCH_TIME_MULTIPLIERS[formData.epochUnit])); + + // Prepare tokens and caps arrays + // Note: Caps are parsed with 18 decimals - this should be enhanced to detect token decimals + const tokens: Address[] = formData.tokens.map((t) => t.address as Address); + const caps: bigint[] = formData.tokens.map((t) => parseUnits(t.amount || "0", 18)); + + // 1. MANDATORY: Deploy Capped Token Transfers Hub + const deployData = encodeFunctionData({ + abi: cappedTokenTransfersHubFactoryAbi, + functionName: "createCappedTokenTransfersHub", + args: [ + safeAddress, // _safe + formData.recipientAddress as Address, // _recipient + tokens, // _tokens + caps, // _caps + epochLengthSeconds, // _epochLength + ], + }); + + steps.push({ + id: "deploy-capped-transfer-hub", + title: "Deploy Hub", + description: "Deploy the Capped Token Transfers Hub contract", + status: "pending", + to: CAPPED_TOKEN_TRANSFERS_HUB_FACTORY, + data: deployData, + }); + + // 2. MANDATORY: Record in Registry + const recordData = encodeFunctionData({ + abi: canonGuardRegistryAbi, + functionName: "record", + args: [guardAddress, ["0x0000000000000000000000000000000000000000" as Address], [formData.title || "Untitled Hub"]], + }); + + steps.push({ + id: "record-registry", + title: "Save to Canon List", + description: "Save the hub for future use", + status: "pending", + to: CANON_GUARD_REGISTRY, + data: recordData, + }); + + // 3. OPTIONAL: Propose Pre-Approval + if (proposePreApproval) { + const deployPreApproveData = encodeFunctionData({ + abi: preApproveActionFactoryAbi, + functionName: "createPreApproveAction", + args: ["0x0000000000000000000000000000000000000000" as Address, preApprovalDuration], + }); + + steps.push({ + id: "deploy-preapprove", + title: "Deploy Pre-Approval", + description: "Deploy the pre-approval action contract", + status: "pending", + to: PRE_APPROVE_ACTION_FACTORY, + data: deployPreApproveData, + }); + + const queuePreApproveData = encodeFunctionData({ + abi: canonGuardEntrypointAbi, + functionName: "queueTransaction", + args: ["0x0000000000000000000000000000000000000000" as Address], + }); + + steps.push({ + id: "queue-preapprove", + title: "Queue Pre-Approval", + description: "Add the pre-approval to the Canon Guard queue", + status: "pending", + to: guardAddress, + data: queuePreApproveData, + }); + + const signPreApproveData = encodeFunctionData({ + abi: safeAbi, + functionName: "approveHash", + args: ["0x0000000000000000000000000000000000000000000000000000000000000000" as Hex], + }); + + steps.push({ + id: "sign-preapprove", + title: "Sign Pre-Approval", + description: "Sign the pre-approval transaction in your Safe wallet", + status: "pending", + to: safeAddress, + data: signPreApproveData, + }); + } + + return { steps }; +} + +// Options for building Hub Child deployment steps +export interface BuildDeployHubChildStepsOptions { + formData: HubChildFormData; + hubAddress: Address; + safeAddress: Address; + guardAddress: Address; + proposeTransaction: boolean; +} + +/** + * Builds the list of transaction steps for deploying a Hub Child (CappedTokenTransfers) + */ +export function buildDeployHubChildSteps(options: BuildDeployHubChildStepsOptions): TransactionStepsResult { + const { formData, hubAddress, safeAddress, guardAddress, proposeTransaction } = options; + + const steps: TransactionStep[] = []; + + // Parse amount - assuming 18 decimals for now + const amount = parseUnits(formData.amount || "0", 18); + + // 1. MANDATORY: Deploy Hub Child via createNewActionsBuilder on the hub + const deployData = encodeFunctionData({ + abi: cappedTokenTransfersHubAbi, + functionName: "createNewActionsBuilder", + args: [formData.token as Address, amount], + }); + + steps.push({ + id: "deploy-hub-child", + title: "Deploy Child Action", + description: "Deploy the capped transfer action via the hub", + status: "pending", + to: hubAddress, + data: deployData, + }); + + // 2. MANDATORY: Record in Registry + const recordData = encodeFunctionData({ + abi: canonGuardRegistryAbi, + functionName: "record", + args: [ + guardAddress, + ["0x0000000000000000000000000000000000000000" as Address], + [formData.title || "Untitled Transfer"], + ], + }); + + steps.push({ + id: "record-registry", + title: "Save to Canon List", + description: "Save the action for future use", + status: "pending", + to: CANON_GUARD_REGISTRY, + data: recordData, + }); + + // 3. OPTIONAL: Propose Transaction (queue + sign) + if (proposeTransaction) { + const queueData = encodeFunctionData({ + abi: canonGuardEntrypointAbi, + functionName: "queueTransaction", + args: ["0x0000000000000000000000000000000000000000" as Address], + }); + + steps.push({ + id: "queue-action", + title: "Queue Transaction", + description: "Add the transaction to the Canon Guard queue", + status: "pending", + to: guardAddress, + data: queueData, + }); + + const approveHashData = encodeFunctionData({ + abi: safeAbi, + functionName: "approveHash", + args: ["0x0000000000000000000000000000000000000000000000000000000000000000" as Hex], + }); + + steps.push({ + id: "sign-safe-tx", + title: "Sign Transaction", + description: "Sign the transaction in your Safe wallet", + status: "pending", + to: safeAddress, + data: approveHashData, + }); + } + + return { steps }; +} + +/** + * Get human-readable summary of transaction steps + */ +export function getTransactionSummary(steps: TransactionStep[]): string { + const signed = steps.filter((s) => s.status === "signed").length; + const total = steps.length; + return `${signed}/${total} transactions signed`; +} diff --git a/src/types/canon-guard.ts b/src/types/canon-guard.ts index bb61c48..3f5d756 100644 --- a/src/types/canon-guard.ts +++ b/src/types/canon-guard.ts @@ -18,11 +18,18 @@ export enum QueuedTransactionState { * Types of action factories available in the Canon Guard system */ export enum ActionFactoryType { - SIMPLE_ACTIONS = "simple_actions", + SAFE_ENTRYPOINT = "safe_entrypoint", + ARBITRARY_ACTIONS = "arbitrary_actions", SIMPLE_TRANSFERS = "simple_transfers", CAPPED_TOKEN_TRANSFERS = "capped_token_transfers", ALLOWANCE_CLAIMOR = "allowance_claimor", APPROVE_ACTION = "approve_action", + CHANGE_SAFE_GUARD = "change_safe_guard", + SET_EMERGENCY_CALLER = "set_emergency_caller", + SET_EMERGENCY_TRIGGER = "set_emergency_trigger", + EVERCLEAR_TOKEN_CONVERSION = "everclear_token_conversion", + OPX_ACTION = "opx_action", + UNKNOWN = "unknown", } /** @@ -41,7 +48,7 @@ export interface CanonRegistry { allowanceClaimorFactory: Address; approveActionFactory: Address; cappedTokenTransfersHubFactory: Address; - simpleActionsFactory: Address; + arbitraryActionsFactory: Address; simpleTransfersFactory: Address; } @@ -55,7 +62,8 @@ export interface CanonRegistry { export interface ActionBuilder { address: Address; factoryType: ActionFactoryType; - factoryAddress: Address; + actionBuilderAddress: Address; + factoryLabel: string; createdAt: Date; isApproved: boolean; approvalExpiresAt?: Date; @@ -67,7 +75,7 @@ export interface ActionBuilder { */ export interface ActionHub { address: Address; - factoryAddress: Address; + actionBuilderAddress: Address; type: ActionFactoryType; isApproved: boolean; approvalExpiresAt?: Date; @@ -91,7 +99,7 @@ export interface QueuedTransaction { } /** - * Pre-approved action builder or hub + * Pre-approved action builder or hub - represents Safe transactions waiting for approval */ export interface PreApprovedItem { address: Address; @@ -99,7 +107,13 @@ export interface PreApprovedItem { factoryType?: ActionFactoryType; approvedAt: Date; expiresAt: Date; - approvalDuration: number; // in seconds + approvalDuration: number; + + // Safe transaction specific fields for "Waiting for Approval" column + safeTxHash?: Hash; + approversCount?: number; + requiredApprovals?: number; + approvers?: Address[]; } // ================================================================ @@ -107,40 +121,37 @@ export interface PreApprovedItem { // ================================================================ /** - * Canon Guard configuration for a specific vault + * Canon Guard configuration for a specific Safe */ export interface CanonGuardConfiguration { - vaultAddress: string; - entrypointAddress: string; - shortTxExecutionDelay: number; // in seconds - longTxExecutionDelay: number; // in seconds - txExpiryDelay: number; // in seconds - maxApprovalDuration: number; // in seconds - emergencyTriggerAddress: string; - emergencyCallerAddress: string; + safeAddress: Address; + entrypointAddress: Address; + shortTxExecutionDelay: number; + longTxExecutionDelay: number; + txExpiryDelay: number; + maxApprovalDuration: number; + emergencyTriggerAddress: Address; + emergencyCallerAddress: Address; isEmergencyMode: boolean; } -/** - * Information about a Safe vault with Canon Guard - */ -export interface VaultInfo { - address: string; +export interface SafeInfo { + address: Address; chainId: number; network: string; threshold: number; - owners: string[]; + owners: Address[]; totalOwners: number; - hasCanonGuard: boolean; - guardAddress?: string; + /** Whether the Safe has any guard attached */ + hasGuard: boolean; + /** Whether the attached guard is a valid Canon Guard (deployed from supported factory) */ + isValidCanonGuard: boolean; + guardAddress?: Address; nonce: number; } -/** - * Complete vault data including configuration and actions - */ -export interface VaultData { - vaultInfo: VaultInfo; +export interface CanonGuardData { + safeInfo: SafeInfo; configuration?: CanonGuardConfiguration; queuedTransactions: QueuedTransaction[]; preApprovedItems: PreApprovedItem[]; @@ -171,20 +182,20 @@ export interface ExecutedTransaction { export interface ActionDetails { actionBuilder: Address; target: Address; - value: string; // in wei + value: bigint; calldata: Hex; fnSignature?: string; // Function signature like "approve(address,uint256)" decodedParams?: Record; } /** - * Simple action structure for building new actions + * Arbitrary action structure for building new actions */ -export interface SimpleAction { +export interface ArbitraryAction { target: Address; - fnSignature: string; // Function signature like "transfer(address,uint256)" - data: Hex; // ABI-encoded parameters - value: string; // in wei + fnSignature?: string; // Optional function signature like "transfer(address,uint256)" + data: Hex; // Full calldata including selector + value: bigint; } /** @@ -193,7 +204,7 @@ export interface SimpleAction { export interface TransferAction { token: Address; to: Address; - amount: string; // in token units + amount: bigint; } // ================================================================ @@ -201,36 +212,88 @@ export interface TransferAction { // ================================================================ /** - * UI tab types for navigation + * UI tab types for navigation (matches Figma header design) */ export enum TabType { QUEUE = "queue", - PRE_APPROVED = "pre-approved", - HISTORY = "history", - CONFIGURATION = "configuration", - ACTIONS = "actions", + CANON_LIST = "canon-list", + CREATE = "create", } +// ================================================================ +// CANON GUARD UTILITIES TYPES +// ================================================================ + /** - * Action creation wizard steps + * Canon Guard entrypoint configuration */ -export enum ActionWizardStep { - SELECT_TYPE = "select-type", - CONFIGURE_ACTION = "configure-action", - REVIEW = "review", - DEPLOY = "deploy", - QUEUE = "queue", +export interface EntrypointConfiguration { + shortTxExecutionDelay: bigint; + longTxExecutionDelay: bigint; + txExpiryDelay: bigint; + maxApprovalDuration: bigint; } /** - * Filter options for transaction lists + * Transaction details from Canon Guard */ -export interface TransactionFilters { - state?: QueuedTransactionState[]; - factoryType?: ActionFactoryType[]; - dateRange?: { - from: Date; - to: Date; - }; - hasHub?: boolean; +export interface TransactionDetails { + actionsData: Hex; + executableAt: bigint; + expiresAt: bigint; + safeTxHash: Hash; + approvalExpiry: bigint; +} + +/** + * Batched transaction details with approvers + */ +export interface BatchedTransactionDetails { + actionsData: Hex; + executableAt: bigint; + expiresAt: bigint; + safeTxHash: Hash; + approvalExpiry: bigint; + approvers: Address[]; +} + +// ================================================================ +// HUB TYPES +// ================================================================ + +/** + * Types of hub factories available in the Canon Guard system + */ +export enum HubFactoryType { + CAPPED_TOKEN_TRANSFERS_HUB = "capped_token_transfers_hub", +} + +/** + * Token configuration for a CappedTokenTransfersHub + */ +export interface HubTokenConfig { + address: Address; + cap: bigint; + capLeft: bigint; + decimals: number; +} + +/** + * Hub information for CappedTokenTransfersHub + */ +export interface CappedTokenTransfersHubInfo { + address: Address; + recipient: Address; + epochLength: bigint; + tokens: HubTokenConfig[]; +} + +/** + * Information about a hub child entity in the Canon List + */ +export interface HubChildInfo { + parentHubAddress: Address; + token: Address; + amount: bigint; + recipient: Address; } diff --git a/src/utils/factoryDisplay.ts b/src/utils/factoryDisplay.ts new file mode 100644 index 0000000..f41d3de --- /dev/null +++ b/src/utils/factoryDisplay.ts @@ -0,0 +1,40 @@ +import { ActionFactoryType, HubFactoryType } from "~/types/canon-guard"; + +// Centralized factory display names - single source of truth +// Use proper capitalization here; CSS handles uppercase display where needed +export const FACTORY_DISPLAY_NAMES: Partial> = { + [ActionFactoryType.SIMPLE_TRANSFERS]: "Transfer", + [ActionFactoryType.CAPPED_TOKEN_TRANSFERS]: "Capped Transfer", + [ActionFactoryType.ARBITRARY_ACTIONS]: "Arbitrary Action", + [ActionFactoryType.APPROVE_ACTION]: "Pre-Approve", + [ActionFactoryType.ALLOWANCE_CLAIMOR]: "Claim Allowance", + [ActionFactoryType.CHANGE_SAFE_GUARD]: "Change Guard", + [ActionFactoryType.SET_EMERGENCY_CALLER]: "Emergency Caller", + [ActionFactoryType.SET_EMERGENCY_TRIGGER]: "Emergency Trigger", + [ActionFactoryType.EVERCLEAR_TOKEN_CONVERSION]: "Token Conversion", + [ActionFactoryType.OPX_ACTION]: "OPx Action", +}; + +// Hub factory display names - keyed by HubFactoryType +export const HUB_FACTORY_DISPLAY_NAMES: Record = { + [HubFactoryType.CAPPED_TOKEN_TRANSFERS_HUB]: "Capped Transfer", +}; + +// Hub-specific display names for UI (keyed by ActionFactoryType for legacy compatibility) +export const HUB_DISPLAY_NAMES: Partial> = { + [ActionFactoryType.CAPPED_TOKEN_TRANSFERS]: "Cap Transfer", +}; + +/** + * Get a user-friendly display name for the factory type + */ +export const getFactoryDisplayName = (factoryType: ActionFactoryType): string => { + return FACTORY_DISPLAY_NAMES[factoryType] ?? "Unknown"; +}; + +/** + * Get a user-friendly display name for a hub factory type + */ +export const getHubDisplayName = (factoryType: ActionFactoryType): string => { + return HUB_DISPLAY_NAMES[factoryType] ?? "Unknown Hub"; +}; diff --git a/src/utils/format.ts b/src/utils/format.ts index 911ea40..5f92e6f 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -5,6 +5,7 @@ export const truncateAddress = (address: string) => { export const formatTimeRemaining = (milliseconds: number) => { const hours = Math.floor(milliseconds / (1000 * 60 * 60)); const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000); if (hours >= 24) { const days = Math.floor(hours / 24); @@ -12,7 +13,15 @@ export const formatTimeRemaining = (milliseconds: number) => { return `${days}d ${remainingHours}h`; } - return `${hours}h ${minutes}m`; + if (hours > 0) { + return `${hours}h ${minutes}m ${seconds}s`; + } + + if (minutes > 0) { + return `${minutes}m ${seconds}s`; + } + + return `${seconds}s`; }; export const formatDate = (date: Date) => { diff --git a/src/utils/multicall.ts b/src/utils/multicall.ts new file mode 100644 index 0000000..e508343 --- /dev/null +++ b/src/utils/multicall.ts @@ -0,0 +1,34 @@ +import { MulticallReturnType } from "viem"; + +export function parseMulticallResults(results: MulticallReturnType): unknown[] { + if (!results) return []; + + const values = []; + for (let i = 0; i < results.length; i++) { + const result = results[i]; + if (result && result.status === "success") { + values.push(result.result); + } else { + values.push(null); + } + } + return values; +} + +export function parseMulticallResultsStrict(results: MulticallReturnType, errorContexts?: string[]): unknown[] { + if (!results) return []; + + const values = []; + for (let i = 0; i < results.length; i++) { + const result = results[i]; + if (result && result.status === "success") { + values.push(result.result); + } else if (errorContexts && errorContexts[i]) { + const error = result?.error?.message || "Unknown error"; + throw new Error(`${errorContexts[i]}: ${error}`); + } else { + values.push(null); + } + } + return values; +} diff --git a/src/utils/timeUnits.ts b/src/utils/timeUnits.ts new file mode 100644 index 0000000..7e413a9 --- /dev/null +++ b/src/utils/timeUnits.ts @@ -0,0 +1,35 @@ +/** + * Centralized time unit constants and utilities + * Used for epoch configuration and pre-approval duration settings + */ + +// Epoch time units (for hub configuration) - no seconds option +export type EpochTimeUnit = "minutes" | "hours" | "days" | "weeks" | "months"; + +export const EPOCH_TIME_MULTIPLIERS: Record = { + minutes: 60, + hours: 3600, + days: 86400, + weeks: 604800, + months: 2592000, // 30 days +}; + +// Duration time units (for pre-approval) - includes seconds +export type DurationTimeUnit = "seconds" | "minutes" | "hours" | "days" | "weeks" | "months"; + +export const DURATION_TIME_MULTIPLIERS: Record = { + seconds: 1, + minutes: 60, + hours: 3600, + days: 86400, + weeks: 604800, + months: 2592000, // 30 days +}; + +/** + * Convert a value and time unit to seconds + */ +export const toSeconds = (value: number, unit: EpochTimeUnit | DurationTimeUnit): number => { + const multipliers: Record = { ...EPOCH_TIME_MULTIPLIERS, seconds: 1 }; + return Math.floor(value * multipliers[unit]); +}; diff --git a/src/utils/validation.ts b/src/utils/validation.ts new file mode 100644 index 0000000..c9ea04b --- /dev/null +++ b/src/utils/validation.ts @@ -0,0 +1,66 @@ +import { isAddress, isHex } from "viem"; + +/** + * Shared validation utilities for form fields. + * Used across Transfer, Arbitrary Action, and Hub form components. + */ + +/** + * Validates an Ethereum address. + * Returns true for empty strings (incomplete but not invalid). + */ +export const isValidAddress = (value: string): boolean => { + if (!value.trim()) return true; // Empty is not invalid (just incomplete) + return isAddress(value); +}; + +/** + * Validates a positive numeric amount. + * Returns true for empty strings (incomplete but not invalid). + */ +export const isValidAmount = (value: string): boolean => { + if (!value.trim()) return true; // Empty is not invalid (just incomplete) + const num = parseFloat(value); + return !isNaN(num) && num >= 0 && /^[0-9]*\.?[0-9]*$/.test(value); +}; + +/** + * Validates a positive numeric amount (strictly greater than zero). + * Returns true for empty strings (incomplete but not invalid). + */ +export const isValidPositiveAmount = (value: string): boolean => { + if (!value.trim()) return true; // Empty is not invalid (just incomplete) + const num = parseFloat(value); + return !isNaN(num) && num > 0 && /^[0-9]*\.?[0-9]*$/.test(value); +}; + +/** + * Validates a Solidity function signature format. + * Example: "transfer(address,uint256)" + * Returns true for empty strings (incomplete but not invalid). + */ +export const isValidSignature = (value: string): boolean => { + if (!value.trim()) return true; // Empty is not invalid + // Basic validation: should be a function signature like "transfer(address,uint256)" + return /^[a-zA-Z_][a-zA-Z0-9_]*\([^)]*\)$/.test(value); +}; + +/** + * Validates hex-encoded data. + * Returns true for empty strings or "0x" (empty bytes). + */ +export const isValidHexData = (value: string): boolean => { + if (!value.trim()) return true; // Empty is not invalid + if (value === "0x") return true; // Empty bytes + return isHex(value); +}; + +/** + * Validates a numeric value (can be zero or positive). + * Returns true for empty strings (incomplete but not invalid). + */ +export const isValidValue = (value: string): boolean => { + if (!value.trim()) return true; // Empty is not invalid + const num = parseFloat(value); + return !isNaN(num) && num >= 0 && /^[0-9]*\.?[0-9]*$/.test(value); +}; diff --git a/tests/safe-info-sidebar.spec.ts b/tests/safe-info-sidebar.spec.ts index 4d8101f..e366032 100644 --- a/tests/safe-info-sidebar.spec.ts +++ b/tests/safe-info-sidebar.spec.ts @@ -16,21 +16,22 @@ async function fillVaultSetup(page: Page, vaultAddress: string) { test.describe("Safe Validation", () => { test("non-Safe contract shows error", async ({ page }) => { await fillVaultSetup(page, USDC_OPTIMISM); - await expect(page.getByText("Failed to load vault data, please try again.")).toBeVisible(); + await expect(page.getByText("Unable to load data from the provided address and RPC endpoint.")).toBeVisible({}); }); test("Safe without guard shows error", async ({ page }) => { await fillVaultSetup(page, DEMO_SAFE_NO_GUARD); - await expect(page.getByText("This address is not a Canon Vault, please set it up and try again.")).toBeVisible(); + await expect(page.getByText("This Safe address does not have Canon Guard configured.")).toBeVisible({}); }); test("Safe with guard loads successfully", async ({ page }) => { await fillVaultSetup(page, DEMO_SAFE_WITH_GUARD); - await expect(page.getByText("Failed to load vault data")).not.toBeVisible(); - await expect(page.getByText("This address is not a Canon Vault")).not.toBeVisible(); + await expect(page.getByText("Connection Failed")).not.toBeVisible(); + await expect(page.getByText("Canon Guard Not Found")).not.toBeVisible(); - await expect(page.getByText("Queued Actions")).toBeVisible(); + // Use more specific selector to avoid ambiguity + await expect(page.getByRole("heading", { name: "Queued Actions" })).toBeVisible(); }); }); @@ -50,7 +51,7 @@ test.describe("Safe Sidebar Features", () => { await expect(threshold).toBeVisible(); await expect(network).toBeVisible(); - await expect(threshold).toHaveText("1/1"); + await expect(threshold).toHaveText("2/3"); await expect(network).toContainText("OP Mainnet"); }); @@ -64,6 +65,6 @@ test.describe("Safe Sidebar Features", () => { await page.getByTestId("safe-more-options").click(); await page.getByText("Clear Vault Configuration").click(); - await expect(page.getByText("Setup Canon Vault")).toBeVisible(); + await expect(page.getByText("Connect Safe")).toBeVisible(); }); }); diff --git a/tests/url-params.spec.ts b/tests/url-params.spec.ts new file mode 100644 index 0000000..69b5384 --- /dev/null +++ b/tests/url-params.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from "@playwright/test"; +import { DEMO_SAFE_WITH_GUARD, OPTIMISM_MAINNET_RPC } from "../src/constants/addresses"; + +test.describe("URL Parameters", () => { + test("automatically loads vault when safeAddress and rpcUrl are in URL", async ({ page }) => { + const encodedRpcUrl = encodeURIComponent(OPTIMISM_MAINNET_RPC); + const urlWithParams = `/?safeAddress=${DEMO_SAFE_WITH_GUARD}&rpcUrl=${encodedRpcUrl}`; + + await page.goto(urlWithParams); + + await expect(page.getByText("Connect Safe")).not.toBeVisible(); + await page.waitForLoadState("networkidle", { timeout: 15000 }); + + await expect(page.getByText("Queue Management")).toBeVisible({ timeout: 15000 }); + }); + + test("shows setup modal when URL params are missing", async ({ page }) => { + await page.goto("/"); + + await expect(page.getByText("Connect Safe")).toBeVisible(); + await expect(page.getByTestId("safe-address-input")).toBeVisible(); + await expect(page.getByTestId("chain-select")).toBeVisible(); + }); + + test("shows setup modal when safeAddress is invalid", async ({ page }) => { + const encodedRpcUrl = encodeURIComponent(OPTIMISM_MAINNET_RPC); + const urlWithInvalidAddress = `/?safeAddress=invalid-address&rpcUrl=${encodedRpcUrl}`; + + await page.goto(urlWithInvalidAddress); + + await expect(page.getByText("Connect Safe")).toBeVisible(); + }); + + test("shows setup modal when chainId is missing", async ({ page }) => { + const urlWithMissingChain = `/?safeAddress=${DEMO_SAFE_WITH_GUARD}`; + + await page.goto(urlWithMissingChain); + + await expect(page.getByText("Connect Safe")).toBeVisible(); + }); + + test("shows setup modal when chainId is invalid", async ({ page }) => { + const urlWithInvalidChain = `/?safeAddress=${DEMO_SAFE_WITH_GUARD}&chainId=99999`; + + await page.goto(urlWithInvalidChain); + + await expect(page.getByText("Connect Safe")).toBeVisible(); + }); +}); diff --git a/vercel.json b/vercel.json index 541ca25..2338ebb 100644 --- a/vercel.json +++ b/vercel.json @@ -1,4 +1,5 @@ { + "installCommand": "git config --global url.\"https://github.com/\".insteadOf git@github.com: && pnpm install", "rewrites": [{ "source": "/(.*)", "destination": "/" }], "github": { "enabled": false