From 5281ba97ac6c0b3df361f99dfdc347b42c7febe2 Mon Sep 17 00:00:00 2001 From: Sergey Zhuravlev Date: Tue, 14 Jan 2025 15:00:48 +0100 Subject: [PATCH] refactor: wallet connect pairing and signing (#2956) --- .eslintrc.cjs | 2 + package.json | 12 +- pnpm-lock.yaml | 630 +++++++++++++----- .../domains/network/accounts/model.test.ts | 4 +- .../domains/network/accounts/model.ts | 44 +- .../multisig/model/multisigs-model.ts | 2 +- .../entities/wallet/model/wallet-model.ts | 2 + .../entities/wallet/ui/Cards/WalletCardLg.tsx | 36 +- .../entities/wallet/ui/Cards/WalletCardMd.tsx | 14 +- .../MultiAccountsList/MultiAccountsList.tsx | 7 +- src/renderer/entities/walletConnect/index.ts | 4 - .../walletConnect/lib/__tests__/utils.test.ts | 49 -- .../entities/walletConnect/lib/types.ts | 12 - .../entities/walletConnect/lib/utils.ts | 33 - .../model/wallet-connect-model.ts | 405 ----------- .../OperationSign/model/sign-wc-model.ts | 201 ------ .../OperationSign/model/walletConnectSign.ts | 205 ++++++ .../OperationSign/ui/WalletConnect.tsx | 227 ++----- .../features/sign-wallet-connect/index.tsx | 0 .../features/wallet-details/lib/constants.ts | 2 - .../features/wallet-details/lib/utils.ts | 12 +- .../__tests__/vault-details-model.test.ts | 2 +- .../model/walletConnectForgot.ts | 76 +++ .../model/walletConnectReconnect.ts | 99 +++ .../wallet-details/model/wc-details-model.ts | 198 ------ .../ui/components/WalletConnectAccounts.tsx | 23 +- .../ui/wallets/WalletConnectDetails.tsx | 69 +- .../features/wallet-multisig/index.tsx | 19 +- .../components/ManageStep.tsx | 101 +-- .../components/PairingModal.tsx | 39 +- .../components/WalletConnectQrCode.tsx | 9 +- .../lib/constants.ts | 4 +- .../model/pairingForm.ts | 101 ++- .../features/wallet-polkadot-vault/index.tsx | 10 +- .../features/wallet-proxied/index.tsx | 12 +- .../wallet-select/components/WalletSelect.tsx | 18 +- src/renderer/features/wallet-select/index.ts | 4 +- .../service/walletSelectService.ts | 4 +- .../components/WalletGroup.tsx | 57 +- .../components/WalletIcon.tsx | 29 + .../features/wallet-wallet-connect/index.tsx | 38 +- .../wallet-wallet-connect}/lib/constants.ts | 4 +- .../wallet-wallet-connect/lib/service.test.ts | 22 + .../wallet-wallet-connect/lib/service.ts | 137 ++++ .../wallet-wallet-connect/lib/types.ts | 13 + .../wallet-wallet-connect/model/connect.ts | 168 +++++ .../wallet-wallet-connect/model/feature.ts | 7 + .../wallet-wallet-connect/model/signClient.ts | 75 +++ .../wallet-wallet-connect/model/wallets.ts | 2 +- .../features/wallet-watch-only/index.tsx | 10 +- .../__tests__/forget-wallet-model.test.ts | 4 +- .../__tests__/rename-wallet-model.test.ts | 4 +- .../RenameWallet/model/rename-wallet-model.ts | 2 +- src/renderer/shared/core/types/account.ts | 5 +- src/renderer/shared/core/types/wallet.ts | 2 - src/renderer/shared/di/createSlot.tsx | 4 +- src/renderer/shared/di/index.ts | 4 +- .../effector/{helpers => }/createBuffer.ts | 0 src/renderer/shared/effector/index.ts | 5 +- .../effector/{helpers => }/series.test.ts | 0 .../shared/effector/{helpers => }/series.ts | 16 +- src/renderer/shared/effector/waitFor.test.ts | 58 ++ src/renderer/shared/effector/waitFor.ts | 58 ++ src/renderer/shared/feature/createFeature.ts | 2 +- .../shared/feature/registerFeatures.ts | 2 +- src/renderer/shared/lib/utils/functions.ts | 13 + src/renderer/shared/mocks/index.ts | 1 - vite.config.renderer.ts | 42 +- vitest.config.ts | 14 +- 69 files changed, 1899 insertions(+), 1590 deletions(-) delete mode 100644 src/renderer/entities/walletConnect/index.ts delete mode 100644 src/renderer/entities/walletConnect/lib/__tests__/utils.test.ts delete mode 100644 src/renderer/entities/walletConnect/lib/types.ts delete mode 100644 src/renderer/entities/walletConnect/lib/utils.ts delete mode 100644 src/renderer/entities/walletConnect/model/wallet-connect-model.ts delete mode 100644 src/renderer/features/operations/OperationSign/model/sign-wc-model.ts create mode 100644 src/renderer/features/operations/OperationSign/model/walletConnectSign.ts create mode 100644 src/renderer/features/sign-wallet-connect/index.tsx create mode 100644 src/renderer/features/wallet-details/model/walletConnectForgot.ts create mode 100644 src/renderer/features/wallet-details/model/walletConnectReconnect.ts delete mode 100644 src/renderer/features/wallet-details/model/wc-details-model.ts create mode 100644 src/renderer/features/wallet-wallet-connect/components/WalletIcon.tsx rename src/renderer/{entities/walletConnect => features/wallet-wallet-connect}/lib/constants.ts (92%) create mode 100644 src/renderer/features/wallet-wallet-connect/lib/service.test.ts create mode 100644 src/renderer/features/wallet-wallet-connect/lib/service.ts create mode 100644 src/renderer/features/wallet-wallet-connect/lib/types.ts create mode 100644 src/renderer/features/wallet-wallet-connect/model/connect.ts create mode 100644 src/renderer/features/wallet-wallet-connect/model/feature.ts create mode 100644 src/renderer/features/wallet-wallet-connect/model/signClient.ts rename src/renderer/shared/effector/{helpers => }/createBuffer.ts (100%) rename src/renderer/shared/effector/{helpers => }/series.test.ts (100%) rename src/renderer/shared/effector/{helpers => }/series.ts (50%) create mode 100644 src/renderer/shared/effector/waitFor.test.ts create mode 100644 src/renderer/shared/effector/waitFor.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 0f780c161b..1312f8a024 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -251,6 +251,8 @@ module.exports = { 'effector/enforce-effect-naming-convention': 'off', // Took around 4 seconds to check this single rule 'effector/enforce-store-naming-convention': 'off', + // Makes no sense since we're replacing effect handlers while testing + 'effector/strict-effect-handlers': 'off', // Boundaries setup 'boundaries/entry-point': [ diff --git a/package.json b/package.json index c6efccdb4e..86929c2e8c 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,8 @@ "main:staging": "vite build --mode staging -c vite.config.main.ts", "main:prod": "vite build -c vite.config.main.ts", "build": "cross-env CHAINS_FILE=chains TOKENS_FILE=tokens pnpm r clean:prod main:prod preload:prod renderer:prod", - "build:dev": "cross-env CHAINS_FILE=chains-dev TOKENS_FILE=tokens-dev pnpm r clean:prod main:dev preload:dev renderer:dev", - "build:staging": "cross-env CHAINS_FILE=chains TOKENS_FILE=tokens pnpm r clean:prod renderer:staging preload:staging main:staging", + "build:dev": "cross-env CHAINS_FILE=chains-dev TOKENS_FILE=tokens-dev pnpm r clean:prod clean:build main:dev preload:dev renderer:dev", + "build:staging": "cross-env CHAINS_FILE=chains TOKENS_FILE=tokens pnpm r clean:prod clean:build renderer:staging preload:staging main:staging", "postbuild": "node scripts/postbuild.js", "postbuild:staging": "node scripts/postbuild.js staging", "dist": "cross-env NODE_ENV=production electron-builder --config electron-builder.js -p never", @@ -96,9 +96,10 @@ "@substrate/connect": "2.1.1", "@substrate/txwrapper-orml": "7.5.3", "@substrate/txwrapper-polkadot": "7.5.3", - "@walletconnect/types": "2.17.2", - "@walletconnect/universal-provider": "^2.17.2", - "@walletconnect/utils": "2.17.2", + "@walletconnect/core": "2.17.3", + "@walletconnect/sign-client": "2.17.3", + "@walletconnect/types": "2.17.3", + "@walletconnect/utils": "2.17.3", "@zxing/browser": "0.1.5", "@zxing/library": "0.21.3", "bignumber.js": "9.1.2", @@ -220,6 +221,7 @@ "prettier-plugin-tailwindcss": "0.6.8", "pretty-quick": "4.0.0", "react-refresh": "0.14.2", + "react-scan": "0.0.54", "rimraf": "^3.0.2", "source-map-support": "^0.5.21", "storybook": "8.4.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 01b1c2f589..cc7c134ed4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,15 +86,18 @@ importers: '@substrate/txwrapper-polkadot': specifier: 7.5.3 version: 7.5.3(@polkadot/util-crypto@13.3.1(@polkadot/util@13.3.1))(@polkadot/util@13.3.1) + '@walletconnect/core': + specifier: 2.17.3 + version: 2.17.3 + '@walletconnect/sign-client': + specifier: 2.17.3 + version: 2.17.3 '@walletconnect/types': - specifier: 2.17.2 - version: 2.17.2 - '@walletconnect/universal-provider': - specifier: ^2.17.2 - version: 2.17.3(encoding@0.1.13) + specifier: 2.17.3 + version: 2.17.3 '@walletconnect/utils': - specifier: 2.17.2 - version: 2.17.2 + specifier: 2.17.3 + version: 2.17.3 '@zxing/browser': specifier: 0.1.5 version: 0.1.5(@zxing/library@0.21.3) @@ -245,7 +248,7 @@ importers: version: 2.3.1 '@peterek/vite-plugin-favicons': specifier: 2.0.0 - version: 2.0.0(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1)) + version: 2.0.0(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1)) '@playwright/test': specifier: ^1.49.1 version: 1.49.1 @@ -266,7 +269,7 @@ importers: version: 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.3.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(prettier@3.3.3))(typescript@5.7.3) '@storybook/react-vite': specifier: ^8.4.7 - version: 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.3.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.30.0)(storybook@8.4.7(prettier@3.3.3))(typescript@5.7.3)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1)) + version: 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.3.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.30.0)(storybook@8.4.7(prettier@3.3.3))(typescript@5.7.3)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1)) '@storybook/test': specifier: ^8.4.7 version: 8.4.7(storybook@8.4.7(prettier@3.3.3)) @@ -314,7 +317,7 @@ importers: version: 8.10.0(eslint@8.57.1)(typescript@5.7.3) '@vitejs/plugin-react-swc': specifier: 3.7.2 - version: 3.7.2(@swc/helpers@0.5.15)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1)) + version: 3.7.2(@swc/helpers@0.5.15)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1)) '@vitest/coverage-v8': specifier: 2.1.8 version: 2.1.8(vitest@2.1.8) @@ -453,6 +456,9 @@ importers: react-refresh: specifier: 0.14.2 version: 0.14.2 + react-scan: + specifier: 0.0.54 + version: 0.0.54(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-router@6.26.2(react@18.3.1))(react@18.3.1)(rollup@4.30.0) rimraf: specifier: ^3.0.2 version: 3.0.2 @@ -479,25 +485,25 @@ importers: version: 5.7.3 vite: specifier: 6.0.7 - version: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1) + version: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1) vite-plugin-compression2: specifier: 1.3.3 - version: 1.3.3(rollup@4.30.0)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1)) + version: 1.3.3(rollup@4.30.0)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1)) vite-plugin-mkcert: specifier: 1.17.6 - version: 1.17.6(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1)) + version: 1.17.6(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1)) vite-plugin-node-polyfills: specifier: 0.22.0 - version: 0.22.0(rollup@4.30.0)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1)) + version: 0.22.0(rollup@4.30.0)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1)) vite-plugin-svgr: specifier: 4.3.0 - version: 4.3.0(rollup@4.30.0)(typescript@5.7.3)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1)) + version: 4.3.0(rollup@4.30.0)(typescript@5.7.3)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1)) vite-plugin-target: specifier: 0.1.1 version: 0.1.1 vite-tsconfig-paths: specifier: 5.1.4 - version: 5.1.4(typescript@5.7.3)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1)) + version: 5.1.4(typescript@5.7.3)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1)) vitest: specifier: 2.1.8 version: 2.1.8(@types/node@20.17.12)(@vitest/ui@2.1.8)(happy-dom@16.3.0)(jsdom@20.0.3)(terser@5.37.0) @@ -566,6 +572,10 @@ packages: resolution: {integrity: sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==} engines: {node: '>=6.9.0'} + '@babel/core@7.26.0': + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} + engines: {node: '>=6.9.0'} + '@babel/generator@7.26.3': resolution: {integrity: sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==} engines: {node: '>=6.9.0'} @@ -624,6 +634,12 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@clack/core@0.3.5': + resolution: {integrity: sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==} + + '@clack/prompts@0.8.2': + resolution: {integrity: sha512-6b9Ab2UiZwJYA9iMyboYyW9yJvAO9V753ZhS+DHKEjZRKAxPPOb7MXXu84lsPFG+vZt6FRFniZ8rXi+zCIw4yQ==} + '@commitlint/cli@17.8.1': resolution: {integrity: sha512-ay+WbzQesE0Rv4EQKfNbSMiJJ12KdKTDzIt0tcK4k11FdsWmtwP0Kp1NWMOUswfIWo6Eb7p7Ln721Nx9FLNBjg==} engines: {node: '>=v14'} @@ -749,6 +765,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.23.1': + resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.24.2': resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} engines: {node: '>=18'} @@ -761,6 +783,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.23.1': + resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.24.2': resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} engines: {node: '>=18'} @@ -773,6 +801,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.23.1': + resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.24.2': resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} engines: {node: '>=18'} @@ -785,6 +819,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.23.1': + resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.24.2': resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} engines: {node: '>=18'} @@ -797,6 +837,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.23.1': + resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.24.2': resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} engines: {node: '>=18'} @@ -809,6 +855,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.23.1': + resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.24.2': resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} engines: {node: '>=18'} @@ -821,6 +873,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.23.1': + resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.24.2': resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} engines: {node: '>=18'} @@ -833,6 +891,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.23.1': + resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.24.2': resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} engines: {node: '>=18'} @@ -845,6 +909,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.23.1': + resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.24.2': resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} engines: {node: '>=18'} @@ -857,6 +927,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.23.1': + resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.24.2': resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} engines: {node: '>=18'} @@ -869,6 +945,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.23.1': + resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.24.2': resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} engines: {node: '>=18'} @@ -881,6 +963,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.23.1': + resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.24.2': resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} engines: {node: '>=18'} @@ -893,6 +981,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.23.1': + resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.24.2': resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} engines: {node: '>=18'} @@ -905,6 +999,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.23.1': + resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.24.2': resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} engines: {node: '>=18'} @@ -917,6 +1017,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.23.1': + resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.24.2': resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} engines: {node: '>=18'} @@ -929,6 +1035,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.23.1': + resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.24.2': resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} engines: {node: '>=18'} @@ -941,6 +1053,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.23.1': + resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.24.2': resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} engines: {node: '>=18'} @@ -959,12 +1077,24 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.23.1': + resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.24.2': resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/openbsd-arm64@0.23.1': + resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-arm64@0.24.2': resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} engines: {node: '>=18'} @@ -977,6 +1107,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.23.1': + resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.24.2': resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} engines: {node: '>=18'} @@ -989,6 +1125,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.23.1': + resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.24.2': resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} engines: {node: '>=18'} @@ -1001,6 +1143,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.23.1': + resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.24.2': resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} engines: {node: '>=18'} @@ -1013,6 +1161,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.23.1': + resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.24.2': resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} engines: {node: '>=18'} @@ -1025,6 +1179,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.23.1': + resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.24.2': resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} engines: {node: '>=18'} @@ -1692,6 +1852,14 @@ packages: resolution: {integrity: sha512-ytqkC7FwVs4BlzNFAmPMFp+xD1KIdMMP/mvCSOrnxjlsyM5DVGop4x4c2ZgDUBmrFqmIiVkWDfMIZeOxui2OLQ==} engines: {node: '>=18'} + '@preact/signals-core@1.8.0': + resolution: {integrity: sha512-OBvUsRZqNmjzCZXWLxkZfhcgT+Fk8DDcT/8vD6a1xhDemodyy87UJRJfASMuSD8FaAIeGgGm85ydXhm7lr4fyA==} + + '@preact/signals@1.3.2': + resolution: {integrity: sha512-naxcJgUJ6BTOROJ7C3QML7KvwKwCXQJYTc5L/b0eEsdYgPB6SxwoQ1vDGcS0Q7GVjAenVq/tXrybVdFShHYZWg==} + peerDependencies: + preact: 10.x + '@radix-ui/number@1.1.0': resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} @@ -3099,9 +3267,6 @@ packages: '@walletconnect/heartbeat@1.2.2': resolution: {integrity: sha512-uASiRmC5MwhuRuf05vq4AT48Pq8RMi876zV8rr8cV969uTOzWdB/k+Lj5yI2PBtB1bGQisGen7MM1GcZlQTBXw==} - '@walletconnect/jsonrpc-http-connection@1.0.8': - resolution: {integrity: sha512-+B7cRuaxijLeFDJUq5hAzNyef3e3tBDIxyaCNmFtjwnod5AGis3RToNqzFU33vpVcxFhofkpE7Cx+5MYejbMGw==} - '@walletconnect/jsonrpc-provider@1.0.14': resolution: {integrity: sha512-rtsNY1XqHvWj0EtITNeuf8PHMvlCLiS3EjQL+WOkxEOA4KPxsohFnBDeyPYiNm4ZvkQdLnece36opYidmtbmow==} @@ -3140,18 +3305,9 @@ packages: '@walletconnect/time@1.0.2': resolution: {integrity: sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==} - '@walletconnect/types@2.17.2': - resolution: {integrity: sha512-j/+0WuO00lR8ntu7b1+MKe/r59hNwYLFzW0tTmozzhfAlDL+dYwWasDBNq4AH8NbVd7vlPCQWmncH7/6FVtOfQ==} - '@walletconnect/types@2.17.3': resolution: {integrity: sha512-5eFxnbZGJJx0IQyCS99qz+OvozpLJJYfVG96dEHGgbzZMd+C9V1eitYqVClx26uX6V+WQVqVwjpD2Dyzie++Wg==} - '@walletconnect/universal-provider@2.17.3': - resolution: {integrity: sha512-Aen8h+vWTN57sv792i96vaTpN06WnpFUWhACY5gHrpL2XgRKmoXUgW7793p252QdgyofNAOol7wJEs1gX8FjgQ==} - - '@walletconnect/utils@2.17.2': - resolution: {integrity: sha512-T7eLRiuw96fgwUy2A5NZB5Eu87ukX8RCVoO9lji34RFV4o2IGU9FhTEWyd4QQKI8OuQRjSknhbJs0tU0r0faPw==} - '@walletconnect/utils@2.17.3': resolution: {integrity: sha512-tG77UpZNeLYgeOwViwWnifpyBatkPlpKSSayhN0gcjY1lZAUNqtYslpm4AdTxlrA3pL61MnyybXgWYT5eZjarw==} @@ -3485,6 +3641,9 @@ packages: binary-searching@2.0.5: resolution: {integrity: sha512-v4N2l3RxL+m4zDxyxz3Ne2aTmiPn8ZUpKFpdPtO+ItW1NcTCXA7JeHG5GMBSvoKSkQZ9ycS+EouDVxYB9ufKWA==} + bippy@0.0.25: + resolution: {integrity: sha512-+rvlmS7vbv704MjmpMLaSNKezGkc7xux7/DbhTp61RFQZAYwH8V0pbxGYiDWxA9a+7RxNFhHtsSIu9uoB+eK0Q==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -4355,9 +4514,6 @@ packages: elliptic@6.5.4: resolution: {integrity: sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==} - elliptic@6.6.0: - resolution: {integrity: sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==} - elliptic@6.6.1: resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} @@ -4452,6 +4608,11 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.23.1: + resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + engines: {node: '>=18'} + hasBin: true + esbuild@0.24.2: resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} engines: {node: '>=18'} @@ -5593,6 +5754,10 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + knip@5.33.3: resolution: {integrity: sha512-saUxedVDCa/8p3w445at66vLmYKretzYsX7+elMJ5ROWGzU+1aTRm3EmKELTaho1ue7BlwJB5BxLJROy43+LtQ==} engines: {node: '>=18.6.0'} @@ -6807,6 +6972,9 @@ packages: resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} engines: {node: ^10 || ^12 || >=14} + preact@10.25.4: + resolution: {integrity: sha512-jLdZDb+Q+odkHJ+MpW/9U5cODzqnB+fy2EiHSZES7ldV5LK7yjlVzTp7R8Xy6W6y75kfK8iWYtFVH7lvjwrCMA==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -7111,6 +7279,26 @@ packages: peerDependencies: react: '>=16.8' + react-scan@0.0.54: + resolution: {integrity: sha512-3ydazntrl7M4JUoCq75hoKYiZv1vBIuA8cnsRVj8DVv4GXYFSbO0R759T1pwIhcf5DBj9Ik8GPnQ2NHcWJ7TUA==} + hasBin: true + peerDependencies: + '@remix-run/react': '>=1.0.0' + next: '>=13.0.0' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-router: ^5.0.0 || ^6.0.0 || ^7.0.0 + react-router-dom: ^5.0.0 || ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + '@remix-run/react': + optional: true + next: + optional: true + react-router: + optional: true + react-router-dom: + optional: true + react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -7457,6 +7645,9 @@ packages: resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==} engines: {node: '>=18'} + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -7945,6 +8136,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.19.2: + resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} + engines: {node: '>=18.0.0'} + hasBin: true + tty-browserify@0.0.1: resolution: {integrity: sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==} @@ -8079,6 +8275,10 @@ packages: resolution: {integrity: sha512-5liCNPuJW8dqh3+DM6uNM2EI3MLLpCKp/KY+9pB5M2S2SR2qvvDHhKgBOaTWEbZTAws3CXfB0rKTIolWKL05VQ==} engines: {node: '>=14.0.0'} + unplugin@2.1.0: + resolution: {integrity: sha512-us4j03/499KhbGP8BU7Hrzrgseo+KdfJYWcbcajCOqsAyb8Gk0Yn2kiUIcZISYCb1JFaZfIuG3b42HmguVOKCQ==} + engines: {node: '>=18.12.0'} + unstorage@1.14.4: resolution: {integrity: sha512-1SYeamwuYeQJtJ/USE1x4l17LkmQBzg7deBJ+U9qOBoHo15d1cDxG4jM31zKRgF7pG0kirZy4wVMX6WL6Zoscg==} peerDependencies: @@ -8667,6 +8867,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/core@7.26.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.3 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.3 + '@babel/template': 7.25.9 + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + convert-source-map: 2.0.0 + debug: 4.4.0 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/generator@7.26.3': dependencies: '@babel/parser': 7.26.3 @@ -8699,6 +8919,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + '@babel/helper-string-parser@7.25.9': {} '@babel/helper-validator-identifier@7.25.9': {} @@ -8743,6 +8972,17 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} + '@clack/core@0.3.5': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@clack/prompts@0.8.2': + dependencies: + '@clack/core': 0.3.5 + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@commitlint/cli@17.8.1(@swc/core@1.9.3(@swc/helpers@0.5.15))': dependencies: '@commitlint/format': 17.8.1 @@ -8957,102 +9197,153 @@ snapshots: '@esbuild/aix-ppc64@0.21.5': optional: true + '@esbuild/aix-ppc64@0.23.1': + optional: true + '@esbuild/aix-ppc64@0.24.2': optional: true '@esbuild/android-arm64@0.21.5': optional: true + '@esbuild/android-arm64@0.23.1': + optional: true + '@esbuild/android-arm64@0.24.2': optional: true '@esbuild/android-arm@0.21.5': optional: true + '@esbuild/android-arm@0.23.1': + optional: true + '@esbuild/android-arm@0.24.2': optional: true '@esbuild/android-x64@0.21.5': optional: true + '@esbuild/android-x64@0.23.1': + optional: true + '@esbuild/android-x64@0.24.2': optional: true '@esbuild/darwin-arm64@0.21.5': optional: true + '@esbuild/darwin-arm64@0.23.1': + optional: true + '@esbuild/darwin-arm64@0.24.2': optional: true '@esbuild/darwin-x64@0.21.5': optional: true + '@esbuild/darwin-x64@0.23.1': + optional: true + '@esbuild/darwin-x64@0.24.2': optional: true '@esbuild/freebsd-arm64@0.21.5': optional: true + '@esbuild/freebsd-arm64@0.23.1': + optional: true + '@esbuild/freebsd-arm64@0.24.2': optional: true '@esbuild/freebsd-x64@0.21.5': optional: true + '@esbuild/freebsd-x64@0.23.1': + optional: true + '@esbuild/freebsd-x64@0.24.2': optional: true '@esbuild/linux-arm64@0.21.5': optional: true + '@esbuild/linux-arm64@0.23.1': + optional: true + '@esbuild/linux-arm64@0.24.2': optional: true '@esbuild/linux-arm@0.21.5': optional: true + '@esbuild/linux-arm@0.23.1': + optional: true + '@esbuild/linux-arm@0.24.2': optional: true '@esbuild/linux-ia32@0.21.5': optional: true + '@esbuild/linux-ia32@0.23.1': + optional: true + '@esbuild/linux-ia32@0.24.2': optional: true '@esbuild/linux-loong64@0.21.5': optional: true + '@esbuild/linux-loong64@0.23.1': + optional: true + '@esbuild/linux-loong64@0.24.2': optional: true '@esbuild/linux-mips64el@0.21.5': optional: true + '@esbuild/linux-mips64el@0.23.1': + optional: true + '@esbuild/linux-mips64el@0.24.2': optional: true '@esbuild/linux-ppc64@0.21.5': optional: true + '@esbuild/linux-ppc64@0.23.1': + optional: true + '@esbuild/linux-ppc64@0.24.2': optional: true '@esbuild/linux-riscv64@0.21.5': optional: true + '@esbuild/linux-riscv64@0.23.1': + optional: true + '@esbuild/linux-riscv64@0.24.2': optional: true '@esbuild/linux-s390x@0.21.5': optional: true + '@esbuild/linux-s390x@0.23.1': + optional: true + '@esbuild/linux-s390x@0.24.2': optional: true '@esbuild/linux-x64@0.21.5': optional: true + '@esbuild/linux-x64@0.23.1': + optional: true + '@esbuild/linux-x64@0.24.2': optional: true @@ -9062,39 +9353,60 @@ snapshots: '@esbuild/netbsd-x64@0.21.5': optional: true + '@esbuild/netbsd-x64@0.23.1': + optional: true + '@esbuild/netbsd-x64@0.24.2': optional: true + '@esbuild/openbsd-arm64@0.23.1': + optional: true + '@esbuild/openbsd-arm64@0.24.2': optional: true '@esbuild/openbsd-x64@0.21.5': optional: true + '@esbuild/openbsd-x64@0.23.1': + optional: true + '@esbuild/openbsd-x64@0.24.2': optional: true '@esbuild/sunos-x64@0.21.5': optional: true + '@esbuild/sunos-x64@0.23.1': + optional: true + '@esbuild/sunos-x64@0.24.2': optional: true '@esbuild/win32-arm64@0.21.5': optional: true + '@esbuild/win32-arm64@0.23.1': + optional: true + '@esbuild/win32-arm64@0.24.2': optional: true '@esbuild/win32-ia32@0.21.5': optional: true + '@esbuild/win32-ia32@0.23.1': + optional: true + '@esbuild/win32-ia32@0.24.2': optional: true '@esbuild/win32-x64@0.21.5': optional: true + '@esbuild/win32-x64@0.23.1': + optional: true + '@esbuild/win32-x64@0.24.2': optional: true @@ -9409,11 +9721,11 @@ snapshots: '@jonasgeiler/tsc-files@2.3.1': {} - '@joshwooding/vite-plugin-react-docgen-typescript@0.4.2(typescript@5.7.3)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.4.2(typescript@5.7.3)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1))': dependencies: magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.7.3) - vite: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1) + vite: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1) optionalDependencies: typescript: 5.7.3 @@ -9557,10 +9869,10 @@ snapshots: dependencies: '@octokit/openapi-types': 22.2.0 - '@peterek/vite-plugin-favicons@2.0.0(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1))': + '@peterek/vite-plugin-favicons@2.0.0(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1))': dependencies: favicons: 7.2.0 - vite: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1) + vite: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1) '@pkgjs/parseargs@0.11.0': optional: true @@ -10078,6 +10390,13 @@ snapshots: - bufferutil - utf-8-validate + '@preact/signals-core@1.8.0': {} + + '@preact/signals@1.3.2(preact@10.25.4)': + dependencies: + '@preact/signals-core': 1.8.0 + preact: 10.25.4 + '@radix-ui/number@1.1.0': {} '@radix-ui/primitive@1.1.0': {} @@ -10873,13 +11192,13 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/builder-vite@8.4.7(storybook@8.4.7(prettier@3.3.3))(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1))': + '@storybook/builder-vite@8.4.7(storybook@8.4.7(prettier@3.3.3))(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1))': dependencies: '@storybook/csf-plugin': 8.4.7(storybook@8.4.7(prettier@3.3.3)) browser-assert: 1.2.1 storybook: 8.4.7(prettier@3.3.3) ts-dedent: 2.2.0 - vite: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1) + vite: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1) '@storybook/components@8.4.7(storybook@8.4.7(prettier@3.3.3))': dependencies: @@ -10941,11 +11260,11 @@ snapshots: react-dom: 18.3.1(react@18.3.1) storybook: 8.4.7(prettier@3.3.3) - '@storybook/react-vite@8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.3.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.30.0)(storybook@8.4.7(prettier@3.3.3))(typescript@5.7.3)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1))': + '@storybook/react-vite@8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.3.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.30.0)(storybook@8.4.7(prettier@3.3.3))(typescript@5.7.3)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.4.2(typescript@5.7.3)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.4.2(typescript@5.7.3)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1)) '@rollup/pluginutils': 5.1.4(rollup@4.30.0) - '@storybook/builder-vite': 8.4.7(storybook@8.4.7(prettier@3.3.3))(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1)) + '@storybook/builder-vite': 8.4.7(storybook@8.4.7(prettier@3.3.3))(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1)) '@storybook/react': 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.3.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.7(prettier@3.3.3))(typescript@5.7.3) find-up: 5.0.0 magic-string: 0.30.17 @@ -10955,7 +11274,7 @@ snapshots: resolve: 1.22.10 storybook: 8.4.7(prettier@3.3.3) tsconfig-paths: 4.2.0 - vite: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1) + vite: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1) transitivePeerDependencies: - '@storybook/test' - rollup @@ -11560,10 +11879,10 @@ snapshots: '@ungap/structured-clone@1.2.1': {} - '@vitejs/plugin-react-swc@3.7.2(@swc/helpers@0.5.15)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1))': + '@vitejs/plugin-react-swc@3.7.2(@swc/helpers@0.5.15)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1))': dependencies: '@swc/core': 1.9.3(@swc/helpers@0.5.15) - vite: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1) + vite: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1) transitivePeerDependencies: - '@swc/helpers' @@ -11714,15 +12033,6 @@ snapshots: '@walletconnect/time': 1.0.2 events: 3.3.0 - '@walletconnect/jsonrpc-http-connection@1.0.8(encoding@0.1.13)': - dependencies: - '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/safe-json': 1.0.2 - cross-fetch: 3.2.0(encoding@0.1.13) - events: 3.3.0 - transitivePeerDependencies: - - encoding - '@walletconnect/jsonrpc-provider@1.0.14': dependencies: '@walletconnect/jsonrpc-utils': 1.0.8 @@ -11833,34 +12143,6 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/types@2.17.2': - dependencies: - '@walletconnect/events': 1.0.1 - '@walletconnect/heartbeat': 1.2.2 - '@walletconnect/jsonrpc-types': 1.0.4 - '@walletconnect/keyvaluestorage': 1.1.1 - '@walletconnect/logger': 2.1.2 - 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/kv' - - aws4fetch - - db0 - - ioredis - - uploadthing - '@walletconnect/types@2.17.3': dependencies: '@walletconnect/events': 1.0.1 @@ -11889,85 +12171,6 @@ snapshots: - ioredis - uploadthing - '@walletconnect/universal-provider@2.17.3(encoding@0.1.13)': - dependencies: - '@walletconnect/events': 1.0.1 - '@walletconnect/jsonrpc-http-connection': 1.0.8(encoding@0.1.13) - '@walletconnect/jsonrpc-provider': 1.0.14 - '@walletconnect/jsonrpc-types': 1.0.4 - '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1 - '@walletconnect/logger': 2.1.2 - '@walletconnect/sign-client': 2.17.3 - '@walletconnect/types': 2.17.3 - '@walletconnect/utils': 2.17.3 - events: 3.3.0 - lodash: 4.17.21 - 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/kv' - - aws4fetch - - bufferutil - - db0 - - encoding - - ioredis - - uploadthing - - utf-8-validate - - '@walletconnect/utils@2.17.2': - dependencies: - '@ethersproject/hash': 5.7.0 - '@ethersproject/transactions': 5.7.0 - '@stablelib/chacha20poly1305': 1.0.1 - '@stablelib/hkdf': 1.0.1 - '@stablelib/random': 1.0.2 - '@stablelib/sha256': 1.0.1 - '@stablelib/x25519': 1.0.3 - '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1 - '@walletconnect/relay-api': 1.0.11 - '@walletconnect/relay-auth': 1.0.4 - '@walletconnect/safe-json': 1.0.2 - '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.17.2 - '@walletconnect/window-getters': 1.0.1 - '@walletconnect/window-metadata': 1.0.1 - detect-browser: 5.3.0 - elliptic: 6.6.0 - query-string: 7.1.3 - uint8arrays: 3.1.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/kv' - - aws4fetch - - db0 - - ioredis - - uploadthing - '@walletconnect/utils@2.17.3': dependencies: '@ethersproject/hash': 5.7.0 @@ -12399,6 +12602,8 @@ snapshots: binary-searching@2.0.5: {} + bippy@0.0.25: {} + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -13444,16 +13649,6 @@ snapshots: minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 - elliptic@6.6.0: - dependencies: - bn.js: 4.12.1 - brorand: 1.1.0 - hash.js: 1.1.7 - hmac-drbg: 1.0.1 - inherits: 2.0.4 - minimalistic-assert: 1.0.1 - minimalistic-crypto-utils: 1.0.1 - elliptic@6.6.1: dependencies: bn.js: 4.12.1 @@ -13653,6 +13848,33 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 + esbuild@0.23.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 + esbuild@0.24.2: optionalDependencies: '@esbuild/aix-ppc64': 0.24.2 @@ -14998,6 +15220,8 @@ snapshots: kind-of@6.0.3: {} + kleur@4.1.5: {} + knip@5.33.3(@types/node@20.17.12)(typescript@5.7.3): dependencies: '@nodelib/fs.walk': 1.2.8 @@ -16458,6 +16682,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + preact@10.25.4: {} + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.0: @@ -16723,6 +16949,34 @@ snapshots: '@remix-run/router': 1.19.2 react: 18.3.1 + react-scan@0.0.54(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-router@6.26.2(react@18.3.1))(react@18.3.1)(rollup@4.30.0): + dependencies: + '@babel/core': 7.26.0 + '@babel/generator': 7.26.3 + '@babel/types': 7.26.3 + '@clack/core': 0.3.5 + '@clack/prompts': 0.8.2 + '@preact/signals': 1.3.2(preact@10.25.4) + '@rollup/pluginutils': 5.1.4(rollup@4.30.0) + '@types/node': 20.17.12 + bippy: 0.0.25 + esbuild: 0.24.2 + estree-walker: 3.0.3 + kleur: 4.1.5 + mri: 1.2.0 + playwright: 1.49.1 + preact: 10.25.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tsx: 4.19.2 + optionalDependencies: + react-router: 6.26.2(react@18.3.1) + react-router-dom: 6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + unplugin: 2.1.0 + transitivePeerDependencies: + - rollup + - supports-color + react-style-singleton@2.2.3(@types/react@18.0.14)(react@18.3.1): dependencies: get-nonce: 1.0.1 @@ -17171,6 +17425,8 @@ snapshots: mrmime: 2.0.0 totalist: 3.0.1 + sisteransi@1.0.5: {} + slash@3.0.0: {} slice-ansi@3.0.0: @@ -17733,6 +17989,13 @@ snapshots: tslib@2.8.1: {} + tsx@4.19.2: + dependencies: + esbuild: 0.23.1 + get-tsconfig: 4.8.1 + optionalDependencies: + fsevents: 2.3.3 + tty-browserify@0.0.1: {} type-check@0.4.0: @@ -17884,6 +18147,12 @@ snapshots: acorn: 8.14.0 webpack-virtual-modules: 0.6.2 + unplugin@2.1.0: + dependencies: + acorn: 8.14.0 + webpack-virtual-modules: 0.6.2 + optional: true + unstorage@1.14.4(idb-keyval@6.2.1): dependencies: anymatch: 3.1.3 @@ -18006,38 +18275,38 @@ snapshots: - supports-color - terser - vite-plugin-compression2@1.3.3(rollup@4.30.0)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1)): + vite-plugin-compression2@1.3.3(rollup@4.30.0)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1)): dependencies: '@rollup/pluginutils': 5.1.4(rollup@4.30.0) tar-mini: 0.2.0 - vite: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1) + vite: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1) transitivePeerDependencies: - rollup - vite-plugin-mkcert@1.17.6(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1)): + vite-plugin-mkcert@1.17.6(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1)): dependencies: '@octokit/rest': 20.1.1 axios: 1.7.9(debug@4.4.0) debug: 4.4.0 picocolors: 1.1.1 - vite: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1) + vite: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1) transitivePeerDependencies: - supports-color - vite-plugin-node-polyfills@0.22.0(rollup@4.30.0)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1)): + vite-plugin-node-polyfills@0.22.0(rollup@4.30.0)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1)): dependencies: '@rollup/plugin-inject': 5.0.5(rollup@4.30.0) node-stdlib-browser: 1.3.0 - vite: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1) + vite: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1) transitivePeerDependencies: - rollup - vite-plugin-svgr@4.3.0(rollup@4.30.0)(typescript@5.7.3)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1)): + vite-plugin-svgr@4.3.0(rollup@4.30.0)(typescript@5.7.3)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1)): dependencies: '@rollup/pluginutils': 5.1.4(rollup@4.30.0) '@svgr/core': 8.1.0(typescript@5.7.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.7.3)) - vite: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1) + vite: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1) transitivePeerDependencies: - rollup - supports-color @@ -18047,13 +18316,13 @@ snapshots: dependencies: lib-esm: 0.3.0 - vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1)): + vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1)): dependencies: debug: 4.4.0 globrex: 0.1.2 tsconfck: 3.1.4(typescript@5.7.3) optionalDependencies: - vite: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1) + vite: 6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1) transitivePeerDependencies: - supports-color - typescript @@ -18068,7 +18337,7 @@ snapshots: fsevents: 2.3.3 terser: 5.37.0 - vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(yaml@2.5.1): + vite@6.0.7(@types/node@20.17.12)(jiti@2.4.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.1): dependencies: esbuild: 0.24.2 postcss: 8.4.49 @@ -18078,6 +18347,7 @@ snapshots: fsevents: 2.3.3 jiti: 2.4.2 terser: 5.37.0 + tsx: 4.19.2 yaml: 2.5.1 vitest@2.1.8(@types/node@20.17.12)(@vitest/ui@2.1.8)(happy-dom@16.3.0)(jsdom@20.0.3)(terser@5.37.0): diff --git a/src/renderer/domains/network/accounts/model.test.ts b/src/renderer/domains/network/accounts/model.test.ts index 7bc1cd0c6b..b75d2604bd 100644 --- a/src/renderer/domains/network/accounts/model.test.ts +++ b/src/renderer/domains/network/accounts/model.test.ts @@ -55,7 +55,7 @@ describe('accounts model', () => { it('should successfully update account', async () => { const scope = fork({ values: [[accountsDomainModel.__test.$list, accounts]], - handlers: [[accountsDomainModel.__test.updateAccountFx, () => true]], + handlers: [[accountsDomainModel.updateAccount, () => true]], }); const draft: AnyAccountDraft = { @@ -77,7 +77,7 @@ describe('accounts model', () => { it('should skip update if account is not defined', async () => { const scope = fork({ values: [[accountsDomainModel.__test.$list, accounts]], - handlers: [[accountsDomainModel.__test.updateAccountFx, () => false]], + handlers: [[accountsDomainModel.updateAccounts, () => false]], }); const draft: AnyAccountDraft = { diff --git a/src/renderer/domains/network/accounts/model.ts b/src/renderer/domains/network/accounts/model.ts index 0572b27836..a37f6a68cc 100644 --- a/src/renderer/domains/network/accounts/model.ts +++ b/src/renderer/domains/network/accounts/model.ts @@ -2,7 +2,7 @@ import { attach, createEffect, createStore, restore, sample } from 'effector'; import { once, readonly } from 'patronum'; import { storageService } from '@/shared/api/storage'; -import { merge, nonNullable, nullable } from '@/shared/lib/utils'; +import { merge, nonNullable } from '@/shared/lib/utils'; import { accountsService } from './service'; import { type AnyAccount, type AnyAccountDraft } from './types'; @@ -22,26 +22,28 @@ const createAccountsFx = createEffect(async (accounts: AnyAccount[]): Promise x ?? []); }); -const updateAccountFx = createEffect(async (account: AnyAccountDraft | null): Promise => { - if (nullable(account)) return false; +const updateAccountsFx = createEffect(async (accounts: AnyAccountDraft[]): Promise => { + if (accounts.length === 0) return false; - const id = accountsService.uniqId(account); - const record = { ...account }; - delete record['id']; + const drafts = accounts.map(a => { + const id = accountsService.uniqId(a); - return storageService.accounts2.update(id, record).then(nonNullable); + return { ...a, id }; + }); + + return storageService.accounts2.updateAll(drafts).then(nonNullable); }); const updateAccount = attach({ source: $accounts, mapParams: (draft: AnyAccountDraft, accounts) => { if (accounts.find(a => accountsService.uniqId(a) === accountsService.uniqId(draft))) { - return draft; + return [draft]; } - return null; + return []; }, - effect: updateAccountFx, + effect: updateAccountsFx, }); const deleteAccountsFx = createEffect(async (accounts: AnyAccount[]) => { @@ -75,9 +77,25 @@ sample({ fn: (accounts, { params: draft }) => { const draftId = accountsService.uniqId(draft); + return accounts.map(a => (accountsService.uniqId(a) === draftId ? { ...a, ...draft } : a)); + }, + target: $accounts, +}); + +sample({ + clock: updateAccountsFx.done, + source: $accounts, + filter: (_, { result: successful }) => successful, + fn: (accounts, { params: drafts }) => { + const draftsMap = drafts.reduce>((acc, draft) => { + acc[accountsService.uniqId(draft)] = draft; + + return acc; + }, {}); + return accounts.map(a => - accountsService.uniqId(a) === draftId ? ({ ...a, ...draft } as AnyAccount) : a, - ) as AnyAccount[]; + accountsService.uniqId(a) in draftsMap ? { ...a, ...draftsMap[accountsService.uniqId(a)] } : a, + ); }, target: $accounts, }); @@ -100,10 +118,10 @@ export const accountsDomainModel = { populate: populateFx, createAccounts: createAccountsFx, updateAccount, + updateAccounts: updateAccountsFx, deleteAccounts: deleteAccountsFx, __test: { $list: $accounts, - updateAccountFx, }, }; diff --git a/src/renderer/entities/multisig/model/multisigs-model.ts b/src/renderer/entities/multisig/model/multisigs-model.ts index 0952be4c51..d2dcc2b123 100644 --- a/src/renderer/entities/multisig/model/multisigs-model.ts +++ b/src/renderer/entities/multisig/model/multisigs-model.ts @@ -311,7 +311,7 @@ sample({ clock: $flexibleWithProxy, filter: nonNullable, fn: (flexibleWithProxy) => flexibleWithProxy!.accounts, - target: series(accounts.updateAccount), + target: accounts.updateAccounts, }); sample({ diff --git a/src/renderer/entities/wallet/model/wallet-model.ts b/src/renderer/entities/wallet/model/wallet-model.ts index db1c0901e4..d086a73235 100644 --- a/src/renderer/entities/wallet/model/wallet-model.ts +++ b/src/renderer/entities/wallet/model/wallet-model.ts @@ -396,6 +396,8 @@ export const walletModel = { $activeAccounts, $isLoadingWallets: fetchAllWalletsFx.pending, + createWallet: walletCreatedFx, + events: { walletStarted, watchOnlyCreated, diff --git a/src/renderer/entities/wallet/ui/Cards/WalletCardLg.tsx b/src/renderer/entities/wallet/ui/Cards/WalletCardLg.tsx index d3c25dc8c8..1c9c600b67 100644 --- a/src/renderer/entities/wallet/ui/Cards/WalletCardLg.tsx +++ b/src/renderer/entities/wallet/ui/Cards/WalletCardLg.tsx @@ -1,24 +1,19 @@ -import { type ReactNode } from 'react'; +import { type PropsWithChildren, type ReactNode } from 'react'; import { type Wallet, WalletIconType } from '@/shared/core'; -import { useI18n } from '@/shared/i18n'; import { cnTw } from '@/shared/lib/utils'; -import { BodyText, FootnoteText, StatusLabel } from '@/shared/ui'; +import { BodyText, FootnoteText } from '@/shared/ui'; import { walletUtils } from '../../lib/wallet-utils'; import { WalletIcon } from '../WalletIcon/WalletIcon'; -type Props = { +type Props = PropsWithChildren<{ + className?: string; wallet: Wallet; description?: string | ReactNode; - full?: boolean; - className?: string; -}; - -export const WalletCardLg = ({ wallet, description, full, className }: Props) => { - const { t } = useI18n(); - - const isWalletConnect = walletUtils.isWalletConnectGroup(wallet); + additionalInfo?: ReactNode; +}>; +export const WalletCardLg = ({ wallet, description, additionalInfo, className, children }: Props) => { const type = walletUtils.isFlexibleMultisig(wallet) && !wallet.activated ? WalletIconType.FLEXIBLE_MULTISIG_INACTIVE @@ -28,14 +23,7 @@ export const WalletCardLg = ({ wallet, description, full, className }: Props) =>
- {isWalletConnect && !full && ( - - )} + {additionalInfo}
{wallet.name} @@ -46,13 +34,7 @@ export const WalletCardLg = ({ wallet, description, full, className }: Props) => )}
- {isWalletConnect && full && ( - - )} + {children}
); }; diff --git a/src/renderer/entities/wallet/ui/Cards/WalletCardMd.tsx b/src/renderer/entities/wallet/ui/Cards/WalletCardMd.tsx index cee06e2f0c..b2d4c5d09b 100644 --- a/src/renderer/entities/wallet/ui/Cards/WalletCardMd.tsx +++ b/src/renderer/entities/wallet/ui/Cards/WalletCardMd.tsx @@ -3,12 +3,12 @@ import { type MouseEvent, type PropsWithChildren, type ReactNode } from 'react'; import { type Wallet } from '@/shared/core'; import { cnTw, nonNullable, nullable } from '@/shared/lib/utils'; import { BodyText, FootnoteText } from '@/shared/ui'; -import { walletUtils } from '../../lib/wallet-utils'; import { WalletIcon } from '../WalletIcon/WalletIcon'; type Props = { wallet: Wallet; description?: string | ReactNode; + meta?: ReactNode; prefix?: ReactNode; hideIcon?: boolean; onClick?: () => void; @@ -17,13 +17,12 @@ type Props = { export const WalletCardMd = ({ wallet, description, + meta, prefix, hideIcon, children, onClick, }: PropsWithChildren) => { - const isWalletConnect = walletUtils.isWalletConnectGroup(wallet); - const handleClick = (fn?: () => void) => { return (event: MouseEvent) => { if (!fn) return; @@ -60,14 +59,7 @@ export const WalletCardMd = ({ > {wallet.name} - {isWalletConnect && ( - - )} + {meta} {typeof description === 'string' ? ( {description} diff --git a/src/renderer/entities/wallet/ui/MultiAccountsList/MultiAccountsList.tsx b/src/renderer/entities/wallet/ui/MultiAccountsList/MultiAccountsList.tsx index 760a24c623..971530a3f5 100644 --- a/src/renderer/entities/wallet/ui/MultiAccountsList/MultiAccountsList.tsx +++ b/src/renderer/entities/wallet/ui/MultiAccountsList/MultiAccountsList.tsx @@ -1,5 +1,6 @@ import { type Chain } from '@/shared/core'; import { useI18n } from '@/shared/i18n'; +import { useDeferredList } from '@/shared/lib/hooks'; import { cnTw, toAddress } from '@/shared/lib/utils'; import { type AccountId } from '@/shared/polkadotjs-schemas'; import { FootnoteText } from '@/shared/ui'; @@ -19,6 +20,8 @@ type Props = { export const MultiAccountsList = ({ accounts, className, headerClassName }: Props) => { const { t } = useI18n(); + const { list } = useDeferredList({ list: accounts }); + return (
@@ -29,8 +32,8 @@ export const MultiAccountsList = ({ accounts, className, headerClassName }: Prop
-
    - {accounts.map(({ chain, accountId }) => { +
      + {list.map(({ chain, accountId }) => { const { chainId, addressPrefix } = chain; return ( diff --git a/src/renderer/entities/walletConnect/index.ts b/src/renderer/entities/walletConnect/index.ts deleted file mode 100644 index d6d2627287..0000000000 --- a/src/renderer/entities/walletConnect/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { walletConnectModel } from './model/wallet-connect-model'; -export { DEFAULT_POLKADOT_METHODS } from './lib/constants'; -export { walletConnectUtils } from './lib/utils'; -export type { InitConnectParams, InitReconnectParams } from './lib/types'; diff --git a/src/renderer/entities/walletConnect/lib/__tests__/utils.test.ts b/src/renderer/entities/walletConnect/lib/__tests__/utils.test.ts deleted file mode 100644 index 5fa1976a71..0000000000 --- a/src/renderer/entities/walletConnect/lib/__tests__/utils.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type Provider from '@walletconnect/universal-provider'; - -import { type Chain } from '@/shared/core'; -import { walletConnectUtils } from '../utils'; - -describe('entities/walletConnect/lib/onChainUtils', () => { - test('should return chain ids in wallet connect type', () => { - const chains = [ - { - chainId: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3', - }, - { - chainId: '0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe', - }, - ]; - - const result = walletConnectUtils.getWalletConnectChains(chains as unknown as Chain[]); - - expect(result).toEqual(['polkadot:91b171bb158e2d3848fa23a9f1c25182', 'polkadot:b0a8d493285c2df73290dfb7e61f870f']); - }); - - test('should return false if not connected', () => { - const provider = { - client: { - session: { - getAll: () => [], - }, - }, - } as unknown as Provider; - - const result = walletConnectUtils.isConnected(provider, 'topic'); - - expect(result).toEqual(false); - }); - - test('should return true if connected', () => { - const provider = { - client: { - session: { - getAll: () => ['topic'], - }, - }, - } as unknown as Provider; - - const result = walletConnectUtils.isConnected(provider, 'topic'); - - expect(result).toEqual(false); - }); -}); diff --git a/src/renderer/entities/walletConnect/lib/types.ts b/src/renderer/entities/walletConnect/lib/types.ts deleted file mode 100644 index cd46cb81d4..0000000000 --- a/src/renderer/entities/walletConnect/lib/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type Provider from '@walletconnect/universal-provider'; - -export type InitConnectParams = { - provider: Provider; - chains: string[]; - pairing?: any; -}; - -export type InitReconnectParams = { - chains: string[]; - pairing: any; -}; diff --git a/src/renderer/entities/walletConnect/lib/utils.ts b/src/renderer/entities/walletConnect/lib/utils.ts deleted file mode 100644 index 258e5ef0a4..0000000000 --- a/src/renderer/entities/walletConnect/lib/utils.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type Provider from '@walletconnect/universal-provider'; - -import { type Chain, type ChainId, type Wallet } from '@/shared/core'; -import { walletUtils } from '@/entities/wallet'; - -import { FIRST_CHAIN_ID_SYMBOL, LAST_CHAIN_ID_SYMBOL } from './constants'; - -export const walletConnectUtils = { - getWalletConnectChains, - getWalletConnectChainId, - isConnected, - isConnectedByAccounts, -}; - -function getWalletConnectChains(chains: Chain[]): string[] { - return chains.map((c) => getWalletConnectChainId(c.chainId)); -} - -function getWalletConnectChainId(chainId: ChainId): string { - return `polkadot:${chainId.slice(FIRST_CHAIN_ID_SYMBOL, LAST_CHAIN_ID_SYMBOL)}`; -} - -function isConnected(provider: Provider, sessionTopic: string): boolean { - const sessions = provider.client.session.getAll() || []; - - return sessions.some((session) => session.topic === sessionTopic); -} - -function isConnectedByAccounts(provider: Provider, wallet: Wallet): boolean { - if (!walletUtils.isWalletConnectGroup(wallet)) return false; - - return walletConnectUtils.isConnected(provider, wallet.accounts[0].signingExtras?.sessionTopic); -} diff --git a/src/renderer/entities/walletConnect/model/wallet-connect-model.ts b/src/renderer/entities/walletConnect/model/wallet-connect-model.ts deleted file mode 100644 index fe4d9edb75..0000000000 --- a/src/renderer/entities/walletConnect/model/wallet-connect-model.ts +++ /dev/null @@ -1,405 +0,0 @@ -import { type SessionTypes } from '@walletconnect/types'; -// eslint-disable-next-line import-x/no-named-as-default -import Provider from '@walletconnect/universal-provider'; -import { getSdkError } from '@walletconnect/utils'; -import { createEffect, createEvent, createStore, restore, sample, scopeBind } from 'effector'; -import isEmpty from 'lodash/isEmpty'; - -import { localStorageService } from '@/shared/api/local-storage'; -import { type Account, type ID, kernelModel } from '@/shared/core'; -import { series } from '@/shared/effector'; -import { nonNullable } from '@/shared/lib/utils'; -// TODO wallet connect model should be in feature, not entities -// eslint-disable-next-line boundaries/element-types -import { type AnyAccount, accounts } from '@/domains/network'; -import { accountUtils, walletModel, walletUtils } from '@/entities/wallet'; -import { - DEFAULT_APP_METADATA, - DEFAULT_LOGGER, - DEFAULT_POLKADOT_EVENTS, - DEFAULT_POLKADOT_METHODS, - DEFAULT_PROJECT_ID, - DEFAULT_RELAY_URL, - EXTEND_PAIRING, - WALLETCONNECT_CLIENT_ID, -} from '../lib/constants'; -import { type InitConnectParams } from '../lib/types'; -import { walletConnectUtils } from '../lib/utils'; - -type SessionTopicParams = { - walletId: ID; - accounts: Account[]; - topic: string; -}; - -type UpdateAccountsParams = { - walletId: ID; - accounts: Account[]; -}; - -const connect = createEvent>(); -const disconnectCurrentSessionStarted = createEvent(); -const disconnectStarted = createEvent(); -const reset = createEvent(); -const sessionUpdated = createEvent(); -const uriUpdated = createEvent(); -const connected = createEvent(); -const connectionRejected = createEvent(); -const sessionTopicUpdated = createEvent(); -const accountsUpdated = createEvent(); -const pairingRemoved = createEvent(); - -const $provider = createStore(null).reset(reset); -const $session = createStore(null).reset(reset); -const $uri = restore(uriUpdated, '').reset(disconnectCurrentSessionStarted); -const $accounts = createStore([]).reset(reset); - -const createProviderFx = createEffect(async (): Promise => { - return Provider.init({ - logger: DEFAULT_LOGGER, - relayUrl: DEFAULT_RELAY_URL, - projectId: DEFAULT_PROJECT_ID, - metadata: DEFAULT_APP_METADATA, - }); -}); - -const initConnectFx = createEffect( - async ({ provider, chains, pairing }: InitConnectParams): Promise => { - const optionalNamespaces = { - polkadot: { - chains, - methods: [DEFAULT_POLKADOT_METHODS.POLKADOT_SIGN_TRANSACTION], - events: [DEFAULT_POLKADOT_EVENTS.CHAIN_CHANGED, DEFAULT_POLKADOT_EVENTS.ACCOUNTS_CHANGED], - }, - }; - - return provider.connect({ pairingTopic: pairing?.topic, optionalNamespaces }); - }, -); - -const extendSessionsFx = createEffect((client: Provider) => { - const sessions = client.client.session.getAll(); - - for (const s of sessions) { - client.client.extend({ topic: s.topic }).catch((e) => console.warn(e)); - } - - const pairings = client.client.pairing.getAll({ active: true }); - - for (const p of pairings) { - client.client.core.pairing.updateExpiry({ - topic: p.topic, - expiry: Math.round(Date.now() / 1000) + EXTEND_PAIRING, - }); - } -}); - -const subscribeUriFx = createEffect((provider: Provider) => { - const boundUriUpdated = scopeBind(uriUpdated, { safe: true }); - - provider.on('display_uri', (uri: string) => { - boundUriUpdated(uri); - }); -}); - -const subscribeToEventsFx = createEffect(({ client }: Provider) => { - const boundSessionUpdated = scopeBind(sessionUpdated, { safe: true }); - const boundReset = scopeBind(reset, { safe: true }); - - client.on('session_update', ({ topic, params }) => { - console.log('WC EVENT', 'session_update', { topic, params }); - const { namespaces } = params; - const _session = client.session.get(topic); - const updatedSession = { ..._session, namespaces }; - - boundSessionUpdated(updatedSession); - }); - - client.on('session_ping', (args) => { - console.log('WC EVENT', 'session_ping', args); - }); - - client.on('session_event', (args) => { - console.log('WC EVENT', 'session_event', args); - }); - - client.on('session_delete', () => { - console.log('WC EVENT', 'session_delete'); - boundReset(); - }); -}); - -const checkPersistedStateFx = createEffect((client: Provider) => { - if (client.client.session.length) { - const lastKeyIndex = client.client.session.keys.length - 1; - const session = client.client.session.get(client.client.session.keys[lastKeyIndex]); - sessionUpdated(session); - } -}); - -const logProviderIdFx = createEffect(async (client: Provider) => { - try { - const clientId = await client.client.core.crypto.getClientId(); - console.log('WalletConnect ProviderID: ', clientId); - localStorageService.saveToStorage(WALLETCONNECT_CLIENT_ID, clientId); - } catch (error) { - console.error('Failed to set WalletConnect clientId in localStorage: ', error); - } -}); - -const sessionTopicUpdatedFx = createEffect( - async ({ accounts: accountsToUpdate, topic, provider, walletId }: SessionTopicParams & { provider: Provider }) => { - const account = accountsToUpdate.at(0); - if (!account) { - return { - walletId, - accounts: [], - }; - } - - if (!accountUtils.isWcAccount(account)) { - return { - walletId, - accounts: [], - }; - } - - const oldSessionTopic = account.signingExtras?.sessionTopic; - let oldSession: SessionTypes.Struct | undefined; - - try { - oldSession = provider.client.session.get(oldSessionTopic); - } catch (e) { - console.error(e); - } - - const updatedAccounts = accountsToUpdate.map((account) => { - if (accountUtils.isWcAccount(account)) { - return { - ...account, - signingExtras: { ...account.signingExtras, sessionTopic: topic }, - }; - } - - return account; - }); - - await Promise.all(updatedAccounts.map(accounts.updateAccount)); - - if (oldSession) { - await disconnectFx({ provider, session: oldSession }); - } - - return { - walletId, - accounts: updatedAccounts, - }; - }, -); - -const removePairingFx = createEffect( - async ({ provider, topic }: { provider: Provider; topic: string }): Promise => { - const reason = getSdkError('USER_DISCONNECTED'); - - await provider.client.pairing.delete(topic, reason); - }, -); - -const updateWcAccountsFx = createEffect(async (wcAccounts: AnyAccount[]) => { - return Promise.all(wcAccounts.map(accounts.updateAccount)); -}); - -type DisconnectParams = { - provider: Provider; - session: SessionTypes.Struct; -}; - -const disconnectFx = createEffect(async ({ provider, session }: DisconnectParams) => { - const reason = getSdkError('USER_DISCONNECTED'); - - await provider.client.disconnect({ - topic: session.topic, - reason, - }); -}); - -const removeSessionFx = createEffect( - async ({ provider, session }: { provider: Provider; session: SessionTypes.Struct }) => { - const reason = getSdkError('USER_DISCONNECTED'); - - await provider.client.session.delete(session.topic, reason); - }, -); - -sample({ - clock: connect, - source: $provider, - filter: nonNullable, - target: subscribeUriFx, -}); - -sample({ - clock: accountsUpdated, - filter: ({ accounts }) => !isEmpty(accounts), - fn: ({ accounts }) => accounts, - target: updateWcAccountsFx, -}); - -sample({ - clock: kernelModel.events.appStarted, - target: createProviderFx, -}); - -sample({ - clock: createProviderFx.doneData, - filter: (client): client is Provider => client !== null, - target: [extendSessionsFx, subscribeToEventsFx, checkPersistedStateFx, logProviderIdFx], -}); - -// sample({ -// clock: disconnectFx.done, -// target: createProviderFx, -// }); - -sample({ - clock: sessionUpdated, - target: $session, -}); - -sample({ - clock: createProviderFx.doneData, - filter: (client) => nonNullable(client), - fn: (client) => client!, - target: $provider, -}); - -sample({ - clock: createProviderFx.failData, - fn: (e) => console.error('Failed to create WalletConnect client', e), - target: createProviderFx, -}); - -sample({ - clock: connect, - source: $provider, - filter: (provider, props) => provider !== null && !isEmpty(props.chains), - fn: (provider, props) => ({ - provider: provider!, - ...props, - }), - target: initConnectFx, -}); - -sample({ - clock: initConnectFx.doneData, - filter: nonNullable, - fn: (session) => - Object.values(session!.namespaces) - .map((namespace) => namespace.accounts) - .flat(), - target: $accounts, -}); - -sample({ - clock: initConnectFx.doneData, - filter: nonNullable, - target: $session, -}); - -sample({ - clock: initConnectFx.done, - target: connected, -}); - -sample({ - clock: disconnectCurrentSessionStarted, - source: $session, - filter: (session) => nonNullable(session), - fn: (session) => session!.topic, - target: disconnectStarted, -}); - -sample({ - clock: disconnectStarted, - source: $provider, - filter: (provider, sessionTopic) => nonNullable(provider?.client.session.get(sessionTopic)), - fn: (provider, sessionTopic) => ({ - provider: provider!, - session: provider!.client.session.get(sessionTopic)!, - }), - target: disconnectFx, -}); - -sample({ - clock: disconnectFx.done, - fn: ({ params }) => params, - target: removeSessionFx, -}); - -sample({ - clock: sessionTopicUpdated, - source: $provider, - filter: nonNullable, - fn: (provider, params) => ({ provider: provider!, ...params }), - target: sessionTopicUpdatedFx, -}); - -sample({ - clock: initConnectFx.fail, - fn: ({ error }) => { - console.error('Failed to connect:', error); - - return error.message; - }, - target: connectionRejected, -}); - -sample({ - clock: pairingRemoved, - source: $provider, - filter: (provider: Provider | null): provider is Provider => provider !== null, - fn: (provider, topic) => ({ provider, topic }), - target: removePairingFx, -}); - -sample({ - clock: [$provider, walletModel.events.walletCreatedDone, updateWcAccountsFx.doneData], - source: { - wallets: walletModel.$allWallets, - provider: $provider, - }, - filter: ({ provider }) => Boolean(provider), - fn: ({ wallets, provider }) => { - return wallets.filter(walletUtils.isWalletConnectGroup).map((wallet) => { - return { - walletId: wallet.id, - data: { isConnected: walletConnectUtils.isConnectedByAccounts(provider!, wallet) }, - }; - }); - }, - target: series(walletModel.events.updateWallet), -}); - -export const walletConnectModel = { - $provider, - $session, - $uri, - $accounts, - - events: { - connect, - initConnectFailed: initConnectFx.fail, - disconnectCurrentSessionStarted, - disconnectStarted, - sessionUpdated, - connected, - connectionRejected, - sessionTopicUpdated, - sessionTopicUpdateFailed: sessionTopicUpdatedFx.fail, - sessionTopicUpdateDone: sessionTopicUpdatedFx.doneData, - accountsUpdated, - accountsUpdateDone: updateWcAccountsFx.doneData, - pairingRemoved, - reset, - }, -}; diff --git a/src/renderer/features/operations/OperationSign/model/sign-wc-model.ts b/src/renderer/features/operations/OperationSign/model/sign-wc-model.ts deleted file mode 100644 index 8d98c8d0b2..0000000000 --- a/src/renderer/features/operations/OperationSign/model/sign-wc-model.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { type EngineTypes } from '@walletconnect/types'; -import type Provider from '@walletconnect/universal-provider'; -import { combine, createEffect, createEvent, createStore, sample } from 'effector'; -import { combineEvents } from 'patronum'; - -import { AccountType, type HexString, type WcAccount } from '@/shared/core'; -import { nonNullable, toAccountId } from '@/shared/lib/utils'; -import { networkModel } from '@/entities/network'; -import { accountUtils, walletModel, walletUtils } from '@/entities/wallet'; -import { type InitReconnectParams, walletConnectModel } from '@/entities/walletConnect'; -import { operationSignUtils } from '../lib/operation-sign-utils'; -import { ReconnectStep } from '../lib/types'; - -import { operationSignModel } from './operation-sign-model'; - -type SignParams = { - provider: Provider; - payload: EngineTypes.RequestParams; -}; - -const reset = createEvent(); -const reconnectModalShown = createEvent(); -const reconnectStarted = createEvent(); -const reconnectAborted = createEvent(); -const reconnectDone = createEvent(); -const signingStarted = createEvent(); - -const $reconnectStep = createStore(ReconnectStep.NOT_STARTED).reset(reset); -const $isSigningRejected = createStore(false).reset(reset); -const $signatures = createStore([]).reset(reset); - -type SignResponse = { - signature: HexString; -}; - -const $isStatusShown = combine( - { - reconnectStep: $reconnectStep, - isSigningRejected: $isSigningRejected, - }, - ({ reconnectStep, isSigningRejected }): boolean => { - return ( - operationSignUtils.isReconnectingStep(reconnectStep) || - operationSignUtils.isConnectedStep(reconnectStep) || - operationSignUtils.isRejectedStep(reconnectStep) || - operationSignUtils.isFailedStep(reconnectStep) || - isSigningRejected - ); - }, -); - -const signFx = createEffect(async (signParams: SignParams[]): Promise => { - const results: SignResponse[] = []; - - for (const { provider, payload } of signParams) { - // should be signed step by step - const response = await provider.client.request(payload); - - results.push(response); - } - - return results; -}); - -sample({ - clock: signingStarted, - target: signFx, -}); - -sample({ - clock: signFx.doneData, - fn: (responses) => responses.map(({ signature }) => signature), - target: $signatures, -}); - -sample({ - clock: signFx.fail, - fn: () => true, - target: $isSigningRejected, -}); - -sample({ - clock: reconnectModalShown, - fn: () => ReconnectStep.READY_TO_RECONNECT, - target: $reconnectStep, -}); - -sample({ - clock: reconnectStarted, - fn: () => ReconnectStep.RECONNECTING, - target: $reconnectStep, -}); - -sample({ - clock: reconnectStarted, - target: walletConnectModel.events.connect, -}); - -sample({ - clock: [walletConnectModel.events.initConnectFailed, walletConnectModel.events.sessionTopicUpdateFailed], - source: $reconnectStep, - filter: (step) => step === ReconnectStep.RECONNECTING, - fn: () => ReconnectStep.FAILED, - target: $reconnectStep, -}); - -sample({ - clock: walletConnectModel.events.connected, - source: { - signer: operationSignModel.$signer, - wallets: walletModel.$wallets, - step: $reconnectStep, - session: walletConnectModel.$session, - }, - filter: ({ step, session, signer }) => { - const isCorrectStep = - operationSignUtils.isReconnectingStep(step) || - operationSignUtils.isFailedStep(step) || - operationSignUtils.isConnectedStep(step); - - return isCorrectStep && operationSignUtils.isTopicExist(session) && nonNullable(signer); - }, - fn: ({ wallets, signer, session }) => ({ - walletId: signer!.walletId, - accounts: walletUtils.getAccountsBy(wallets, (a) => a.walletId === signer?.walletId), - topic: session!.topic, - }), - target: walletConnectModel.events.sessionTopicUpdated, -}); - -sample({ - clock: combineEvents({ - events: [walletConnectModel.events.sessionTopicUpdateDone], - reset: reconnectStarted, - }), - source: { - signer: operationSignModel.$signer, - wallets: walletModel.$wallets, - newAccounts: walletConnectModel.$accounts, - chains: networkModel.$chains, - }, - filter: ({ signer }) => Boolean(signer?.walletId), - fn: ({ signer, wallets, newAccounts, chains }) => { - const oldAccount = walletUtils.getAccountBy(wallets, (a) => a.walletId === signer?.walletId); - const updatedAccounts: WcAccount[] = []; - - for (const account of newAccounts) { - const [_, chainId, address] = account.split(':'); - const accountId = toAccountId(address); - const chain = Object.values(chains).find((chain) => chain.chainId.includes(chainId)); - - if (!chain || !oldAccount || !accountUtils.isWcAccount(oldAccount)) continue; - - updatedAccounts.push({ - ...oldAccount, - chainId: chain.chainId, - accountType: AccountType.WALLET_CONNECT, - accountId, - signingExtras: oldAccount.signingExtras, - }); - } - - return { walletId: signer!.walletId, accounts: updatedAccounts }; - }, - target: walletConnectModel.events.accountsUpdated, -}); - -sample({ - clock: walletConnectModel.events.connectionRejected, - source: $reconnectStep, - filter: operationSignUtils.isReconnectingStep, - fn: () => ReconnectStep.REJECTED, - target: $reconnectStep, -}); - -sample({ - clock: walletConnectModel.events.accountsUpdateDone, - fn: () => ReconnectStep.SUCCESS, - target: $reconnectStep, -}); - -sample({ - clock: [reconnectAborted, reconnectDone], - target: reset, -}); - -export const signWcModel = { - $reconnectStep, - $isSigningRejected, - $signatures, - $isStatusShown, - - events: { - signingStarted, - reset, - reconnectModalShown, - reconnectStarted, - reconnectAborted, - reconnectDone, - }, -}; diff --git a/src/renderer/features/operations/OperationSign/model/walletConnectSign.ts b/src/renderer/features/operations/OperationSign/model/walletConnectSign.ts new file mode 100644 index 0000000000..043c916346 --- /dev/null +++ b/src/renderer/features/operations/OperationSign/model/walletConnectSign.ts @@ -0,0 +1,205 @@ +import { type ApiPromise } from '@polkadot/api'; +import { type SignerPayloadJSON } from '@substrate/txwrapper-polkadot'; +import { type SessionTypes } from '@walletconnect/types'; +import { attach, createEffect, createStore, sample } from 'effector'; +import { createGate } from 'effector-react'; + +import { type Address, type ChainId, type HexString } from '@/shared/core'; +import { series, waitFor } from '@/shared/effector'; +import { assert, createTxMetadata, nonNullable, toAddress, upgradeNonce } from '@/shared/lib/utils'; +import { networkModel } from '@/entities/network'; +import { transactionService } from '@/entities/transaction'; +import { DEFAULT_POLKADOT_METHODS, walletConnect, walletConnectService } from '@/features/wallet-wallet-connect'; +import { type SigningPayload } from '../lib/types'; + +type Step = 'idle' | 'signing' | 'rejected' | 'failed' | 'success'; + +type SignResponse = { + signature: HexString; +}; + +const flow = createGate<{ payloads: SigningPayload[] }>({ defaultState: { payloads: [] } }); + +const $signingPayloads = flow.state.map(({ payloads }) => payloads); +const $transactions = createStore[]>([]); +const $session = createStore(null); +const $step = createStore('idle'); +const $signed = createStore([]).reset(flow.close); + +const gotFirstPayload = $signingPayloads.updates.map((payloads) => payloads.at(0)).filter({ fn: nonNullable }); + +type SetupParams = { + payloads: SigningPayload[]; + apis: Record; +}; + +const setupTransactionFx = createEffect(async ({ payloads, apis }: SetupParams) => { + const payload = payloads.at(0); + assert(payload, "Can't prepare empty payload"); + + const account = payload.signatory || payload.account; + const api = apis[payload.chain.chainId]; + + const address = toAddress(account.accountId, { prefix: payload.chain.addressPrefix }); + let metadata = await createTxMetadata(address, api); + + const result: ReturnType[] = []; + + for (const { transaction } of payloads) { + const payload = transactionService.createPayloadWithMetadata(transaction, api, metadata); + result.push(payload); + metadata = upgradeNonce(metadata, 1); + } + + transactionService.logPayload(result); + + return result; +}); + +const getSessionFx = attach({ effect: walletConnect.restoreSession }); + +type SignParams = { + session: SessionTypes.Struct; + chainId: ChainId; + address: Address; + payload: SignerPayloadJSON; +}; + +const signFx = attach({ + source: walletConnect.$client, + async effect(client, { chainId, address, payload, session }: SignParams) { + assert(client, 'Wallet Connect client not found.'); + + const response = await walletConnect.request({ + client, + session, + chainId: walletConnectService.getWalletConnectChainId(chainId), + request: { + method: DEFAULT_POLKADOT_METHODS.POLKADOT_SIGN_TRANSACTION, + params: { + address, + transactionPayload: payload, + }, + }, + }); + + return response as SignResponse; + }, +}); + +const signAllFx = series(signFx); + +// Storing session + +sample({ + clock: getSessionFx.doneData, + target: $session, +}); + +sample({ + clock: flow.close, + target: $session.reinit, +}); + +// Storing transaction data + +sample({ + clock: setupTransactionFx.doneData, + target: $transactions, +}); + +sample({ + clock: flow.close, + target: $transactions.reinit, +}); + +// Steps + +sample({ + clock: flow.open, + fn: () => 'signing' as const, + target: $step, +}); + +sample({ + clock: getSessionFx.fail, + fn: () => 'rejected' as const, + target: $step, +}); + +sample({ + clock: signAllFx.fail, + fn: () => 'failed' as const, + target: $step, +}); + +sample({ + clock: signAllFx.done, + fn: () => 'success' as const, + target: $step, +}); + +sample({ + clock: flow.close, + target: $step.reinit, +}); + +// Main signing flow + +sample({ + clock: gotFirstPayload, + source: networkModel.$chains, + fn: (chains, { account }) => { + return { + pairingTopic: walletConnectService.isWalletConnectAccount(account) + ? account.signingExtras.pairingTopic + : undefined, + chains: Object.values(chains).map((c) => c.chainId), + }; + }, + target: getSessionFx, +}); + +sample({ + clock: $signingPayloads, + source: networkModel.$apis, + filter: (_, payloads) => payloads.length > 0, + fn: (apis, payloads) => ({ apis, payloads }), + target: setupTransactionFx, +}); + +const readyToSign = waitFor({ + source: getSessionFx.doneData, + clock: setupTransactionFx.doneData, + reset: flow.close, +}); + +sample({ + clock: readyToSign, + source: walletConnect.$client, + filter: nonNullable, + fn(client, { trigger: transactions, event: session }) { + return transactions.map(({ info, unsigned: { metadataRpc: _, ...unsigned } }) => ({ + client: client!, + session, + chainId: info.genesisHash as ChainId, + address: info.address, + payload: unsigned, + })); + }, + target: signAllFx, +}); + +sample({ + clock: signAllFx.doneData, + target: $signed, +}); + +export const walletConnectSign = { + $pairingUri: walletConnect.$pairingUri, + $transactions, + $session, + $step, + $signed, + flow, +}; diff --git a/src/renderer/features/operations/OperationSign/ui/WalletConnect.tsx b/src/renderer/features/operations/OperationSign/ui/WalletConnect.tsx index 305fd8ce76..02553e3b7b 100644 --- a/src/renderer/features/operations/OperationSign/ui/WalletConnect.tsx +++ b/src/renderer/features/operations/OperationSign/ui/WalletConnect.tsx @@ -1,4 +1,3 @@ -import { type UnsignedTransaction } from '@substrate/txwrapper-polkadot'; import { useGate, useUnit } from 'effector-react'; import { useEffect, useState } from 'react'; @@ -7,73 +6,39 @@ import wallet_connect_confirm_webm from '@/shared/assets/video/wallet_connect_co import { type HexString } from '@/shared/core'; import { useI18n } from '@/shared/i18n'; import { useCountdown } from '@/shared/lib/hooks'; -import { ValidationErrors, createTxMetadata, toAddress, upgradeNonce } from '@/shared/lib/utils'; -import { Button, ConfirmModal, Countdown, FootnoteText, SmallTitleText, StatusModal } from '@/shared/ui'; +import { ValidationErrors } from '@/shared/lib/utils'; +import { Button, Countdown, FootnoteText, SmallTitleText, StatusModal } from '@/shared/ui'; import { Animation } from '@/shared/ui/Animation/Animation'; -import { networkModel } from '@/entities/network'; import { transactionService } from '@/entities/transaction'; -import { accountUtils, walletModel, walletUtils } from '@/entities/wallet'; -import { DEFAULT_POLKADOT_METHODS, walletConnectModel, walletConnectUtils } from '@/entities/walletConnect'; +import { accountUtils } from '@/entities/wallet'; import { WalletConnectQrCode } from '@/features/wallet-pairing-wallet-connect'; -import { operationSignUtils } from '../lib/operation-sign-utils'; import { type SigningProps } from '../lib/types'; import { operationSignModel } from '../model/operation-sign-model'; -import { signWcModel } from '../model/sign-wc-model'; +import { walletConnectSign } from '../model/walletConnectSign'; export const WalletConnect = ({ apis, signingPayloads, validateBalance, onGoBack, onResult }: SigningProps) => { + useGate(walletConnectSign.flow, { payloads: signingPayloads }); + const { t } = useI18n(); const [countdown, resetCountdown] = useCountdown(Object.values(apis)); const payload = signingPayloads[0]; - const api = apis[payload.chain.chainId]; - - const wallets = useUnit(walletModel.$wallets); - const session = useUnit(walletConnectModel.$session); - const provider = useUnit(walletConnectModel.$provider); - const reconnectStep = useUnit(signWcModel.$reconnectStep); - const isSigningRejected = useUnit(signWcModel.$isSigningRejected); - const signatures = useUnit(signWcModel.$signatures); - const isStatusShown = useUnit(signWcModel.$isStatusShown); - const uri = useUnit(walletConnectModel.$uri); - const chains = useUnit(networkModel.$chains); + const session = useUnit(walletConnectSign.$session); + const transactions = useUnit(walletConnectSign.$transactions); + const pairingUri = useUnit(walletConnectSign.$pairingUri); + const step = useUnit(walletConnectSign.$step); + const signed = useUnit(walletConnectSign.$signed); - const [txPayloads, setTxPayloads] = useState(); - const [unsignedTxs, setUnsignedTxs] = useState(); const [validationError, setValidationError] = useState(); - const transaction = payload.transaction; const account = payload.signatory || payload.account; + useGate(operationSignModel.SignerGate, account); + if (!accountUtils.isWcAccount(account)) { throw new Error(`Account is not Wallet Connect account, got ${JSON.stringify(account, null, 2)}`); } - useGate(operationSignModel.SignerGate, account); - - useEffect(() => { - if (txPayloads || !provider) return; - - const sessions = provider.client.session.getAll(); - const storedAccount = walletUtils.getAccountsBy(wallets, (a) => a.walletId === account.walletId)[0]; - const storedSession = accountUtils.isWcAccount(storedAccount) - ? sessions.find((s) => s.topic === storedAccount.signingExtras.sessionTopic) - : null; - - if (storedSession) { - walletConnectModel.events.sessionUpdated(storedSession); - - setupTransaction().catch(() => console.warn('WalletConnect | setupTransaction() failed')); - } else { - signWcModel.events.reconnectModalShown(); - } - }, [transaction, api]); - - useEffect(() => { - if (unsignedTxs) { - signTransaction(); - } - }, [unsignedTxs]); - useEffect(() => { if (countdown <= 0) { setValidationError(ValidationErrors.EXPIRED); @@ -81,153 +46,69 @@ export const WalletConnect = ({ apis, signingPayloads, validateBalance, onGoBack }, [countdown]); useEffect(() => { - if (signatures) { - handleSignature(signatures); + if (session) { + resetCountdown(); } - }, [signatures]); - - const setupTransaction = async (): Promise => { - const resultPayloads = []; - const resultUnsignedTxs = []; - - try { - const address = toAddress(account.accountId, { prefix: payload.chain.addressPrefix }); - let metadata = await createTxMetadata(address, apis[payload.chain.chainId]); - - const payloads: ReturnType[] = []; - - for (const { transaction } of signingPayloads) { - const payload = transactionService.createPayloadWithMetadata(transaction, api, metadata); - payloads.push(payload); - - resultPayloads.push(payload.payload); - resultUnsignedTxs.push(payload.unsigned); - - metadata = upgradeNonce(metadata, 1); - } + }, [session]); - setTxPayloads(resultPayloads); - setUnsignedTxs(resultUnsignedTxs); - - transactionService.logPayload(payloads); - - if (payload) { - resetCountdown(); - } - } catch (error) { - console.warn(error); + useEffect(() => { + if (signed.length) { + handleSignature(signed.map((x) => x.signature)); } - }; - - const reconnect = () => { - signWcModel.events.reconnectStarted({ - chains: walletConnectUtils.getWalletConnectChains(Object.values(chains)), - pairing: { topic: account.signingExtras?.pairingTopic }, - }); - }; - - const signTransaction = async () => { - if (!api || !provider || !session || !unsignedTxs) return; - - signWcModel.events.signingStarted( - unsignedTxs.map(({ metadataRpc: _, ...unsigned }) => ({ - provider, - payload: { - // eslint-disable-next-line i18next/no-literal-string - chainId: walletConnectUtils.getWalletConnectChainId(transaction.chainId), - topic: session.topic, - request: { - method: DEFAULT_POLKADOT_METHODS.POLKADOT_SIGN_TRANSACTION, - params: { - address: transaction.address, - transactionPayload: unsigned, - }, - }, - }, - })), - ); - }; + }, [signed]); + // TODO move validation to effector model const handleSignature = async (signatures: HexString[]) => { let isVerified; let balanceValidationError; - for (const [index, signature] of Object.entries(signatures)) { - const txPayload = txPayloads && txPayloads[Number(index)]; + for (const [index, signature] of signatures.entries()) { + const transaction = transactions[index]; isVerified = - txPayload && transactionService.verifySignature(txPayload, signature as HexString, payload.account.accountId); + transaction && + transactionService.verifySignature(transaction.payload, signature as HexString, payload.account.accountId); balanceValidationError = validateBalance && (await validateBalance()); } if (isVerified && balanceValidationError) { setValidationError(balanceValidationError || ValidationErrors.INVALID_SIGNATURE); - } else if (txPayloads) { - onResult(signatures, txPayloads); + } else if (transactions.length) { + onResult( + signatures, + transactions.map((x) => x.payload), + ); } }; const walletName = session?.peer.metadata.name || t('operation.walletConnect.defaultWalletName'); const getStatusProps = () => { - if (operationSignUtils.isReconnectingStep(reconnectStep)) { - return { - title: t('operation.walletConnect.reconnect.reconnecting'), - content: , - onClose: () => { - signWcModel.events.reconnectAborted(); - onGoBack(); - }, - }; - } - - if (operationSignUtils.isConnectedStep(reconnectStep)) { - return { - title: t('operation.walletConnect.reconnect.connected'), - content: , - onClose: () => { - signWcModel.events.reconnectDone(); - setupTransaction(); - }, - }; - } - - if (operationSignUtils.isRejectedStep(reconnectStep)) { + if (step === 'rejected') { return { + isOpen: true, title: t('operation.walletConnect.rejected'), content: , onClose: () => { - signWcModel.events.reconnectAborted(); onGoBack(); }, }; } - if (operationSignUtils.isFailedStep(reconnectStep)) { - return { - title: t('operation.walletConnect.failedTitle'), - description: t('operation.walletConnect.failedDescription'), - content: , - className: 'w-[440px]', - onClose: () => { - signWcModel.events.reconnectAborted(); - onGoBack(); - }, - }; - } - - if (isSigningRejected) { + // TODO fix failed state + if (step === 'failed') { return { + isOpen: true, title: t('operation.walletConnect.rejected'), content: , onClose: () => { - signWcModel.events.reset(); onGoBack(); }, }; } return { + isOpen: false, title: '', content: null, onClose: () => {}, @@ -238,18 +119,22 @@ export const WalletConnect = ({ apis, signingPayloads, validateBalance, onGoBack
      {t('operation.walletConnect.signTitle', { - count: txPayloads?.length || 1, + count: transactions.length || 1, walletName, })} - +
      - + {!pairingUri && ( + + )} + + {pairingUri && } {validationError === ValidationErrors.EXPIRED && ( <> @@ -270,25 +155,7 @@ export const WalletConnect = ({ apis, signingPayloads, validateBalance, onGoBack
      - - - {t('operation.walletConnect.reconnect.title', { - walletName, - })} - - - {t('operation.walletConnect.reconnect.description')} - - - - +
      ); }; diff --git a/src/renderer/features/sign-wallet-connect/index.tsx b/src/renderer/features/sign-wallet-connect/index.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/renderer/features/wallet-details/lib/constants.ts b/src/renderer/features/wallet-details/lib/constants.ts index d0ecfcd08c..279550a2f0 100644 --- a/src/renderer/features/wallet-details/lib/constants.ts +++ b/src/renderer/features/wallet-details/lib/constants.ts @@ -1,9 +1,7 @@ export const enum ReconnectStep { NOT_STARTED, - CONFIRMATION, RECONNECTING, FAILED, - REFRESH_ACCOUNTS, REJECTED, } diff --git a/src/renderer/features/wallet-details/lib/utils.ts b/src/renderer/features/wallet-details/lib/utils.ts index 49fc04cfed..2179cf9ffd 100644 --- a/src/renderer/features/wallet-details/lib/utils.ts +++ b/src/renderer/features/wallet-details/lib/utils.ts @@ -19,9 +19,7 @@ export const wcDetailsUtils = { isReconnecting, isRejected, isReadyToReconnect, - isConfirmation, isFailed, - isRefreshAccounts, }; export const walletDetailsUtils = { @@ -34,11 +32,7 @@ export const walletDetailsUtils = { }; function isNotStarted(step: ReconnectStep, connected: boolean): boolean { - return [ReconnectStep.NOT_STARTED, ReconnectStep.CONFIRMATION].includes(step) && connected; -} - -function isConfirmation(step: ReconnectStep): boolean { - return step === ReconnectStep.CONFIRMATION; + return step === ReconnectStep.NOT_STARTED && connected; } function isReconnecting(step: ReconnectStep): boolean { @@ -53,10 +47,6 @@ function isFailed(step: ReconnectStep): boolean { return step === ReconnectStep.FAILED; } -function isRefreshAccounts(step: ReconnectStep): boolean { - return step === ReconnectStep.REFRESH_ACCOUNTS; -} - function isReadyToReconnect(step: ReconnectStep, connected: boolean): boolean { return isRejected(step) || (step === ReconnectStep.NOT_STARTED && !connected); } diff --git a/src/renderer/features/wallet-details/model/__tests__/vault-details-model.test.ts b/src/renderer/features/wallet-details/model/__tests__/vault-details-model.test.ts index f940fed091..ea9a56956a 100644 --- a/src/renderer/features/wallet-details/model/__tests__/vault-details-model.test.ts +++ b/src/renderer/features/wallet-details/model/__tests__/vault-details-model.test.ts @@ -46,7 +46,7 @@ describe('widgets/WalletDetails/model/vault-details-model', () => { const scope = fork({ values: [[accounts.__test.$list, testAccounts]], - handlers: [[accounts.updateAccount, () => {}]], + handlers: [[accounts.createAccounts, () => {}]], }); await allSettled(vaultDetailsModel.events.keysRemoved, { scope, params: [testAccounts[0]] }); diff --git a/src/renderer/features/wallet-details/model/walletConnectForgot.ts b/src/renderer/features/wallet-details/model/walletConnectForgot.ts new file mode 100644 index 0000000000..74d1b2e1aa --- /dev/null +++ b/src/renderer/features/wallet-details/model/walletConnectForgot.ts @@ -0,0 +1,76 @@ +import { createEvent, createStore, sample } from 'effector'; +import { createGate } from 'effector-react'; + +import { type Wallet } from '@/shared/core'; +import { type AnyAccount } from '@/domains/network'; +import { balanceModel } from '@/entities/balance'; +import { accountUtils, walletModel, walletUtils } from '@/entities/wallet'; +import { walletConnect } from '@/features/wallet-wallet-connect'; +import { ForgetStep } from '../lib/constants'; + +const flow = createGate<{ accounts: AnyAccount[] }>({ defaultState: { accounts: [] } }); + +const forget = createEvent(); +const abort = createEvent(); + +const $forgetStep = createStore(ForgetStep.NOT_STARTED).reset(flow.close); + +sample({ + clock: forget, + source: walletModel.$wallets, + fn: (wallets, wallet) => { + const accounts = walletUtils.getAccountsBy(wallets, account => account.walletId === wallet.id); + + return accounts.map(account => account.accountId); + }, + target: balanceModel.events.balancesRemoved, +}); + +// TODO simplify +sample({ + clock: forget, + fn: wallet => { + const account = wallet.accounts.at(0); + if (!account || !accountUtils.isWcAccount(account)) { + throw new Error('Not Wallet Connect account.'); + } + + return { + pairingTopic: account.signingExtras.pairingTopic ?? '', + }; + }, + target: [walletConnect.removePairing, walletConnect.removeSession], +}); + +sample({ + clock: forget, + fn: () => ForgetStep.FORGETTING, + target: $forgetStep, +}); + +sample({ + clock: [flow.close, abort], + fn: () => ForgetStep.NOT_STARTED, + target: $forgetStep, +}); + +sample({ + clock: forget, + fn: wallet => wallet.accounts[0]?.walletId, + target: walletModel.events.walletRemoved, +}); + +sample({ + clock: walletModel.events.walletRemovedSuccess, + source: { forgetStep: $forgetStep }, + filter: ({ forgetStep }) => forgetStep !== ForgetStep.NOT_STARTED, + fn: () => ForgetStep.SUCCESS, + target: $forgetStep, +}); + +export const walletConnectForget = { + $forgetStep, + forget, + flow, + abort, +}; diff --git a/src/renderer/features/wallet-details/model/walletConnectReconnect.ts b/src/renderer/features/wallet-details/model/walletConnectReconnect.ts new file mode 100644 index 0000000000..5b1765cbdb --- /dev/null +++ b/src/renderer/features/wallet-details/model/walletConnectReconnect.ts @@ -0,0 +1,99 @@ +import { attach, combine, createEvent, createStore, sample } from 'effector'; +import { createGate } from 'effector-react'; + +import { type WcAccount } from '@/shared/core'; +import { type AnyAccount, accounts } from '@/domains/network'; +import { networkModel } from '@/entities/network'; +import { walletConnect, walletConnectService } from '@/features/wallet-wallet-connect'; +import { ReconnectStep } from '../lib/constants'; + +const flow = createGate<{ accounts: AnyAccount[] }>({ defaultState: { accounts: [] } }); +const $accounts = flow.state.map(({ accounts }) => accounts); +const start = createEvent(); +const abort = createEvent(); + +const $connected = combine($accounts, walletConnect.$sessions, (accounts, sessions) => { + return walletConnectService.areAccountsConnected( + sessions, + accounts.filter(walletConnectService.isWalletConnectAccount), + ); +}); + +const updateSessionFx = attach({ + source: { accounts: $accounts, chains: networkModel.$chains }, + mapParams(_, { accounts, chains }) { + const account = accounts + .filter(walletConnectService.isWalletConnectAccount) + .find(a => a.signingExtras.pairingTopic); + const pairingTopic = account?.signingExtras?.pairingTopic; + + return { + pairingTopic, + chains: Object.values(chains).map(c => c.chainId), + }; + }, + effect: walletConnect.restoreSession, +}); + +const $reconnectStep = createStore(ReconnectStep.NOT_STARTED).reset(flow.close); + +sample({ + clock: start, + fn: () => ReconnectStep.RECONNECTING, + target: [$reconnectStep, updateSessionFx], +}); + +sample({ + clock: updateSessionFx.done, + source: { + accounts: $accounts, + chains: networkModel.$chains, + }, + fn: ({ accounts, chains }, { result: session }) => { + const wcAccounts = accounts.filter(walletConnectService.isWalletConnectAccount); + const accountsToUpdate = walletConnectService.getAccountsFromSession(session, Object.values(chains)); + const updates: WcAccount[] = []; + + for (const { accountId, chain } of accountsToUpdate) { + const account = wcAccounts.find(a => a.accountId === accountId && a.chainId === chain.chainId); + + if (account) { + updates.push(walletConnectService.updateAccount(account, session)); + } + } + + return updates; + }, + target: accounts.updateAccounts, +}); + +sample({ + clock: updateSessionFx.done, + source: $reconnectStep, + fn: () => ReconnectStep.NOT_STARTED, + target: $reconnectStep, +}); + +sample({ + clock: updateSessionFx.fail, + source: $reconnectStep, + filter: step => step === ReconnectStep.RECONNECTING, + fn: () => ReconnectStep.REJECTED, + target: $reconnectStep, +}); + +sample({ + clock: abort, + fn: () => ReconnectStep.NOT_STARTED, + target: $reconnectStep, +}); + +export const walletConnectReconnect = { + $accounts, + $connected, + $reconnectStep, + $reconnectUri: walletConnect.$pairingUri, + start, + abort, + flow, +}; diff --git a/src/renderer/features/wallet-details/model/wc-details-model.ts b/src/renderer/features/wallet-details/model/wc-details-model.ts deleted file mode 100644 index f294d799cc..0000000000 --- a/src/renderer/features/wallet-details/model/wc-details-model.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { createEvent, createStore, sample } from 'effector'; -import { createGate } from 'effector-react'; -import { combineEvents, spread } from 'patronum'; - -import { type ChainId, type Wallet, type WcAccount } from '@/shared/core'; -import { nonNullable, toAccountId } from '@/shared/lib/utils'; -import { balanceModel } from '@/entities/balance'; -import { networkModel } from '@/entities/network'; -import { accountUtils, walletModel, walletUtils } from '@/entities/wallet'; -import { type InitConnectParams, walletConnectModel, walletConnectUtils } from '@/entities/walletConnect'; -import { ForgetStep, ReconnectStep } from '../lib/constants'; - -const walletConnectDetailsFlow = createGate<{ wallet: Wallet | null }>({ defaultState: { wallet: null } }); - -const $wallet = walletConnectDetailsFlow.state.map(({ wallet }) => wallet); - -const reset = createEvent(); -const confirmReconnectShown = createEvent(); -const reconnectStarted = createEvent & { currentSession: string }>(); -const reconnectAborted = createEvent(); -const sessionTopicUpdated = createEvent(); -const forgetButtonClicked = createEvent(); -const forgetModalClosed = createEvent(); - -const $reconnectStep = createStore(ReconnectStep.NOT_STARTED).reset(reset); -const $forgetStep = createStore(ForgetStep.NOT_STARTED).reset(reset); - -sample({ - clock: forgetButtonClicked, - source: walletModel.$wallets, - fn: (wallets, wallet) => { - const accounts = walletUtils.getAccountsBy(wallets, account => account.walletId === wallet.id); - - return accounts.map(account => account.accountId); - }, - target: balanceModel.events.balancesRemoved, -}); - -sample({ - clock: confirmReconnectShown, - fn: () => ReconnectStep.CONFIRMATION, - target: $reconnectStep, -}); - -sample({ - clock: reconnectStarted, - fn: () => ReconnectStep.RECONNECTING, - target: $reconnectStep, -}); - -sample({ - clock: reconnectStarted, - target: walletConnectModel.events.connect, -}); - -sample({ - clock: walletConnectModel.events.connected, - source: { - step: $reconnectStep, - wallet: $wallet, - session: walletConnectModel.$session, - }, - filter: ({ step, wallet, session }) => { - const correctStep = step === ReconnectStep.RECONNECTING || step === ReconnectStep.REFRESH_ACCOUNTS; - - return correctStep && nonNullable(wallet) && nonNullable(session?.topic); - }, - fn: ({ wallet, session }) => ({ - walletId: wallet!.id, - accounts: wallet!.accounts, - topic: session!.topic, - }), - target: walletConnectModel.events.sessionTopicUpdated, -}); - -sample({ - clock: combineEvents({ - events: [walletConnectModel.events.sessionTopicUpdateDone], - reset: reconnectStarted, - }), - source: { - wallet: $wallet, - newAccounts: walletConnectModel.$accounts, - chains: networkModel.$chains, - }, - filter: ({ wallet }) => nonNullable(wallet), - fn: ({ wallet, newAccounts, chains }) => { - const updatedAccounts: WcAccount[] = []; - const chainIds = Object.keys(chains); - - for (const newAccount of newAccounts) { - const [_, chainId, address] = newAccount.split(':'); - - const fullChainId = chainIds.find(chain => chain.includes(chainId)); - const chain = fullChainId && chains[fullChainId as ChainId]; - if (!chain) continue; - - const account = wallet!.accounts.at(0); - if (!account || !accountUtils.isWcAccount(account)) continue; - - updatedAccounts.push({ - ...account, - chainId: chain.chainId, - accountId: toAccountId(address), - signingExtras: account.signingExtras || {}, - }); - } - - return { walletId: wallet!.id, accounts: updatedAccounts }; - }, - target: walletConnectModel.events.accountsUpdated, -}); - -sample({ - clock: walletConnectModel.events.connectionRejected, - source: $reconnectStep, - filter: step => step === ReconnectStep.RECONNECTING, - fn: () => ReconnectStep.REJECTED, - target: $reconnectStep, -}); - -sample({ - clock: [walletConnectModel.events.initConnectFailed, walletConnectModel.events.sessionTopicUpdateFailed], - source: $reconnectStep, - fn: () => ReconnectStep.REFRESH_ACCOUNTS, - target: $reconnectStep, -}); - -sample({ - clock: [walletConnectModel.events.initConnectFailed, walletConnectModel.events.sessionTopicUpdateFailed], - source: networkModel.$chains, - fn: chains => ({ chains: walletConnectUtils.getWalletConnectChains(Object.values(chains)) }), - target: walletConnectModel.events.connect, -}); - -sample({ - clock: [walletConnectModel.events.accountsUpdateDone, reconnectAborted], - fn: () => ReconnectStep.NOT_STARTED, - target: $reconnectStep, -}); - -sample({ - clock: forgetButtonClicked, - source: $wallet, - filter: nonNullable, - fn: wallet => { - const account = wallet!.accounts.at(0); - if (!account || !accountUtils.isWcAccount(account)) { - throw new Error('Not Wallet Connect account.'); - } - - return { - sessionTopic: account.signingExtras.sessionTopic ?? '', - pairingTopic: account.signingExtras.pairingTopic ?? '', - }; - }, - target: spread({ - sessionTopic: walletConnectModel.events.disconnectStarted, - pairingTopic: walletConnectModel.events.pairingRemoved, - }), -}); - -sample({ - clock: forgetButtonClicked, - fn: () => ForgetStep.FORGETTING, - target: $forgetStep, -}); - -sample({ - clock: forgetButtonClicked, - source: $wallet, - filter: nonNullable, - fn: wallet => wallet!.id, - target: walletModel.events.walletRemoved, -}); - -sample({ - clock: walletModel.events.walletRemovedSuccess, - source: { forgetStep: $forgetStep }, - filter: ({ forgetStep }) => forgetStep !== ForgetStep.NOT_STARTED, - fn: () => ForgetStep.SUCCESS, - target: $forgetStep, -}); - -export const wcDetailsModel = { - $reconnectStep, - $forgetStep, - events: { - reset, - confirmReconnectShown, - reconnectStarted, - reconnectAborted, - sessionTopicUpdated, - forgetButtonClicked, - forgetModalClosed, - }, - walletConnectDetailsFlow, -}; diff --git a/src/renderer/features/wallet-details/ui/components/WalletConnectAccounts.tsx b/src/renderer/features/wallet-details/ui/components/WalletConnectAccounts.tsx index 3aabcafbb8..8bb163f32e 100644 --- a/src/renderer/features/wallet-details/ui/components/WalletConnectAccounts.tsx +++ b/src/renderer/features/wallet-details/ui/components/WalletConnectAccounts.tsx @@ -10,10 +10,9 @@ import { type AccountId } from '@/shared/polkadotjs-schemas'; import { Button, FootnoteText, Icon, SmallTitleText } from '@/shared/ui'; import { ChainAccountsList } from '@/shared/ui-entities'; import { networkModel } from '@/entities/network'; -import { walletConnectModel } from '@/entities/walletConnect'; import { WalletConnectQrCode } from '@/features/wallet-pairing-wallet-connect'; import { wcDetailsUtils } from '../../lib/utils'; -import { wcDetailsModel } from '../../model/wc-details-model'; +import { walletConnectReconnect } from '../../model/walletConnectReconnect'; type AccountItem = [chain: Chain, accountId: AccountId]; @@ -25,9 +24,9 @@ export const WalletConnectAccounts = memo(({ wallet }: Props) => { const { t } = useI18n(); const chains = Object.values(useUnit(networkModel.$chains)); - const reconnectStep = useUnit(wcDetailsModel.$reconnectStep); - - const uri = useUnit(walletConnectModel.$uri); + const connected = useUnit(walletConnectReconnect.$connected); + const reconnectStep = useUnit(walletConnectReconnect.$reconnectStep); + const reconnectUri = useUnit(walletConnectReconnect.$reconnectUri); const accountsList = useMemo(() => { const accountsMap = keyBy(wallet.accounts, 'chainId'); @@ -45,22 +44,22 @@ export const WalletConnectAccounts = memo(({ wallet }: Props) => { return ( <> - {wcDetailsUtils.isNotStarted(reconnectStep, wallet.isConnected) && } + {wcDetailsUtils.isNotStarted(reconnectStep, connected) && } - {wcDetailsUtils.isReadyToReconnect(reconnectStep, wallet.isConnected) && ( + {wcDetailsUtils.isReadyToReconnect(reconnectStep, connected) && (
      {t('walletDetails.walletConnect.disconnectedTitle')} {t('walletDetails.walletConnect.disconnectedDescription')} -
      )} - {wcDetailsUtils.isReconnecting(reconnectStep) && ( + {wcDetailsUtils.isReconnecting(reconnectStep) && !reconnectUri && (
      )} - {wcDetailsUtils.isRefreshAccounts(reconnectStep) && } + {wcDetailsUtils.isReconnecting(reconnectStep) && !!reconnectUri && ( +
      + +
      + )} ); }); diff --git a/src/renderer/features/wallet-details/ui/wallets/WalletConnectDetails.tsx b/src/renderer/features/wallet-details/ui/wallets/WalletConnectDetails.tsx index 130cc48dbe..d7f786e929 100644 --- a/src/renderer/features/wallet-details/ui/wallets/WalletConnectDetails.tsx +++ b/src/renderer/features/wallet-details/ui/wallets/WalletConnectDetails.tsx @@ -1,16 +1,23 @@ import { useGate, useUnit } from 'effector-react'; -import { useEffect, useState, useTransition } from 'react'; +import { useState, useTransition } from 'react'; -import { chainsService } from '@/shared/api/network'; import { type WalletConnectGroup } from '@/shared/core'; import { useI18n } from '@/shared/i18n'; import { useModalClose, useToggle } from '@/shared/lib/hooks'; -import { Button, ConfirmModal, FootnoteText, Icon, IconButton, SmallTitleText, StatusModal } from '@/shared/ui'; +import { + Button, + ConfirmModal, + FootnoteText, + Icon, + IconButton, + SmallTitleText, + StatusLabel, + StatusModal, +} from '@/shared/ui'; import { Animation } from '@/shared/ui/Animation/Animation'; import { type IconNames } from '@/shared/ui/Icon/data'; import { Dropdown, Modal, Tabs } from '@/shared/ui-kit'; import { WalletCardLg, permissionUtils } from '@/entities/wallet'; -import { walletConnectUtils } from '@/entities/walletConnect'; import { proxyAddFeature } from '@/features/proxy-add'; import { proxyAddPureFeature } from '@/features/proxy-add-pure'; import { forgetWalletModel } from '@/features/wallets/ForgetWallet'; @@ -18,7 +25,8 @@ import { RenameWalletModal } from '@/features/wallets/RenameWallet'; import { ForgetStep } from '../../lib/constants'; import { walletDetailsUtils, wcDetailsUtils } from '../../lib/utils'; import { walletDetailsModel } from '../../model/wallet-details-model'; -import { wcDetailsModel } from '../../model/wc-details-model'; +import { walletConnectForget } from '../../model/walletConnectForgot'; +import { walletConnectReconnect } from '../../model/walletConnectReconnect'; import { NoProxiesAction } from '../components/NoProxiesAction'; import { ProxiesList } from '../components/ProxiesList'; import { WalletConnectAccounts } from '../components/WalletConnectAccounts'; @@ -38,34 +46,25 @@ type Props = { onClose: () => void; }; export const WalletConnectDetails = ({ wallet, onClose }: Props) => { - useGate(wcDetailsModel.walletConnectDetailsFlow, { wallet }); + useGate(walletConnectForget.flow, { accounts: wallet.accounts }); + useGate(walletConnectReconnect.flow, { accounts: wallet.accounts }); const { t } = useI18n(); const hasProxies = useUnit(walletDetailsModel.$hasProxies); - const forgetStep = useUnit(wcDetailsModel.$forgetStep); - const reconnectStep = useUnit(wcDetailsModel.$reconnectStep); + const connected = useUnit(walletConnectReconnect.$connected); + const forgetStep = useUnit(walletConnectForget.$forgetStep); + const reconnectStep = useUnit(walletConnectReconnect.$reconnectStep); const canCreateProxy = useUnit(walletDetailsModel.$canCreateProxy); const [_, startTransition] = useTransition(); const [tab, setTab] = useState('accounts'); const [isModalOpen, closeModal] = useModalClose(true, onClose); + const [isConfirmReconnectOpen, toggleConfirmReconnect] = useToggle(); const [isConfirmForgetOpen, toggleConfirmForget] = useToggle(); const [isRenameModalOpen, toggleIsRenameModalOpen] = useToggle(); - useEffect(() => { - wcDetailsModel.events.reset(); - }, []); - - const reconnect = () => { - wcDetailsModel.events.reconnectStarted({ - chains: walletConnectUtils.getWalletConnectChains(chainsService.getChainsData()), - pairing: { topic: wallet.accounts[0].signingExtras?.pairingTopic }, - currentSession: wallet.accounts[0].signingExtras?.sessionTopic, - }); - }; - const handleForgetWallet = () => { - wcDetailsModel.events.forgetButtonClicked(wallet); + walletConnectForget.forget(wallet); forgetWalletModel.events.forgetWcWallet(wallet); toggleConfirmForget(); }; @@ -84,7 +83,7 @@ export const WalletConnectDetails = ({ wallet, onClose }: Props) => { { icon: 'refresh' as IconNames, title: t('walletDetails.walletConnect.refreshButton'), - onClick: wcDetailsModel.events.confirmReconnectShown, + onClick: walletConnectReconnect.start, }, ]; @@ -134,7 +133,13 @@ export const WalletConnectDetails = ({ wallet, onClose }: Props) => {
      - + + +
      @@ -165,11 +170,17 @@ export const WalletConnectDetails = ({ wallet, onClose }: Props) => { { + toggleConfirmReconnect(); + walletConnectReconnect.start(); + }} + onClose={() => { + toggleConfirmReconnect(); + walletConnectReconnect.abort(); + }} > {t('walletDetails.walletConnect.reconnectConfirmTitle')} @@ -206,7 +217,7 @@ export const WalletConnectDetails = ({ wallet, onClose }: Props) => { content={ forgetStep === ForgetStep.FORGETTING ? : } - onClose={wcDetailsModel.events.forgetModalClosed} + onClose={walletConnectForget.abort} /> { title={t('walletDetails.walletConnect.rejectTitle')} description={t('walletDetails.walletConnect.rejectDescription')} content={} - onClose={wcDetailsModel.events.reconnectAborted} + onClose={walletConnectReconnect.abort} > - diff --git a/src/renderer/features/wallet-multisig/index.tsx b/src/renderer/features/wallet-multisig/index.tsx index 34b2a54636..3332a72ced 100644 --- a/src/renderer/features/wallet-multisig/index.tsx +++ b/src/renderer/features/wallet-multisig/index.tsx @@ -1,12 +1,12 @@ import { useUnit } from 'effector-react'; import { $features } from '@/shared/config/features'; -import { WalletType } from '@/shared/core'; +import { WalletIconType, WalletType } from '@/shared/core'; import { createFeature } from '@/shared/feature'; import { useI18n } from '@/shared/i18n'; import { accountsService } from '@/domains/network'; -import { accountUtils } from '@/entities/wallet'; -import { walletGroupSlot } from '@/features/wallet-select'; +import { WalletIcon, accountUtils, walletUtils } from '@/entities/wallet'; +import { walletGroupSlot, walletIconSlot } from '@/features/wallet-select'; import { WalletGroup, walletActionsSlot } from './components/WalletGroup'; import { walletsModel } from './model/wallets'; @@ -23,8 +23,19 @@ walletMultisigFeature.inject(accountsService.accountActionPermissionAnyOf, ({ ac return accountUtils.isMultisigAccount(account); }); +walletMultisigFeature.inject(walletIconSlot, ({ wallet, size }) => { + if (!walletUtils.isMultisig(wallet)) return null; + + const type = + walletUtils.isFlexibleMultisig(wallet) && !wallet.activated + ? WalletIconType.FLEXIBLE_MULTISIG_INACTIVE + : wallet.type; + + return ; +}); + walletMultisigFeature.inject(walletGroupSlot, { - order: 1, + order: 3, render({ query, onSelect }) { const { t } = useI18n(); const regular = useUnit(walletsModel.$regularMultisig); diff --git a/src/renderer/features/wallet-pairing-wallet-connect/components/ManageStep.tsx b/src/renderer/features/wallet-pairing-wallet-connect/components/ManageStep.tsx index 4225a31c6a..7ea40531a5 100644 --- a/src/renderer/features/wallet-pairing-wallet-connect/components/ManageStep.tsx +++ b/src/renderer/features/wallet-pairing-wallet-connect/components/ManageStep.tsx @@ -1,25 +1,15 @@ -import { useEffect, useState } from 'react'; +import { useUnit } from 'effector-react'; import { Controller, type SubmitHandler, useForm } from 'react-hook-form'; import { useStatusContext } from '@/app/providers'; -import { chainsService } from '@/shared/api/network'; -import { - AccountType, - type Chain, - CryptoType, - ErrorType, - type NoID, - SigningType, - WalletType, - type WcAccount, -} from '@/shared/core'; +import { ErrorType, WalletType } from '@/shared/core'; import { useI18n } from '@/shared/i18n'; -import { toAccountId } from '@/shared/lib/utils'; -import { type AccountId } from '@/shared/polkadotjs-schemas'; -import { Button, Icon, InputHint, SmallTitleText } from '@/shared/ui'; +import { nullable } from '@/shared/lib/utils'; +import { Button, Icon, InputHint, Loader, SmallTitleText } from '@/shared/ui'; import { type IconNames } from '@/shared/ui/Icon/data'; -import { Field, Input, Modal } from '@/shared/ui-kit'; -import { MultiAccountsList, walletModel } from '@/entities/wallet'; +import { Box, Field, Input, Modal } from '@/shared/ui-kit'; +import { MultiAccountsList } from '@/entities/wallet'; +import { pairingForm } from '../model/pairingForm'; const WalletLogo: Record = { [WalletType.WALLET_CONNECT]: 'walletConnectOnboarding', @@ -33,20 +23,25 @@ type WalletForm = { type WalletTypeName = WalletType.NOVA_WALLET | WalletType.WALLET_CONNECT; type Props = { - accounts: string[]; - pairingTopic: string; - sessionTopic: string; type: WalletTypeName; onBack: () => void; onComplete: () => void; }; -export const ManageStep = ({ accounts, type, pairingTopic, sessionTopic, onBack, onComplete }: Props) => { +export const ManageStep = ({ type, onBack, onComplete }: Props) => { const { t } = useI18n(); const { showStatus } = useStatusContext(); - const [chains, setChains] = useState([]); - const [accountsList, setAccountsList] = useState<{ chain: Chain; accountId: AccountId }[]>([]); + const session = useUnit(pairingForm.$session); + const accounts = useUnit(pairingForm.$accounts); + + if (nullable(session)) { + return ( + + + + ); + } const { handleSubmit, @@ -58,67 +53,11 @@ export const ManageStep = ({ accounts, type, pairingTopic, sessionTopic, onBack, defaultValues: { walletName: '' }, }); - useEffect(() => { - setChains(chainsService.getChainsData({ sort: true })); - }, []); - - useEffect(() => { - const list = chains.reduce<{ chain: Chain; accountId: AccountId }[]>((acc, chain) => { - const account = accounts.find(account => { - const [_, chainId] = account.split(':'); - - return chain.chainId.includes(chainId); - }); - - const [_, _chainId, address] = account?.split(':') || []; - - if (address) { - const accountId = toAccountId(address); - - acc.push({ - chain, - accountId, - }); - } - - return acc; - }, []); - - setAccountsList(list.filter(Boolean)); - }, [chains.length]); - // TODO: Rewrite with effector forms const submitHandler: SubmitHandler = async ({ walletName }) => { - const wcAccounts = accounts.map(account => { - const [_, chainId, address] = account.split(':'); - const chain = chains.find(chain => chain.chainId.includes(chainId)); - - return { - type: 'chain', - name: walletName.trim(), - accountId: toAccountId(address), - accountType: AccountType.WALLET_CONNECT, - signingType: SigningType.WALLET_CONNECT, - // TODO check - cryptoType: CryptoType.SR25519, - // TODO and if it's ommited? - chainId: chain!.chainId, - signingExtras: { pairingTopic, sessionTopic }, - } satisfies Omit, 'walletId'>; - }); - - walletModel.events.walletConnectCreated({ - external: false, - wallet: { - name: walletName.trim(), - type, - signingType: SigningType.WALLET_CONNECT, - }, - accounts: wcAccounts, - }); + pairingForm.createWallet({ name: walletName }); reset(); - showStatus({ title: walletName.trim(), description: t('onboarding.walletConnect.pairedDescription'), @@ -191,7 +130,7 @@ export const ManageStep = ({ accounts, type, pairingTopic, sessionTopic, onBack,
      {t('onboarding.vault.accountsTitle')} - +
); diff --git a/src/renderer/features/wallet-pairing-wallet-connect/components/PairingModal.tsx b/src/renderer/features/wallet-pairing-wallet-connect/components/PairingModal.tsx index 5968f269aa..7e483e568e 100644 --- a/src/renderer/features/wallet-pairing-wallet-connect/components/PairingModal.tsx +++ b/src/renderer/features/wallet-pairing-wallet-connect/components/PairingModal.tsx @@ -1,13 +1,11 @@ import { useUnit } from 'effector-react'; import { type PropsWithChildren, memo, useEffect } from 'react'; -import { useStatusContext } from '@/app/providers'; import { WalletType } from '@/shared/core'; import { useI18n } from '@/shared/i18n'; -import { Button, HeaderTitleText, SmallTitleText } from '@/shared/ui'; +import { Button, HeaderTitleText, SmallTitleText, StatusModal } from '@/shared/ui'; import { Animation } from '@/shared/ui/Animation/Animation'; import { Carousel, Modal } from '@/shared/ui-kit'; -import { walletConnectModel } from '@/entities/walletConnect'; import { EXPIRE_TIMEOUT, Step } from '../lib/constants'; import { pairingForm } from '../model/pairingForm'; @@ -23,13 +21,11 @@ type Props = PropsWithChildren<{ export const PairingModal = memo(({ variant, children }: Props) => { const { t } = useI18n(); - const session = useUnit(walletConnectModel.$session); - const uri = useUnit(walletConnectModel.$uri); + const session = useUnit(pairingForm.$session); + const uri = useUnit(pairingForm.$uri); const step = useUnit(pairingForm.$step); const open = useUnit(pairingForm.flow.state) === variant; - const { showStatus } = useStatusContext(); - useEffect(() => { if (!open) return; @@ -38,16 +34,6 @@ export const PairingModal = memo(({ variant, children }: Props) => { return () => clearTimeout(timeout); }, [open]); - useEffect(() => { - if (step === Step.REJECT) { - showStatus({ - title: t('onboarding.walletConnect.rejected'), - content: , - }); - toggleModal(false); - } - }, [step]); - const goToScan = () => { pairingForm.reset(); pairingForm.flow.open(variant); @@ -57,7 +43,6 @@ export const PairingModal = memo(({ variant, children }: Props) => { if (open) { pairingForm.flow.open(variant); } else { - pairingForm.reset(); pairingForm.flow.close(null); } }; @@ -66,6 +51,17 @@ export const PairingModal = memo(({ variant, children }: Props) => { const scanTitle = variant === 'novawallet' ? t('onboarding.novaWallet.scanTitle') : t('onboarding.walletConnect.scanTitle'); + if (step === Step.REJECT) { + return ( + } + title={t('onboarding.walletConnect.rejected')} + onClose={() => toggleModal(false)} + /> + ); + } + return ( {children} @@ -77,7 +73,9 @@ export const PairingModal = memo(({ variant, children }: Props) => { {header} {scanTitle} - +
+ +
diff --git a/src/renderer/features/wallet-select/index.ts b/src/renderer/features/wallet-select/index.ts index 8ec427feec..bfe6bdc635 100644 --- a/src/renderer/features/wallet-select/index.ts +++ b/src/renderer/features/wallet-select/index.ts @@ -1,10 +1,10 @@ import { GROUP_LABELS, WalletGroup } from './components/WalletGroup'; -import { walletGroupSlot, walletSelectActionsSlot } from './components/WalletSelect'; +import { walletGroupSlot, walletIconSlot, walletSelectActionsSlot } from './components/WalletSelect'; import { walletSelectFeatureStatus } from './model/feature'; import { walletSelectModel } from './model/wallet-select-model'; import { walletSelectService } from './service/walletSelectService'; -export { walletSelectActionsSlot, walletGroupSlot }; +export { walletSelectActionsSlot, walletGroupSlot, walletIconSlot }; // TODO remove this mess export const walletSelectFeature = { diff --git a/src/renderer/features/wallet-select/service/walletSelectService.ts b/src/renderer/features/wallet-select/service/walletSelectService.ts index dbbef4cdc3..ce047cda9f 100644 --- a/src/renderer/features/wallet-select/service/walletSelectService.ts +++ b/src/renderer/features/wallet-select/service/walletSelectService.ts @@ -33,7 +33,9 @@ const getWalletByGroups = (wallets: Wallet[], query = ''): Record { - return getWalletByGroups(wallets)[WalletType.POLKADOT_VAULT].at(0) ?? null; + const groups = Object.values(getWalletByGroups(wallets)); + + return groups.find(g => g.length > 0)?.at(0) ?? null; }; export const walletSelectService = { diff --git a/src/renderer/features/wallet-wallet-connect/components/WalletGroup.tsx b/src/renderer/features/wallet-wallet-connect/components/WalletGroup.tsx index 9fd8f42f65..d3fcdd37af 100644 --- a/src/renderer/features/wallet-wallet-connect/components/WalletGroup.tsx +++ b/src/renderer/features/wallet-wallet-connect/components/WalletGroup.tsx @@ -1,12 +1,15 @@ +import { useUnit } from 'effector-react'; import { memo } from 'react'; import { type Wallet, type WalletType } from '@/shared/core'; import { Slot, createSlot } from '@/shared/di'; -import { performSearch } from '@/shared/lib/utils'; +import { cnTw, performSearch } from '@/shared/lib/utils'; import { Icon } from '@/shared/ui'; import { Accordion, Box } from '@/shared/ui-kit'; import { WalletCardMd, WalletIcon } from '@/entities/wallet'; import { walletsFiatBalanceFeature } from '@/features/wallet-fiat-balance'; +import { walletConnectService } from '../lib/service'; +import { walletConnect } from '../model/connect'; // TODO invert this dependency const { @@ -24,6 +27,8 @@ type Props = { }; export const WalletGroup = memo(({ wallets, walletType, query, title, onSelect }: Props) => { + const sessions = useUnit(walletConnect.$sessions); + const filteredWallets = performSearch({ query, records: wallets, @@ -44,26 +49,36 @@ export const WalletGroup = memo(({ wallets, walletType, query, title, onSelect } - {filteredWallets.map(wallet => ( - - } - prefix={ - wallet.isActive ? ( - - ) : ( -
- ) - } - onClick={() => onSelect(wallet)} - > - - - ))} + {filteredWallets.map(wallet => { + const accounts = wallet.accounts.filter(walletConnectService.isWalletConnectAccount); + const connected = walletConnectService.areAccountsConnected(sessions, accounts); + + return ( + + } + prefix={ + wallet.isActive ? ( + + ) : ( +
+ ) + } + meta={ + + } + onClick={() => onSelect(wallet)} + > + + + ); + })} diff --git a/src/renderer/features/wallet-wallet-connect/components/WalletIcon.tsx b/src/renderer/features/wallet-wallet-connect/components/WalletIcon.tsx new file mode 100644 index 0000000000..c3709af2c2 --- /dev/null +++ b/src/renderer/features/wallet-wallet-connect/components/WalletIcon.tsx @@ -0,0 +1,29 @@ +import { useUnit } from 'effector-react'; + +import { type Wallet } from '@/shared/core'; +import { cnTw } from '@/shared/lib/utils'; +import { WalletIcon as Icon } from '@/entities/wallet'; +import { walletConnectService } from '../lib/service'; +import { walletConnect } from '../model/connect'; + +type Props = { + wallet: Wallet; + size: number; +}; + +export const WalletIcon = ({ wallet, size }: Props) => { + const sessions = useUnit(walletConnect.$sessions); + const connected = walletConnectService.areAccountsConnected(sessions, wallet.accounts); + + return ( +
+ + +
+ ); +}; diff --git a/src/renderer/features/wallet-wallet-connect/index.tsx b/src/renderer/features/wallet-wallet-connect/index.tsx index b6bb47eb8b..3767e45dfb 100644 --- a/src/renderer/features/wallet-wallet-connect/index.tsx +++ b/src/renderer/features/wallet-wallet-connect/index.tsx @@ -1,33 +1,32 @@ import { useUnit } from 'effector-react'; -import { $features } from '@/shared/config/features'; import { WalletType } from '@/shared/core'; -import { createFeature } from '@/shared/feature'; import { useI18n } from '@/shared/i18n'; import { accountsService } from '@/domains/network'; -import { accountUtils } from '@/entities/wallet'; -import { walletGroupSlot } from '@/features/wallet-select'; +import { accountUtils, walletUtils } from '@/entities/wallet'; +import { walletGroupSlot, walletIconSlot } from '@/features/wallet-select'; -import { WalletGroup, walletActionsSlot } from './components/WalletGroup'; -import { walletsModel } from './model/wallets'; - -export { walletActionsSlot }; - -export const walletWalletConnectFeature = createFeature({ - name: 'wallet/wallet connect', - enable: $features.map(f => f.walletConnect), -}); +import { WalletGroup } from './components/WalletGroup'; +import { WalletIcon } from './components/WalletIcon'; +import { walletWalletConnectFeature } from './model/feature'; +import { wcWallets } from './model/wallets'; walletWalletConnectFeature.inject(accountsService.accountActionPermissionAnyOf, ({ account }) => { return accountUtils.isWcAccount(account); }); +walletWalletConnectFeature.inject(walletIconSlot, ({ wallet, size }) => { + if (!walletUtils.isWalletConnectGroup(wallet)) return null; + + return ; +}); + walletWalletConnectFeature.inject(walletGroupSlot, { - order: 2, + order: 1, render({ query, onSelect }) { const { t } = useI18n(); - const nova = useUnit(walletsModel.$novaWallets); - const wc = useUnit(walletsModel.$walletConnectWallets); + const nova = useUnit(wcWallets.$novaWallets); + const wc = useUnit(wcWallets.$walletConnectWallets); return ( <> @@ -49,3 +48,10 @@ walletWalletConnectFeature.inject(walletGroupSlot, { ); }, }); + +export { walletWalletConnectFeature } from './model/feature'; +export { WalletGroup, walletActionsSlot } from './components/WalletGroup'; +export { type InitConnectParams, type InitReconnectParams } from './lib/types'; +export { DEFAULT_POLKADOT_METHODS } from './lib/constants'; +export { walletConnectService } from './lib/service'; +export { walletConnect } from './model/connect'; diff --git a/src/renderer/entities/walletConnect/lib/constants.ts b/src/renderer/features/wallet-wallet-connect/lib/constants.ts similarity index 92% rename from src/renderer/entities/walletConnect/lib/constants.ts rename to src/renderer/features/wallet-wallet-connect/lib/constants.ts index 2b284f9365..6802e3a089 100644 --- a/src/renderer/entities/walletConnect/lib/constants.ts +++ b/src/renderer/features/wallet-wallet-connect/lib/constants.ts @@ -6,7 +6,7 @@ export const DEFAULT_LOGGER = 'error'; export const DEFAULT_APP_METADATA = { name: 'Nova Spektr', //dApp name description: 'Full-spectrum Polkadot Desktop Wallet', //dApp description - url: 'https://novaspektr.io', //dApp url + url: 'https://app.novaspektr.io', //dApp url icons: ['https://drive.google.com/uc?id=1oud8FHw3PcldUgHVeX5OjCg8XANhGO5s'], //dApp logo url verifyUrl: 'https://verify.walletconnect.com', }; @@ -48,8 +48,6 @@ export const REGIONALIZED_RELAYER_ENDPOINTS: RelayerType[] = [ }, ]; -export const WALLETCONNECT_CLIENT_ID = 'WALLETCONNECT_CLIENT_ID'; - export const EXTEND_PAIRING = 60 * 60 * 24 * 30; // 30 days export const FIRST_CHAIN_ID_SYMBOL = 2; diff --git a/src/renderer/features/wallet-wallet-connect/lib/service.test.ts b/src/renderer/features/wallet-wallet-connect/lib/service.test.ts new file mode 100644 index 0000000000..b0059eeef4 --- /dev/null +++ b/src/renderer/features/wallet-wallet-connect/lib/service.test.ts @@ -0,0 +1,22 @@ +import { type HexString } from '@/shared/core'; + +import { walletConnectService } from './service'; + +describe('walletConnectService', () => { + test('should return chain ids in wallet connect type', () => { + const chains = [ + { chainId: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3' as HexString }, + { chainId: '0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe' as HexString }, + ]; + + const result = walletConnectService.getWalletConnectChains(chains); + + expect(result).toEqual(['polkadot:91b171bb158e2d3848fa23a9f1c25182', 'polkadot:b0a8d493285c2df73290dfb7e61f870f']); + }); + + test('should return false if not connected', () => { + const result = walletConnectService.isConnected({}, 'topic'); + + expect(result).toEqual(false); + }); +}); diff --git a/src/renderer/features/wallet-wallet-connect/lib/service.ts b/src/renderer/features/wallet-wallet-connect/lib/service.ts new file mode 100644 index 0000000000..f2fb2868bc --- /dev/null +++ b/src/renderer/features/wallet-wallet-connect/lib/service.ts @@ -0,0 +1,137 @@ +import { type SessionTypes } from '@walletconnect/types'; + +import { chainsService } from '@/shared/api/network'; +import { + AccountType, + type Chain, + type ChainId, + type NovaWalletWallet, + type Wallet, + type WalletConnectWallet, + WalletType, + type WcAccount, +} from '@/shared/core'; +import { nonNullable, nullable, toAccountId } from '@/shared/lib/utils'; +import { type AccountId } from '@/shared/polkadotjs-schemas'; +import { type AnyAccount, accountsService } from '@/domains/network'; + +import { + DEFAULT_POLKADOT_EVENTS, + DEFAULT_POLKADOT_METHODS, + FIRST_CHAIN_ID_SYMBOL, + LAST_CHAIN_ID_SYMBOL, +} from './constants'; + +function isNovaWallet(wallet?: Wallet): wallet is NovaWalletWallet { + return wallet?.type === WalletType.NOVA_WALLET; +} + +function isWalletConnect(wallet?: Wallet): wallet is WalletConnectWallet { + return wallet?.type === WalletType.WALLET_CONNECT; +} + +function isWalletConnectGroup(wallet?: Wallet): wallet is NovaWalletWallet | WalletConnectWallet { + return isNovaWallet(wallet) || isWalletConnect(wallet); +} + +function isWalletConnectAccount(account: Partial): account is WcAccount { + return ( + // @ts-expect-error Partial type breaks required type field usage + accountsService.isChainAccount(account) && + 'accountType' in account && + account.accountType === AccountType.WALLET_CONNECT + ); +} + +function getWalletConnectChains(chains: Pick[]): string[] { + return chains.map(c => getWalletConnectChainId(c.chainId)); +} + +function getWalletConnectChainId(chainId: ChainId): string { + return `polkadot:${chainId.slice(FIRST_CHAIN_ID_SYMBOL, LAST_CHAIN_ID_SYMBOL)}`; +} + +function createNamespaces(chains: ChainId[]) { + return { + polkadot: { + chains: chains.map(getWalletConnectChainId), + methods: [DEFAULT_POLKADOT_METHODS.POLKADOT_SIGN_TRANSACTION], + events: [DEFAULT_POLKADOT_EVENTS.CHAIN_CHANGED, DEFAULT_POLKADOT_EVENTS.ACCOUNTS_CHANGED], + }, + }; +} + +function getAccountsFromSession(session: SessionTypes.Struct, chains: Chain[]) { + if (nullable(session)) return []; + + const accounts = session.namespaces.polkadot.accounts + .map(meta => { + const [_, chainId, address] = meta.split(':') || []; + if (nullable(chainId) || nullable(address)) return null; + const accountId = toAccountId(address); + + return { + chainId, + accountId, + }; + }) + .filter(nonNullable); + + const sortedChains = chainsService.sortChains(chains); + const res: { chain: Chain; accountId: AccountId }[] = []; + + for (const chain of sortedChains) { + const accountsOnChain = accounts.filter(({ chainId }) => chain.chainId.includes(chainId)); + + for (const meta of accountsOnChain) { + res.push({ + accountId: meta.accountId, + chain, + }); + } + } + + return res; +} + +function updateAccount(account: WcAccount, session: SessionTypes.Struct): WcAccount { + return { + ...account, + signingExtras: { + pairingTopic: session.pairingTopic, + sessionTopic: session.topic, + }, + }; +} + +function isConnected(sessions: Record, pairingTopic: string): boolean { + return pairingTopic in sessions; +} + +function isAccountConnected(sessions: Record, account: AnyAccount): boolean { + if (!isWalletConnectAccount(account) || !account.signingExtras.pairingTopic) return false; + + return isConnected(sessions, account.signingExtras.pairingTopic); +} + +function areAccountsConnected(sessions: Record, accounts: AnyAccount[]): boolean { + return accounts.every(a => isAccountConnected(sessions, a)); +} + +export const walletConnectService = { + isNovaWallet, + isWalletConnect, + isWalletConnectGroup, + isWalletConnectAccount, + + isConnected, + isAccountConnected, + areAccountsConnected, + + getWalletConnectChains, + getWalletConnectChainId, + getAccountsFromSession, + + createNamespaces, + updateAccount, +}; diff --git a/src/renderer/features/wallet-wallet-connect/lib/types.ts b/src/renderer/features/wallet-wallet-connect/lib/types.ts new file mode 100644 index 0000000000..5e188929d4 --- /dev/null +++ b/src/renderer/features/wallet-wallet-connect/lib/types.ts @@ -0,0 +1,13 @@ +import type Client from '@walletconnect/sign-client'; +import { type PairingTypes } from '@walletconnect/types'; + +export type InitConnectParams = { + provider: Client; + chains: string[]; + pairing?: Pick; +}; + +export type InitReconnectParams = { + chains: string[]; + pairing: any; +}; diff --git a/src/renderer/features/wallet-wallet-connect/model/connect.ts b/src/renderer/features/wallet-wallet-connect/model/connect.ts new file mode 100644 index 0000000000..547cc250d2 --- /dev/null +++ b/src/renderer/features/wallet-wallet-connect/model/connect.ts @@ -0,0 +1,168 @@ +import { type default as Client } from '@walletconnect/sign-client'; +import { type EngineTypes, type SessionTypes } from '@walletconnect/types'; +import { SDK_ERRORS, getSdkError } from '@walletconnect/utils'; +import { attach, createEffect, createEvent, createStore, restore, sample } from 'effector'; +import { produce } from 'immer'; +import { isObject } from 'lodash'; +import { readonly } from 'patronum'; + +import { type ChainId } from '@/shared/core'; +import { nonNullable, nullable } from '@/shared/lib/utils'; +import { walletConnectService } from '../lib/service'; + +import { signClient } from './signClient'; + +const $sessions = createStore>({}); + +/** + * QR code content for pairing + */ +const updateUri = createEvent(); +const $pairingUri = restore(updateUri, ''); + +const populateSessionsFx = createEffect(async (client: Client) => { + return client.session.getAll(); +}); + +const subscribeSessionsFx = createEffect((client: Client) => { + client.on('proposal_expire', () => { + updateUri(''); + }); + + client.on('session_delete', () => { + populateSessionsFx(client); + }); + + client.on('session_expire', () => { + updateUri(''); + populateSessionsFx(client); + }); +}); + +sample({ + clock: signClient.$client, + filter: nonNullable, + target: [populateSessionsFx, subscribeSessionsFx], +}); + +sample({ + clock: populateSessionsFx.doneData, + fn(sessions) { + return Object.fromEntries(sessions.map(s => [s.pairingTopic, s])); + }, + target: $sessions, +}); + +const createSessionFx = createEffect( + async ({ pairingTopic, chains, client }: { pairingTopic?: string; chains: ChainId[]; client: Client }) => { + const optionalNamespaces = walletConnectService.createNamespaces(chains); + const connect = await client.connect({ pairingTopic, optionalNamespaces }); + + if (connect.uri) { + updateUri(connect.uri); + } + + return connect.approval().finally(() => updateUri('')); + }, +); + +const restoreSessionFx = attach({ + source: { client: signClient.$client, sessions: $sessions }, + async effect({ client, sessions }, { pairingTopic, chains }: { pairingTopic?: string; chains: ChainId[] }) { + if (nullable(client)) throw new Error('WalletConnect Client not found'); + + // existing session + if (pairingTopic && sessions[pairingTopic]) { + return sessions[pairingTopic]; + } + + try { + // trying to restore not expiring session + return await createSessionFx({ client, chains, pairingTopic }); + } catch (e) { + // direct reject should be handled immediately + if (isObject(e) && 'code' in e && e.code === SDK_ERRORS.USER_REJECTED.code) { + throw e; + } + + // creating new session (with qr code scanning) + return await createSessionFx({ client, chains }); + } + }, +}); + +const removeSessionFx = attach({ + source: { client: signClient.$client }, + async effect({ client }, { pairingTopic }: { pairingTopic: string }) { + if (nullable(client)) throw new Error('WalletConnect Client not found'); + + const sessions = client.session.getAll(); + const session = sessions.find(s => s.pairingTopic === pairingTopic); + + if (!session) return; + + const reason = getSdkError('USER_DISCONNECTED'); + + return client.disconnect({ + topic: session.topic, + reason, + }); + }, +}); + +sample({ + clock: createSessionFx.doneData, + source: signClient.$client, + filter: nonNullable, + target: populateSessionsFx, +}); + +const removePairingFx = attach({ + source: signClient.$client, + effect(client, { pairingTopic }: { pairingTopic: string }) { + if (nullable(client)) throw new Error('WalletConnect Client not found'); + + const reason = getSdkError('USER_DISCONNECTED'); + + return client.pairing.delete(pairingTopic, reason); + }, +}); + +type RequestParams = { + client: Client; + session: SessionTypes.Struct; + chainId: EngineTypes.RequestParams['chainId']; + request: EngineTypes.RequestParams['request']; +}; + +const requestFx = createEffect(async ({ client, session, request, chainId }: RequestParams): Promise => { + return client.request({ + topic: session.topic, + chainId, + request, + }); +}); + +sample({ + clock: removeSessionFx.done, + source: $sessions, + fn: (sessions, { params: { pairingTopic } }) => { + return produce(sessions, draft => { + delete draft[pairingTopic]; + }); + }, + target: $sessions, +}); + +export const walletConnect = { + $client: readonly(signClient.$client), + $sessions: readonly($sessions), + $pairingUri: readonly($pairingUri), + + request: requestFx, + + createSession: createSessionFx, + restoreSession: restoreSessionFx, + removeSession: removeSessionFx, + removePairing: removePairingFx, +}; diff --git a/src/renderer/features/wallet-wallet-connect/model/feature.ts b/src/renderer/features/wallet-wallet-connect/model/feature.ts new file mode 100644 index 0000000000..66ad92f65a --- /dev/null +++ b/src/renderer/features/wallet-wallet-connect/model/feature.ts @@ -0,0 +1,7 @@ +import { $features } from '@/shared/config/features'; +import { createFeature } from '@/shared/feature'; + +export const walletWalletConnectFeature = createFeature({ + name: 'wallet/wallet connect', + enable: $features.map(f => f.walletConnect), +}); diff --git a/src/renderer/features/wallet-wallet-connect/model/signClient.ts b/src/renderer/features/wallet-wallet-connect/model/signClient.ts new file mode 100644 index 0000000000..268f6ca2bd --- /dev/null +++ b/src/renderer/features/wallet-wallet-connect/model/signClient.ts @@ -0,0 +1,75 @@ +import { Core, RELAYER_EVENTS } from '@walletconnect/core'; +import { default as Client } from '@walletconnect/sign-client'; +import { createEffect, createEvent, createStore, restore, sample } from 'effector'; + +import { + DEFAULT_APP_METADATA, + DEFAULT_LOGGER, + DEFAULT_PROJECT_ID, + DEFAULT_RELAY_URL, + EXTEND_PAIRING, +} from '../lib/constants'; + +import { walletWalletConnectFeature } from './feature'; + +const $client = createStore(null); + +const changeConnectionStatus = createEvent(); +const $connected = restore(changeConnectionStatus, false); + +const createClientFx = createEffect(async () => { + const core = new Core({ + logger: DEFAULT_LOGGER, + relayUrl: DEFAULT_RELAY_URL, + projectId: DEFAULT_PROJECT_ID, + }); + + core.relayer.on(RELAYER_EVENTS.connect, () => { + changeConnectionStatus(true); + }); + + core.relayer.on(RELAYER_EVENTS.disconnect, () => { + changeConnectionStatus(false); + }); + + const client = await Client.init({ + core, + metadata: DEFAULT_APP_METADATA, + }); + + return client; +}); + +const extendSessionsFx = createEffect(async (client: Client) => { + const sessions = client.session.getAll(); + const pairings = client.pairing.getAll({ active: true }); + + await Promise.all(sessions.map(session => client.extend({ topic: session.topic }).catch(console.warn))); + + await Promise.all( + pairings.map(async pairing => + client.core.pairing + .updateExpiry({ + topic: pairing.topic, + expiry: Math.round(Date.now() / 1000) + EXTEND_PAIRING, + }) + .catch(console.warn), + ), + ); +}); + +sample({ + clock: walletWalletConnectFeature.running, + target: createClientFx, +}); + +sample({ + clock: createClientFx.doneData, + target: [$client, extendSessionsFx], +}); + +export const signClient = { + $client, + $connected, + createClient: createClientFx, +}; diff --git a/src/renderer/features/wallet-wallet-connect/model/wallets.ts b/src/renderer/features/wallet-wallet-connect/model/wallets.ts index a6592340c1..44e5c23084 100644 --- a/src/renderer/features/wallet-wallet-connect/model/wallets.ts +++ b/src/renderer/features/wallet-wallet-connect/model/wallets.ts @@ -3,7 +3,7 @@ import { walletModel, walletUtils } from '@/entities/wallet'; export const $walletConnectWallets = walletModel.$wallets.map(list => list.filter(walletUtils.isWalletConnect)); export const $novaWallets = walletModel.$wallets.map(list => list.filter(walletUtils.isNovaWallet)); -export const walletsModel = { +export const wcWallets = { $walletConnectWallets, $novaWallets, }; diff --git a/src/renderer/features/wallet-watch-only/index.tsx b/src/renderer/features/wallet-watch-only/index.tsx index 3805857cc2..02c3ae0b9e 100644 --- a/src/renderer/features/wallet-watch-only/index.tsx +++ b/src/renderer/features/wallet-watch-only/index.tsx @@ -1,7 +1,7 @@ import { createFeature } from '@/shared/feature'; import { accountsService } from '@/domains/network'; -import { accountUtils } from '@/entities/wallet'; -import { walletGroupSlot } from '@/features/wallet-select'; +import { WalletIcon, accountUtils, walletUtils } from '@/entities/wallet'; +import { walletGroupSlot, walletIconSlot } from '@/features/wallet-select'; import { WatchOnlyGroup, walletActionsSlot } from './components/WatchOnlyGroup'; @@ -23,6 +23,12 @@ walletWatchOnlyFeature.inject(accountsService.accountAvailabilityOnChainAnyOf, ( return accountUtils.isWatchOnlyAccount(account); }); +walletWatchOnlyFeature.inject(walletIconSlot, ({ wallet, size }) => { + if (!walletUtils.isWatchOnly(wallet)) return null; + + return ; +}); + walletWatchOnlyFeature.inject(walletGroupSlot, { order: 4, render: ({ query, onSelect }) => , diff --git a/src/renderer/features/wallets/ForgetWallet/model/__tests__/forget-wallet-model.test.ts b/src/renderer/features/wallets/ForgetWallet/model/__tests__/forget-wallet-model.test.ts index 2d4afb89d9..01905b8a59 100644 --- a/src/renderer/features/wallets/ForgetWallet/model/__tests__/forget-wallet-model.test.ts +++ b/src/renderer/features/wallets/ForgetWallet/model/__tests__/forget-wallet-model.test.ts @@ -27,8 +27,8 @@ vi.mock('@/entities/balance', async () => ({ useBalanceService: () => ({ deleteBalance: jest.fn() }), })); -vi.mock('@walletconnect/universal-provider', () => ({ - Provider: {}, +vi.mock('@walletconnect/sign-client', () => ({ + Client: {}, })); vi.mock('@walletconnect/utils', () => ({ diff --git a/src/renderer/features/wallets/RenameWallet/model/__tests__/rename-wallet-model.test.ts b/src/renderer/features/wallets/RenameWallet/model/__tests__/rename-wallet-model.test.ts index 61e4a47b71..2560c96421 100644 --- a/src/renderer/features/wallets/RenameWallet/model/__tests__/rename-wallet-model.test.ts +++ b/src/renderer/features/wallets/RenameWallet/model/__tests__/rename-wallet-model.test.ts @@ -13,8 +13,8 @@ vi.mock('@walletconnect/utils', () => ({ getSdkError: jest.fn(), })); -vi.mock('@walletconnect/universal-provider', () => ({ - Provider: {}, +vi.mock('@walletconnect/sign-client', () => ({ + Client: {}, })); describe('entities/wallet/model/wallet-model', () => { diff --git a/src/renderer/features/wallets/RenameWallet/model/rename-wallet-model.ts b/src/renderer/features/wallets/RenameWallet/model/rename-wallet-model.ts index 735aa71cf8..5a63ed3117 100644 --- a/src/renderer/features/wallets/RenameWallet/model/rename-wallet-model.ts +++ b/src/renderer/features/wallets/RenameWallet/model/rename-wallet-model.ts @@ -45,7 +45,7 @@ const $walletForm = createForm({ const renameWalletFx = createEffect(async ({ id, accounts, ...rest }: Wallet): Promise => { await storageService.wallets.update(id, rest); - await Promise.all(accounts.map(networkDomain.accounts.updateAccount)); + await networkDomain.accounts.updateAccounts(accounts); return { id, accounts, ...rest }; }); diff --git a/src/renderer/shared/core/types/account.ts b/src/renderer/shared/core/types/account.ts index d145be3a91..06ab66e9f9 100644 --- a/src/renderer/shared/core/types/account.ts +++ b/src/renderer/shared/core/types/account.ts @@ -44,7 +44,10 @@ export type FlexibleMultisigAccount = ChainAccount<{ export type WcAccount = ChainAccount<{ accountType: AccountType.WALLET_CONNECT; - signingExtras: Record; + signingExtras: { + pairingTopic?: string; + sessionTopic?: string; + }; }>; export type ProxiedAccount = ChainAccount<{ diff --git a/src/renderer/shared/core/types/wallet.ts b/src/renderer/shared/core/types/wallet.ts index bdd9ce5127..5da32ae4ea 100644 --- a/src/renderer/shared/core/types/wallet.ts +++ b/src/renderer/shared/core/types/wallet.ts @@ -74,13 +74,11 @@ export interface ProxiedWallet extends Wallet { export interface WalletConnectWallet extends Wallet { type: WalletType.WALLET_CONNECT; accounts: WcAccount[]; - isConnected: boolean; } export interface NovaWalletWallet extends Wallet { type: WalletType.NOVA_WALLET; accounts: WcAccount[]; - isConnected: boolean; } export type WalletsMap = Record; diff --git a/src/renderer/shared/di/createSlot.tsx b/src/renderer/shared/di/createSlot.tsx index d9fe17971a..60f2be2912 100644 --- a/src/renderer/shared/di/createSlot.tsx +++ b/src/renderer/shared/di/createSlot.tsx @@ -7,9 +7,9 @@ import { shallowEqual } from './lib/shallowEqual'; import { type Identifier } from './types'; // Public interface -type SlotHandler = FunctionComponent | SlotHandlerExtended; +export type SlotHandler = FunctionComponent | SlotHandlerExtended; -type SlotHandlerExtended = { +export type SlotHandlerExtended = { order?: number; render: FunctionComponent; }; diff --git a/src/renderer/shared/di/index.ts b/src/renderer/shared/di/index.ts index c898eff029..a3a76b29f7 100644 --- a/src/renderer/shared/di/index.ts +++ b/src/renderer/shared/di/index.ts @@ -3,8 +3,8 @@ export { createAsyncPipeline } from './createAsyncPipeline'; export { createPipeline, isPipelineIdentifier } from './createPipeline'; export { createSlot, isSlotIdentifier, normalizeSlotHandler } from './createSlot'; export { skipAction } from './constants'; -export { combineIdentifiers } from './helpers'; +export { combineIdentifiers, isIdentifier } from './helpers'; export { usePipeline, useSlot, Slot } from './reactIntegration'; -export type { AnyIdentifier, InferHandlerBody, InferInput, InferOutput, HandlerInput } from './types'; +export type { AnyIdentifier, InferHandlerBody, InferInput, InferOutput, HandlerInput, Handler } from './types'; diff --git a/src/renderer/shared/effector/helpers/createBuffer.ts b/src/renderer/shared/effector/createBuffer.ts similarity index 100% rename from src/renderer/shared/effector/helpers/createBuffer.ts rename to src/renderer/shared/effector/createBuffer.ts diff --git a/src/renderer/shared/effector/index.ts b/src/renderer/shared/effector/index.ts index eaf5ce5cc2..a0471c50ce 100644 --- a/src/renderer/shared/effector/index.ts +++ b/src/renderer/shared/effector/index.ts @@ -1,5 +1,6 @@ export { createDataSource } from './createDataSource'; export { createDataSubscription, createPagesHandler } from './createDataSubscription'; -export { series } from './helpers/series'; -export { createBuffer } from './helpers/createBuffer'; +export { series } from './series'; +export { waitFor } from './waitFor'; +export { createBuffer } from './createBuffer'; diff --git a/src/renderer/shared/effector/helpers/series.test.ts b/src/renderer/shared/effector/series.test.ts similarity index 100% rename from src/renderer/shared/effector/helpers/series.test.ts rename to src/renderer/shared/effector/series.test.ts diff --git a/src/renderer/shared/effector/helpers/series.ts b/src/renderer/shared/effector/series.ts similarity index 50% rename from src/renderer/shared/effector/helpers/series.ts rename to src/renderer/shared/effector/series.ts index 817721f31b..e2d87504ea 100644 --- a/src/renderer/shared/effector/helpers/series.ts +++ b/src/renderer/shared/effector/series.ts @@ -1,4 +1,4 @@ -import { type Effect, type EventCallable, createEffect } from 'effector'; +import { type Effect, type EventCallable, createEffect, is } from 'effector'; /** * Triggers target unit on each element of the input list. @@ -17,10 +17,18 @@ import { type Effect, type EventCallable, createEffect } from 'effector'; * // event(0); event(1); event(2) * ``` */ -export const series = (target: EventCallable | Effect) => { - return createEffect((data: Iterable) => { +export const series = (target: EventCallable | Effect) => { + return createEffect(async (data: Iterable) => { + const result: R[] = []; for (const value of data) { - target(value); + if (is.effect(target)) { + const r = await target(value); + result.push(r); + } else { + target(value); + } } + + return result; }); }; diff --git a/src/renderer/shared/effector/waitFor.test.ts b/src/renderer/shared/effector/waitFor.test.ts new file mode 100644 index 0000000000..cc2ecc68cd --- /dev/null +++ b/src/renderer/shared/effector/waitFor.test.ts @@ -0,0 +1,58 @@ +import { allSettled, createEvent, createStore, createWatch, fork } from 'effector'; +import { isString } from 'lodash'; + +import { waitFor } from './waitFor'; + +describe('waitFor', () => { + it('should pass event', async () => { + const scope = fork(); + const spy = vi.fn(); + + const event = createEvent(); + const clock = createEvent(); + + const waitEvent = waitFor({ + source: event, + clock, + filter: isString, + }); + + createWatch({ + unit: waitEvent, + fn: spy, + }); + + await allSettled(event, { params: 'test_event', scope }); + await allSettled(clock, { params: 'test_clock', scope }); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith({ event: 'test_event', trigger: 'test_clock' }); + }); + + it('should work with store clock', async () => { + const scope = fork(); + const spy = vi.fn(); + + const event = createEvent(); + const clock = createStore('test_1'); + + const waitEvent = waitFor({ + source: event, + clock, + filter: (s): s is string => s !== 'test_1', + }); + + createWatch({ + unit: waitEvent, + fn: spy, + }); + + await allSettled(event, { params: 'test_event', scope }); + await allSettled(clock, { params: 'test_2', scope }); + await allSettled(event, { params: 'test_event_2', scope }); + + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledWith({ event: 'test_event', trigger: 'test_2' }); + expect(spy).toHaveBeenCalledWith({ event: 'test_event_2', trigger: 'test_2' }); + }); +}); diff --git a/src/renderer/shared/effector/waitFor.ts b/src/renderer/shared/effector/waitFor.ts new file mode 100644 index 0000000000..b1a220673b --- /dev/null +++ b/src/renderer/shared/effector/waitFor.ts @@ -0,0 +1,58 @@ +import { type Event, type Store, type Unit, createEvent, is, sample } from 'effector'; +import { combineEvents } from 'patronum'; + +import { nonNullable } from '@/shared/lib/utils'; + +type Params = { + source: Store | Event; + clock: Store | Event; + reset?: Unit | Unit[]; + filter?: (value: NoInfer) => value is F; +}; + +export const waitFor = ({ + source, + clock, + reset, + filter = (v: R): v is F => true, +}: Params) => { + const sourceEvent = is.store(source) ? source.updates : source; + const clockEvent = is.store(clock) ? clock.updates : clock; + const wait = createEvent<{ event: E; trigger: F }>(); + + const resetEvent = createEvent(); + + const combined = combineEvents({ + events: [sourceEvent, clockEvent], + reset: resetEvent, + }).filterMap(([event, trigger]) => { + if (!filter || filter(trigger)) { + return { event, trigger }; + } + }); + + // @ts-expect-error R to F conversion + sample({ + clock: combined, + target: wait, + }); + + if (is.store(clock)) { + sample({ + clock: sourceEvent, + source: clock, + filter: filter, + fn: (trigger, event) => ({ event, trigger }), + target: wait, + }); + } + + const resetEvents = Array.isArray(reset) ? reset : [reset]; + + sample({ + clock: resetEvents.concat(sourceEvent).filter(nonNullable), + resetEvent, + }); + + return wait; +}; diff --git a/src/renderer/shared/feature/createFeature.ts b/src/renderer/shared/feature/createFeature.ts index ad0c0fb61c..f5b7df961a 100644 --- a/src/renderer/shared/feature/createFeature.ts +++ b/src/renderer/shared/feature/createFeature.ts @@ -120,7 +120,7 @@ export const createFeature = ({ sample({ clock: startIfNecessary, source: $identifiers, - filter: identifiers => !identifiers.every(isSlotIdentifier), + filter: identifiers => identifiers.length > 0 && !identifiers.every(isSlotIdentifier), target: start, }); diff --git a/src/renderer/shared/feature/registerFeatures.ts b/src/renderer/shared/feature/registerFeatures.ts index 2774e8712b..100f3da005 100644 --- a/src/renderer/shared/feature/registerFeatures.ts +++ b/src/renderer/shared/feature/registerFeatures.ts @@ -31,7 +31,7 @@ export const registerFeatures = (features: Feature[]) => { // eslint-disable-next-line effector/no-getState const message = `${feature.name.split('/').at(1) ?? 'unknown'}${feature.status.getState() !== 'idle' ? ' | started' : ''}`; - console.log(message); + console.info(message); } console.groupEnd(); } diff --git a/src/renderer/shared/lib/utils/functions.ts b/src/renderer/shared/lib/utils/functions.ts index 87f9ed54c7..5713c46d98 100644 --- a/src/renderer/shared/lib/utils/functions.ts +++ b/src/renderer/shared/lib/utils/functions.ts @@ -20,6 +20,19 @@ export function nullable(value: unknown): value is null | undefined { return value === null || value === undefined; } +/** + * Type guard that checks is value nullable + * + * @param value Value to be checked + * + * @returns {Boolean} + */ +export function assert(value: unknown, message?: string): asserts value is NonNullable { + if (value === null || value === undefined) { + throw new Error(message ?? 'Value is nullish'); + } +} + /** * Type guard that checks is Promise settled fulfilled * diff --git a/src/renderer/shared/mocks/index.ts b/src/renderer/shared/mocks/index.ts index 26cc77b372..f477587ed4 100644 --- a/src/renderer/shared/mocks/index.ts +++ b/src/renderer/shared/mocks/index.ts @@ -173,7 +173,6 @@ export const createWcWallet = (id: number, accounts: WcAccount[]): WalletConnect accounts, type: WalletType.WALLET_CONNECT, isActive: true, - isConnected: true, name: `WalletConnect ${id}`, signingType: SigningType.WALLET_CONNECT, }); diff --git a/vite.config.renderer.ts b/vite.config.renderer.ts index 72c90fe6f6..e489e87a40 100644 --- a/vite.config.renderer.ts +++ b/vite.config.renderer.ts @@ -1,11 +1,27 @@ /// +import { cpus } from 'node:os'; import { resolve } from 'node:path'; -import { type UserConfigFn } from 'vite'; +import { type Plugin, type UserConfigFn } from 'vite'; import { folders, renderer, title, version } from './config/index.js'; +function skipSourcemaps(paths: string[]): Plugin { + return { + name: 'skip-sourcemaps', + transform(code, id) { + if (paths.some((pkg) => id.includes(pkg))) { + return { + code: code, + // /~https://github.com/rollup/rollup/blob/master/docs/plugin-development/index.md#source-code-transformations + map: { mappings: '' }, + }; + } + }, + }; +} + const config: UserConfigFn = async ({ mode, command }) => { const { defineConfig } = await import('vite'); const { default: tsconfigPaths } = await import('vite-tsconfig-paths'); @@ -21,6 +37,7 @@ const config: UserConfigFn = async ({ mode, command }) => { const isStage = mode === 'staging'; const commonPlugins = [ + skipSourcemaps(['node_modules']), tsconfigPaths(), nodePolyfills({ include: ['buffer', 'events', 'crypto', 'stream'], @@ -50,7 +67,8 @@ const config: UserConfigFn = async ({ mode, command }) => { emptyOutDir: false, target: 'es2021', rollupOptions: { - treeshake: 'smallest', + treeshake: 'recommended', + maxParallelFileOps: Math.max(1, cpus().length - 1), }, }, assetsInclude: ['**/*.wasm'], @@ -105,15 +123,17 @@ const config: UserConfigFn = async ({ mode, command }) => { }, ), - compression({ - algorithm: 'gzip', - include: /.+/, - skipIfLargerOrEqual: true, - threshold: 0, - compressionOptions: { - level: 9, - }, - }), + isProd && + command === 'build' && + compression({ + algorithm: 'gzip', + include: /.+/, + skipIfLargerOrEqual: true, + threshold: 0, + compressionOptions: { + level: 9, + }, + }), ], optimizeDeps: { diff --git a/vitest.config.ts b/vitest.config.ts index 76456a9d77..43d3eb9b73 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -36,7 +36,19 @@ const config: UserConfigFnPromise = async (options) => { root: folders.root, dir: folders.source, globals: true, - environment: 'happy-dom', + environmentMatchGlobs: [ + // This list should dissapear over time, simple logic tests shouldn't depend on environment. + ['src/renderer/shared/lib/hooks/**/*.ts', 'happy-dom'], + ['src/renderer/shared/i18n/**/*.ts', 'happy-dom'], + ['src/renderer/shared/api/**/*.ts', 'happy-dom'], + ['src/renderer/domains/**/*.ts', 'happy-dom'], + ['src/renderer/entities/**/*.ts', 'happy-dom'], + ['src/renderer/features/**/*.ts', 'happy-dom'], + ['src/renderer/widgets/**/*.ts', 'happy-dom'], + ['src/renderer/pages/**/*.ts', 'happy-dom'], + ['**/*.tsx', 'happy-dom'], + ['**/*.ts', 'node'], + ], setupFiles: resolve(folders.root, './vitest.setup.js'), reporters: ['basic', 'junit'], outputFile: {