diff --git a/.github/workflows/app-preview.yaml b/.github/workflows/app-preview.yaml index 9e35c6edf..abdc592d1 100644 --- a/.github/workflows/app-preview.yaml +++ b/.github/workflows/app-preview.yaml @@ -1,4 +1,4 @@ -name: Deploy Pilot App +name: Pilot App on: pull_request: diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 3a34b3eba..8569fd066 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -45,10 +45,35 @@ jobs: cache-dependency-path: '**/pnpm-lock.yaml' - run: pnpm install --prefer-offline - run: pnpm test - - name: 'Report Coverage' - # Set if: always() to also generate the report if tests are failing - # Only works if you set `reportOnFailure: true` in your vite config as specified above - if: always() - uses: davelosert/vitest-coverage-report-action@v2 + + test-utils: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./packages/test-utils + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: latest + cache: 'pnpm' + cache-dependency-path: '**/pnpm-lock.yaml' + - run: pnpm install --prefer-offline + - run: pnpm test + + pilot-app: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./deployables/app + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 with: - working-directory: packages/ui + node-version: latest + cache: 'pnpm' + cache-dependency-path: '**/pnpm-lock.yaml' + - run: pnpm install --prefer-offline + - run: pnpm test diff --git a/deployables/app/app/entry.server.tsx b/deployables/app/app/entry.server.tsx index 336d56fa1..3a58b1722 100644 --- a/deployables/app/app/entry.server.tsx +++ b/deployables/app/app/entry.server.tsx @@ -3,8 +3,6 @@ import { renderToReadableStream } from 'react-dom/server' import type { AppLoadContext, EntryContext } from 'react-router' import { ServerRouter } from 'react-router' -const ABORT_DELAY = 5_000 - export default async function handleRequest( request: Request, responseStatusCode: number, diff --git a/deployables/app/app/root.tsx b/deployables/app/app/root.tsx index 6a75d11f5..1791388c9 100644 --- a/deployables/app/app/root.tsx +++ b/deployables/app/app/root.tsx @@ -11,14 +11,14 @@ import './app.css' export function Layout({ children }: { children: React.ReactNode }) { return ( - + - + {children} diff --git a/deployables/app/app/routes.ts b/deployables/app/app/routes.ts index 5aa26e1e5..d90783a97 100644 --- a/deployables/app/app/routes.ts +++ b/deployables/app/app/routes.ts @@ -1,3 +1,6 @@ -import { type RouteConfig, index } from '@react-router/dev/routes' +import { type RouteConfig, index, route } from '@react-router/dev/routes' -export default [index('routes/index.tsx')] satisfies RouteConfig +export default [ + index('routes/index.tsx'), + route('/edit-route', 'routes/edit-route.tsx'), +] satisfies RouteConfig diff --git a/deployables/app/app/routes/edit-route.spec.ts b/deployables/app/app/routes/edit-route.spec.ts new file mode 100644 index 000000000..92af3b20b --- /dev/null +++ b/deployables/app/app/routes/edit-route.spec.ts @@ -0,0 +1,21 @@ +import { render } from '@/test-utils' +import { screen } from '@testing-library/react' +import { createMockExecutionRoute } from '@zodiac/test-utils' +import { describe, expect, it } from 'vitest' +import EditRoute, { loader } from './edit-route' + +describe('Edit route', () => { + it('shows the name of a route', async () => { + const route = createMockExecutionRoute({ label: 'Test route' }) + + await render( + '/edit-route', + { path: '/edit-route', Component: EditRoute, loader }, + { searchParams: { route: btoa(JSON.stringify(route)) } }, + ) + + expect(screen.getByRole('textbox', { name: 'Label' })).toHaveValue( + 'Test route', + ) + }) +}) diff --git a/deployables/app/app/routes/edit-route.tsx b/deployables/app/app/routes/edit-route.tsx new file mode 100644 index 000000000..54a6d39a4 --- /dev/null +++ b/deployables/app/app/routes/edit-route.tsx @@ -0,0 +1,32 @@ +import { invariantResponse } from '@epic-web/invariant' +import { executionRouteSchema } from '@zodiac/schema' +import { TextInput } from '@zodiac/ui' +import type { Route } from './+types/edit-route' + +export const loader = ({ request }: Route.LoaderArgs) => { + const url = new URL(request.url) + + const routeData = url.searchParams.get('route') + + invariantResponse(routeData != null, 'Missing "route" parameter') + + const decodedData = Buffer.from(routeData, 'base64') + + try { + const rawJson = JSON.parse(decodedData.toString()) + + return { route: executionRouteSchema.parse(rawJson) } + } catch { + throw new Response(null, { status: 400 }) + } +} + +const EditRoute = ({ loaderData }: Route.ComponentProps) => { + return ( + <> + + + ) +} + +export default EditRoute diff --git a/deployables/app/package.json b/deployables/app/package.json index cbc58a086..f60d189b8 100644 --- a/deployables/app/package.json +++ b/deployables/app/package.json @@ -6,29 +6,41 @@ "build": "react-router build", "dev": "react-router dev", "start": "wrangler dev", - "check-types": "react-router typegen && tsc -b" + "check-types": "react-router typegen && tsc -b", + "test": "vitest" }, "dependencies": { + "@epic-web/invariant": "^1.0.0", "@react-router/node": "^7.1.1", "@react-router/serve": "^7.1.1", + "@zodiac/schema": "workspace:*", + "@zodiac/ui": "workspace:*", "isbot": "^5.1.17", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-router": "^7.1.1" + "react-router": "^7.1.1", + "zod": "^3.23.8" }, "devDependencies": { "@cloudflare/workers-types": "4.20241230.0", "@hiogawa/vite-node-miniflare": "0.1.1", "@react-router/dev": "^7.1.1", + "@testing-library/jest-dom": "^6.4.6", + "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "14.5.2", "@types/node": "^22.0.0", "@types/react": "^19.0.1", "@types/react-dom": "^19.0.1", + "@vitest/coverage-v8": "2.1.8", + "@zodiac/test-utils": "workspace:*", "autoprefixer": "^10.4.20", + "eslint": "^9.7.0", "postcss": "^8.4.49", "tailwindcss": "^3.4.16", "typescript": "^5.7.2", "vite": "^5.4.11", "vite-tsconfig-paths": "^5.1.4", + "vitest": "2.1.8", "wrangler": "^3.87.0" } } \ No newline at end of file diff --git a/deployables/app/tailwind.config.ts b/deployables/app/tailwind.config.ts index 2e66b027a..585a5e9f1 100644 --- a/deployables/app/tailwind.config.ts +++ b/deployables/app/tailwind.config.ts @@ -1,7 +1,10 @@ import type { Config } from 'tailwindcss' export default { - content: ['./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}'], + content: [ + './app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}', + '../../packages/ui/**/*.tsx', + ], theme: { extend: { fontFamily: { diff --git a/deployables/app/test-utils/index.ts b/deployables/app/test-utils/index.ts new file mode 100644 index 000000000..5a3c950b2 --- /dev/null +++ b/deployables/app/test-utils/index.ts @@ -0,0 +1 @@ +export { render } from './render' diff --git a/deployables/app/test-utils/render.tsx b/deployables/app/test-utils/render.tsx new file mode 100644 index 000000000..5b96c5e97 --- /dev/null +++ b/deployables/app/test-utils/render.tsx @@ -0,0 +1,14 @@ +import { + renderFramework, + type FrameworkRoute, + type RenderOptions, + type RouteModule, +} from '@zodiac/test-utils' + +export function render( + currentPath: string, + route: FrameworkRoute, + options: RenderOptions, +) { + return renderFramework(currentPath, route, options) +} diff --git a/deployables/app/tsconfig.cloudflare.json b/deployables/app/tsconfig.cloudflare.json index a80f6076b..15f2baaec 100644 --- a/deployables/app/tsconfig.cloudflare.json +++ b/deployables/app/tsconfig.cloudflare.json @@ -2,10 +2,12 @@ "extends": "./tsconfig.json", "include": [ ".react-router/types/**/*", + "vitest.setup.ts", "app/**/*", "app/**/.server/**/*", "app/**/.client/**/*", - "workers/**/*" + "workers/**/*", + "test-utils/**/*" ], "compilerOptions": { "composite": true, @@ -19,7 +21,8 @@ "baseUrl": ".", "rootDirs": [".", "./.react-router/types"], "paths": { - "~/*": ["./app/*"] + "~/*": ["./app/*"], + "@/test-utils": ["./test-utils/index.ts"] }, "esModuleInterop": true, "resolveJsonModule": true diff --git a/deployables/app/tsconfig.json b/deployables/app/tsconfig.json index d7ce9e49b..437daa14f 100644 --- a/deployables/app/tsconfig.json +++ b/deployables/app/tsconfig.json @@ -9,6 +9,9 @@ "verbatimModuleSyntax": true, "skipLibCheck": true, "strict": true, - "noEmit": true + "noEmit": true, + "paths": { + "@/test-utils": ["./test-utils/index.ts"] + } } } diff --git a/deployables/app/vite.config.ts b/deployables/app/vite.config.ts index 4980f60ee..6ff8a5347 100644 --- a/deployables/app/vite.config.ts +++ b/deployables/app/vite.config.ts @@ -1,5 +1,5 @@ -import { vitePluginViteNodeMiniflare } from '@hiogawa/vite-node-miniflare' import { reactRouter } from '@react-router/dev/vite' +import { cloudflareDevProxy } from '@react-router/dev/vite/cloudflare' import autoprefixer from 'autoprefixer' import tailwindcss from 'tailwindcss' import { defineConfig } from 'vite' @@ -20,7 +20,7 @@ export default defineConfig(({ isSsrBuild }) => ({ }, ssr: { target: 'webworker', - noExternal: true, + // noExternal: true, resolve: { conditions: ['workerd', 'browser'], }, @@ -36,13 +36,14 @@ export default defineConfig(({ isSsrBuild }) => ({ }, }, plugins: [ - vitePluginViteNodeMiniflare({ - entry: './workers/app.ts', - miniflareOptions: (options) => { - options.compatibilityDate = '2024-11-18' - options.compatibilityFlags = ['nodejs_compat'] - }, - }), + cloudflareDevProxy(), + // vitePluginViteNodeMiniflare({ + // entry: './workers/app.ts', + // miniflareOptions: (options) => { + // options.compatibilityDate = '2024-11-18' + // options.compatibilityFlags = ['nodejs_compat'] + // }, + // }), reactRouter(), tsconfigPaths(), ], diff --git a/deployables/app/vitest.config.ts b/deployables/app/vitest.config.ts new file mode 100644 index 000000000..d90392d49 --- /dev/null +++ b/deployables/app/vitest.config.ts @@ -0,0 +1,35 @@ +/// + +import { fileURLToPath } from 'url' +import { defineConfig } from 'vitest/config' +import tsConfig from './tsconfig.json' + +const alias = Object.entries(tsConfig.compilerOptions.paths).reduce( + (result, [key, value]) => ({ + ...result, + [key]: fileURLToPath(new URL(value[0], import.meta.url)), + }), + {}, +) + +const { CI } = process.env + +export default defineConfig({ + test: { + alias, + environment: 'happy-dom', + setupFiles: ['./vitest.setup.ts'], + include: ['./app/**/*.{spec,test}.{ts,tsx}'], + mockReset: true, + clearMocks: true, + + coverage: { + skipFull: true, + enabled: CI != null, + reportOnFailure: CI != null, + reporter: CI ? ['json', 'json-summary'] : undefined, + include: ['**/app/**/*.{ts,tsx}'], + exclude: ['**/src/**/*.spec.{ts,tsx}'], + }, + }, +}) diff --git a/deployables/app/vitest.setup.ts b/deployables/app/vitest.setup.ts new file mode 100644 index 000000000..a9d0dd31a --- /dev/null +++ b/deployables/app/vitest.setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom/vitest' diff --git a/deployables/app/workers/app.ts b/deployables/app/workers/app.ts index 91082f988..afb93e6c4 100644 --- a/deployables/app/workers/app.ts +++ b/deployables/app/workers/app.ts @@ -5,9 +5,7 @@ declare global { } declare module 'react-router' { - export interface AppLoadContext { - VALUE_FROM_CLOUDFLARE: string - } + export interface AppLoadContext {} } const requestHandler = createRequestHandler( @@ -18,8 +16,6 @@ const requestHandler = createRequestHandler( export default { fetch(request, env) { - return requestHandler(request, { - VALUE_FROM_CLOUDFLARE: 'Hello from Cloudflare', - }) + return requestHandler(request) }, } satisfies ExportedHandler diff --git a/deployables/app/wrangler.toml b/deployables/app/wrangler.toml index 693e892ee..e719f4d4c 100644 --- a/deployables/app/wrangler.toml +++ b/deployables/app/wrangler.toml @@ -1,6 +1,7 @@ workers_dev = true name = "pilot-app" compatibility_date = "2024-11-18" +compatibility_flags = ["nodejs_compat"] main = "./build/server/index.js" assets = { directory = "./build/client/" } diff --git a/deployables/extension/package.json b/deployables/extension/package.json index 8e11f0ff3..aade620d5 100644 --- a/deployables/extension/package.json +++ b/deployables/extension/package.json @@ -104,6 +104,8 @@ "react-router": "7.1.1", "react-stick": "^5.0.6", "zod": "^3.23.8", - "@zodiac/ui": "workspace:*" + "@zodiac/ui": "workspace:*", + "@zodiac/schema": "workspace:*", + "@zodiac/chains": "workspace:*" } } \ No newline at end of file diff --git a/deployables/extension/src/chains/const.ts b/deployables/extension/src/chains/const.ts deleted file mode 100644 index 10b90aeef..000000000 --- a/deployables/extension/src/chains/const.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* spell-checker: disable */ - -import type { HexAddress } from '@/types' -import type { ChainId, PrefixedAddress } from 'ser-kit' - -export const ZERO_ADDRESS: HexAddress = - '0x0000000000000000000000000000000000000000' -export const ETH_ZERO_ADDRESS: PrefixedAddress = - 'eth:0x0000000000000000000000000000000000000000' -export const EOA_ZERO_ADDRESS: PrefixedAddress = - 'eoa:0x0000000000000000000000000000000000000000' - -export const RPC: Record = { - 1: 'https://airlock.gnosisguild.org/api/v1/1/rpc', - 10: 'https://airlock.gnosisguild.org/api/v1/10/rpc', - 100: 'https://airlock.gnosisguild.org/api/v1/100/rpc', - 137: 'https://airlock.gnosisguild.org/api/v1/137/rpc', - 8453: 'https://airlock.gnosisguild.org/api/v1/8453/rpc', - 42161: 'https://airlock.gnosisguild.org/api/v1/42161/rpc', - 43114: 'https://airlock.gnosisguild.org/api/v1/43114/rpc', - 11155111: 'https://airlock.gnosisguild.org/api/v1/11155111/rpc', -} - -export const EXPLORER_URL: Record = { - 1: 'https://etherscan.io', - 10: 'https://optimistic.etherscan.io', - 100: 'https://gnosisscan.io', - 137: 'https://polygonscan.com', - 8453: 'https://basescan.org', - 42161: 'https://arbiscan.io', - 43114: 'https://snowtrace.io', - 11155111: 'https://sepolia.etherscan.io', -} - -export const EXPLORER_API_URL: Record = { - 1: 'https://api.etherscan.io/api', - 10: 'https://api-optimistic.etherscan.io/api', - 100: 'https://api.gnosisscan.io/api', - 137: 'https://api.polygonscan.com/api', - 8453: 'https://api.basescan.org/api', - 42161: 'https://api.arbiscan.io/api', - 43114: 'https://api.snowtrace.io/api', - 11155111: 'https://api-sepolia.etherscan.io/api', -} - -export const EXPLORER_API_KEY: Record = { - 1: 'N53BKW6ABNX7CNUK8QIXGRAQS2NME92YAN', - 10: 'SM2FQ62U49I6H9V9CCEGFS34QGBK4IIJPH', - 100: 'W575K6DTMSTVB7UFUSNW7GWQ4UWUARTJ7Z', - 137: 'NM937M1IZXVQ6QVDXS73XMF8JSAB677JWQ', - 8453: 'KCC7EQHE17IAQZA9TICUS6BQTJGZUDRNIY', - 42161: 'SJ5BEYBBC3DNSKTH5BAEPFJXUZDAJ133UI', - 43114: 'notrequired', - 11155111: 'N53BKW6ABNX7CNUK8QIXGRAQS2NME92YAN', -} - -export const CHAIN_PREFIX: Record = { - 1: 'eth', - 10: 'oeth', - 100: 'gno', - 137: 'matic', - 8453: 'base', - 42161: 'arb1', - 43114: 'avax', - 11155111: 'sep', -} - -export const CHAIN_CURRENCY: Record = { - 1: 'ETH', - 10: 'ETH', - 100: 'xDAI', - 137: 'MATIC', - 8453: 'ETH', - 42161: 'ETH', - 43114: 'AVAX', - 11155111: 'ETH', -} - -export const CHAIN_NAME: Record = { - 1: 'Ethereum', - 10: 'Optimism', - 100: 'Gnosis', - 137: 'Polygon', - 8453: 'Base', - 42161: 'Arbitrum One', - 43114: 'Avalanche C-Chain', - 11155111: 'Sepolia', -} diff --git a/deployables/extension/src/inject/bridge/useProviderBridge.spec.tsx b/deployables/extension/src/inject/bridge/useProviderBridge.spec.tsx index f67adecba..24fab8910 100644 --- a/deployables/extension/src/inject/bridge/useProviderBridge.spec.tsx +++ b/deployables/extension/src/inject/bridge/useProviderBridge.spec.tsx @@ -1,4 +1,3 @@ -import { ZERO_ADDRESS } from '@/chains' import { InjectedProviderMessageTyp } from '@/messages' import { chromeMock, @@ -10,6 +9,7 @@ import { } from '@/test-utils' import type { Eip1193Provider } from '@/types' import { cleanup, waitFor } from '@testing-library/react' +import { ZERO_ADDRESS } from '@zodiac/chains' import { toQuantity } from 'ethers' import type { PropsWithChildren } from 'react' import type { ChainId } from 'ser-kit' diff --git a/deployables/extension/src/panel/execution-routes/createRoute.ts b/deployables/extension/src/panel/execution-routes/createRoute.ts index df7c6c1cb..2e2a41255 100644 --- a/deployables/extension/src/panel/execution-routes/createRoute.ts +++ b/deployables/extension/src/panel/execution-routes/createRoute.ts @@ -1,5 +1,5 @@ -import { ETH_ZERO_ADDRESS } from '@/chains' import { ProviderType } from '@/types' +import { ETH_ZERO_ADDRESS } from '@zodiac/chains' import { nanoid } from 'nanoid' import { saveRoute } from './saveRoute' diff --git a/deployables/extension/src/panel/execution-routes/useRouteConnect.ts b/deployables/extension/src/panel/execution-routes/useRouteConnect.ts index 2ae5ba52e..dc756a978 100644 --- a/deployables/extension/src/panel/execution-routes/useRouteConnect.ts +++ b/deployables/extension/src/panel/execution-routes/useRouteConnect.ts @@ -1,6 +1,6 @@ -import { getChainId } from '@/chains' import { isConnected, useInjectedWallet, useWalletConnect } from '@/providers' import { type ExecutionRoute, ProviderType } from '@/types' +import { getChainId } from '@zodiac/chains' import { ZeroAddress } from 'ethers' import { useCallback } from 'react' import { parsePrefixedAddress } from 'ser-kit' diff --git a/deployables/extension/src/panel/execution-routes/useRouteProvider.ts b/deployables/extension/src/panel/execution-routes/useRouteProvider.ts index f0dad0053..bf5766dc4 100644 --- a/deployables/extension/src/panel/execution-routes/useRouteProvider.ts +++ b/deployables/extension/src/panel/execution-routes/useRouteProvider.ts @@ -1,10 +1,10 @@ -import { getChainId } from '@/chains' import { getEip1193ReadOnlyProvider, useInjectedWallet, useWalletConnect, } from '@/providers' import { type ExecutionRoute, ProviderType } from '@/types' +import { getChainId } from '@zodiac/chains' export const useRouteProvider = (route: ExecutionRoute) => { const injectedWallet = useInjectedWallet() diff --git a/deployables/extension/src/panel/integrations/safe/kits.ts b/deployables/extension/src/panel/integrations/safe/kits.ts index f5fae9230..8255d3e9e 100644 --- a/deployables/extension/src/panel/integrations/safe/kits.ts +++ b/deployables/extension/src/panel/integrations/safe/kits.ts @@ -1,6 +1,6 @@ -import { RPC } from '@/chains' import SafeApiKit from '@safe-global/api-kit' import Safe from '@safe-global/protocol-kit' +import { RPC } from '@zodiac/chains' import type { ChainId } from 'ser-kit' export const TX_SERVICE_URL: Record = { diff --git a/deployables/extension/src/panel/pages/$activeRouteId/ActiveRoute.spec.ts b/deployables/extension/src/panel/pages/$activeRouteId/ActiveRoute.spec.ts index ceee5ccc2..ff2fa34ca 100644 --- a/deployables/extension/src/panel/pages/$activeRouteId/ActiveRoute.spec.ts +++ b/deployables/extension/src/panel/pages/$activeRouteId/ActiveRoute.spec.ts @@ -1,8 +1,8 @@ -import { EOA_ZERO_ADDRESS } from '@/chains' import { getRoute } from '@/execution-routes' import { useDisconnectWalletConnectIfNeeded } from '@/providers' import { mockRoute, randomAddress, render } from '@/test-utils' import { waitFor } from '@testing-library/react' +import { EOA_ZERO_ADDRESS } from '@zodiac/chains' import { describe, expect, it, vi } from 'vitest' import { action, ActiveRoute, loader } from './ActiveRoute' diff --git a/deployables/extension/src/panel/pages/$activeRouteId/transactions/ContractAddress/index.tsx b/deployables/extension/src/panel/pages/$activeRouteId/transactions/ContractAddress/index.tsx index c91d0ae3d..10cea19c1 100644 --- a/deployables/extension/src/panel/pages/$activeRouteId/transactions/ContractAddress/index.tsx +++ b/deployables/extension/src/panel/pages/$activeRouteId/transactions/ContractAddress/index.tsx @@ -1,4 +1,4 @@ -import { EXPLORER_URL } from '@/chains' +import { EXPLORER_URL } from '@zodiac/chains' import { AddressInput, GhostLinkButton } from '@zodiac/ui' import { getAddress } from 'ethers' import { SquareArrowOutUpRight } from 'lucide-react' diff --git a/deployables/extension/src/panel/pages/$activeRouteId/transactions/Submit.tsx b/deployables/extension/src/panel/pages/$activeRouteId/transactions/Submit.tsx index ca07fddf1..7ba314447 100644 --- a/deployables/extension/src/panel/pages/$activeRouteId/transactions/Submit.tsx +++ b/deployables/extension/src/panel/pages/$activeRouteId/transactions/Submit.tsx @@ -1,4 +1,3 @@ -import { CHAIN_NAME, EXPLORER_URL, getChainId } from '@/chains' import { useExecutionRoute, useRouteConnect } from '@/execution-routes' import { usePilotIsReady } from '@/port-handling' import { getReadOnlyProvider } from '@/providers' @@ -12,6 +11,7 @@ import { decodeRolesV2Error, } from '@/utils' import { invariant } from '@epic-web/invariant' +import { CHAIN_NAME, EXPLORER_URL, getChainId } from '@zodiac/chains' import { errorToast, Modal, diff --git a/deployables/extension/src/panel/pages/$activeRouteId/transactions/Transaction.tsx b/deployables/extension/src/panel/pages/$activeRouteId/transactions/Transaction.tsx index 5c25f6a70..0f480f59e 100644 --- a/deployables/extension/src/panel/pages/$activeRouteId/transactions/Transaction.tsx +++ b/deployables/extension/src/panel/pages/$activeRouteId/transactions/Transaction.tsx @@ -1,7 +1,7 @@ -import { CHAIN_CURRENCY, getChainId } from '@/chains' import { useExecutionRoute } from '@/execution-routes' import type { TransactionState } from '@/state' import type { ExecutionRoute } from '@/types' +import { CHAIN_CURRENCY, getChainId } from '@zodiac/chains' import { CopyToClipboard, Divider, TextInput, ToggleButton } from '@zodiac/ui' import { formatEther, Fragment } from 'ethers' import { useState } from 'react' diff --git a/deployables/extension/src/panel/pages/$activeRouteId/transactions/Transactions.tsx b/deployables/extension/src/panel/pages/$activeRouteId/transactions/Transactions.tsx index 3e018c997..779133908 100644 --- a/deployables/extension/src/panel/pages/$activeRouteId/transactions/Transactions.tsx +++ b/deployables/extension/src/panel/pages/$activeRouteId/transactions/Transactions.tsx @@ -1,4 +1,3 @@ -import { getChainId } from '@/chains' import { useExecutionRoute } from '@/execution-routes' import { useProviderBridge } from '@/inject-bridge' import { usePilotIsReady } from '@/port-handling' @@ -7,6 +6,7 @@ import { useProvider } from '@/providers-ui' import { useDispatch, useTransactions } from '@/state' import { useGloballyApplicableTranslation } from '@/transaction-translation' import { invariant } from '@epic-web/invariant' +import { getChainId } from '@zodiac/chains' import { CopyToClipboard, GhostButton, Info, Page } from '@zodiac/ui' import { RefreshCcw } from 'lucide-react' import { useEffect, useRef } from 'react' diff --git a/deployables/extension/src/panel/pages/routes/edit.$routeId/ChainSelect.tsx b/deployables/extension/src/panel/pages/routes/edit.$routeId/ChainSelect.tsx index 3a2da2f80..9963de2a2 100644 --- a/deployables/extension/src/panel/pages/routes/edit.$routeId/ChainSelect.tsx +++ b/deployables/extension/src/panel/pages/routes/edit.$routeId/ChainSelect.tsx @@ -1,5 +1,5 @@ -import { CHAIN_NAME } from '@/chains' import { invariant } from '@epic-web/invariant' +import { CHAIN_NAME } from '@zodiac/chains' import { Select } from '@zodiac/ui' import type { ChainId } from 'ser-kit' diff --git a/deployables/extension/src/panel/pages/routes/edit.$routeId/EditRoute.tsx b/deployables/extension/src/panel/pages/routes/edit.$routeId/EditRoute.tsx index 58a786cf3..8b6fad2ef 100644 --- a/deployables/extension/src/panel/pages/routes/edit.$routeId/EditRoute.tsx +++ b/deployables/extension/src/panel/pages/routes/edit.$routeId/EditRoute.tsx @@ -1,4 +1,3 @@ -import { EOA_ZERO_ADDRESS } from '@/chains' import { getLastUsedRouteId, getRoute, @@ -26,6 +25,7 @@ import { } from '@/zodiac' import { invariantResponse } from '@epic-web/invariant' import { KnownContracts } from '@gnosis.pm/zodiac' +import { EOA_ZERO_ADDRESS } from '@zodiac/chains' import { Breadcrumbs, Error, diff --git a/deployables/extension/src/panel/pages/routes/edit.$routeId/useSafeDelegates.ts b/deployables/extension/src/panel/pages/routes/edit.$routeId/useSafeDelegates.ts index 854a51a97..d35a4ab50 100644 --- a/deployables/extension/src/panel/pages/routes/edit.$routeId/useSafeDelegates.ts +++ b/deployables/extension/src/panel/pages/routes/edit.$routeId/useSafeDelegates.ts @@ -1,8 +1,8 @@ -import { getChainId } from '@/chains' import { useRouteProvider } from '@/execution-routes' import { initSafeApiKit } from '@/safe' import type { ExecutionRoute } from '@/types' import { validateAddress } from '@/utils' +import { getChainId } from '@zodiac/chains' import { useEffect, useState } from 'react' import type { ChainId } from 'ser-kit' diff --git a/deployables/extension/src/panel/pages/routes/edit.$routeId/useSafesWithOwner.ts b/deployables/extension/src/panel/pages/routes/edit.$routeId/useSafesWithOwner.ts index 0527f8db3..4edd42a04 100644 --- a/deployables/extension/src/panel/pages/routes/edit.$routeId/useSafesWithOwner.ts +++ b/deployables/extension/src/panel/pages/routes/edit.$routeId/useSafesWithOwner.ts @@ -1,7 +1,7 @@ -import { getChainId } from '@/chains' import { initSafeApiKit } from '@/safe' import type { ExecutionRoute } from '@/types' import { validateAddress } from '@/utils' +import { getChainId } from '@zodiac/chains' import { useEffect, useState } from 'react' import type { ChainId } from 'ser-kit' diff --git a/deployables/extension/src/panel/pages/routes/edit.$routeId/wallet/ConnectWallet.tsx b/deployables/extension/src/panel/pages/routes/edit.$routeId/wallet/ConnectWallet.tsx index 7f9f7e759..5329a3ce8 100644 --- a/deployables/extension/src/panel/pages/routes/edit.$routeId/wallet/ConnectWallet.tsx +++ b/deployables/extension/src/panel/pages/routes/edit.$routeId/wallet/ConnectWallet.tsx @@ -1,10 +1,10 @@ -import { getChainId } from '@/chains' import { type InjectedWalletContextT, type WalletConnectResult, isConnected as isConnectedBase, } from '@/providers' import { type ExecutionRoute, ProviderType } from '@/types' +import { getChainId } from '@zodiac/chains' import { ZeroAddress } from 'ethers' import { type ChainId, parsePrefixedAddress } from 'ser-kit' import { InjectedWallet, InjectedWalletConnect } from './injectedWallet' diff --git a/deployables/extension/src/panel/pages/routes/edit.$routeId/wallet/SwitchChain.tsx b/deployables/extension/src/panel/pages/routes/edit.$routeId/wallet/SwitchChain.tsx index f1fcc0604..50d3ad18c 100644 --- a/deployables/extension/src/panel/pages/routes/edit.$routeId/wallet/SwitchChain.tsx +++ b/deployables/extension/src/panel/pages/routes/edit.$routeId/wallet/SwitchChain.tsx @@ -1,4 +1,4 @@ -import { CHAIN_NAME } from '@/chains' +import { CHAIN_NAME } from '@zodiac/chains' import { SecondaryButton, Warning } from '@zodiac/ui' import type { PropsWithChildren } from 'react' import type { ChainId } from 'ser-kit' diff --git a/deployables/extension/src/panel/pages/routes/list/ListRoutes.spec.ts b/deployables/extension/src/panel/pages/routes/list/ListRoutes.spec.ts index 20c5f0e55..5a2713f0d 100644 --- a/deployables/extension/src/panel/pages/routes/list/ListRoutes.spec.ts +++ b/deployables/extension/src/panel/pages/routes/list/ListRoutes.spec.ts @@ -1,4 +1,3 @@ -import { ETH_ZERO_ADDRESS, ZERO_ADDRESS } from '@/chains' import { getRoutes, saveLastUsedRouteId } from '@/execution-routes' import { connectMockWallet, @@ -10,6 +9,7 @@ import { } from '@/test-utils' import { screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { ETH_ZERO_ADDRESS, ZERO_ADDRESS } from '@zodiac/chains' import { expectRouteToBe } from '@zodiac/test-utils' import { describe, expect, it } from 'vitest' import { action, ListRoutes, loader } from './ListRoutes' diff --git a/deployables/extension/src/panel/providers-ui/ProvideProvider.tsx b/deployables/extension/src/panel/providers-ui/ProvideProvider.tsx index decd4dbce..8f0fd42bb 100644 --- a/deployables/extension/src/panel/providers-ui/ProvideProvider.tsx +++ b/deployables/extension/src/panel/providers-ui/ProvideProvider.tsx @@ -1,8 +1,8 @@ -import { getChainId } from '@/chains' import { useExecutionRoute } from '@/execution-routes' import { ForkProvider } from '@/providers' import type { Eip1193Provider } from '@/types' import { invariant } from '@epic-web/invariant' +import { getChainId } from '@zodiac/chains' import { AbiCoder, BrowserProvider, id, TransactionReceipt } from 'ethers' import { createContext, diff --git a/deployables/extension/src/panel/providers/fork-provider/readOnlyProvider.ts b/deployables/extension/src/panel/providers/fork-provider/readOnlyProvider.ts index 70212424c..b42c1f9cd 100644 --- a/deployables/extension/src/panel/providers/fork-provider/readOnlyProvider.ts +++ b/deployables/extension/src/panel/providers/fork-provider/readOnlyProvider.ts @@ -1,5 +1,5 @@ -import { RPC } from '@/chains' import type { JsonRpcRequest } from '@/types' +import { RPC } from '@zodiac/chains' import { JsonRpcProvider, toQuantity } from 'ethers' import EventEmitter from 'events' import type { ChainId } from 'ser-kit' diff --git a/deployables/extension/src/panel/providers/injected-provider/useConnectProvider.tsx b/deployables/extension/src/panel/providers/injected-provider/useConnectProvider.tsx index 4947c0852..43aa7d505 100644 --- a/deployables/extension/src/panel/providers/injected-provider/useConnectProvider.tsx +++ b/deployables/extension/src/panel/providers/injected-provider/useConnectProvider.tsx @@ -1,6 +1,6 @@ -import { CHAIN_CURRENCY, CHAIN_NAME, EXPLORER_URL, RPC } from '@/chains' import type { Eip1193Provider } from '@/types' import { invariant } from '@epic-web/invariant' +import { CHAIN_CURRENCY, CHAIN_NAME, EXPLORER_URL, RPC } from '@zodiac/chains' import { infoToast } from '@zodiac/ui' import { useEffect, useState } from 'react' import type { ChainId } from 'ser-kit' diff --git a/deployables/extension/src/panel/providers/wallet-connect/useDisconnectWalletConnectIfNeeded.spec.ts b/deployables/extension/src/panel/providers/wallet-connect/useDisconnectWalletConnectIfNeeded.spec.ts index 29ed7c53d..a121fefb7 100644 --- a/deployables/extension/src/panel/providers/wallet-connect/useDisconnectWalletConnectIfNeeded.spec.ts +++ b/deployables/extension/src/panel/providers/wallet-connect/useDisconnectWalletConnectIfNeeded.spec.ts @@ -1,6 +1,6 @@ -import { ETH_ZERO_ADDRESS } from '@/chains' import { createMockRoute, renderHook } from '@/test-utils' import { ProviderType } from '@/types' +import { ETH_ZERO_ADDRESS } from '@zodiac/chains' import { sleepTillIdle } from '@zodiac/test-utils' import { describe, expect, it, vi } from 'vitest' import { useDisconnectWalletConnectIfNeeded } from './useDisconnectWalletConnectIfNeeded' diff --git a/deployables/extension/src/panel/providers/wallet-connect/useWalletConnectProvider.ts b/deployables/extension/src/panel/providers/wallet-connect/useWalletConnectProvider.ts index 05d1b7812..dc2842376 100644 --- a/deployables/extension/src/panel/providers/wallet-connect/useWalletConnectProvider.ts +++ b/deployables/extension/src/panel/providers/wallet-connect/useWalletConnectProvider.ts @@ -1,5 +1,5 @@ -import { RPC } from '@/chains' import { invariant } from '@epic-web/invariant' +import { RPC } from '@zodiac/chains' import { useEffect, useState } from 'react' import { WALLETCONNECT_PROJECT_ID, diff --git a/deployables/extension/src/panel/transactionTranslations/useApplicableTranslation.ts b/deployables/extension/src/panel/transactionTranslations/useApplicableTranslation.ts index 2fafc74ef..884c2ccac 100644 --- a/deployables/extension/src/panel/transactionTranslations/useApplicableTranslation.ts +++ b/deployables/extension/src/panel/transactionTranslations/useApplicableTranslation.ts @@ -1,9 +1,9 @@ -import { getChainId } from '@/chains' import { useExecutionRoute } from '@/execution-routes' import { ForkProvider } from '@/providers' import { useProvider } from '@/providers-ui' import { type TransactionState, useDispatch, useTransactions } from '@/state' import { invariant } from '@epic-web/invariant' +import { getChainId } from '@zodiac/chains' import { useCallback, useEffect, useState } from 'react' import { type ChainId, diff --git a/deployables/extension/src/panel/transactionTranslations/useGloballyApplicableTranslation.ts b/deployables/extension/src/panel/transactionTranslations/useGloballyApplicableTranslation.ts index 644f4290d..65bbea8fe 100644 --- a/deployables/extension/src/panel/transactionTranslations/useGloballyApplicableTranslation.ts +++ b/deployables/extension/src/panel/transactionTranslations/useGloballyApplicableTranslation.ts @@ -1,8 +1,8 @@ -import { getChainId } from '@/chains' import { useExecutionRoute } from '@/execution-routes' import { ForkProvider } from '@/providers' import { useProvider } from '@/providers-ui' import { type TransactionState, useDispatch, useTransactions } from '@/state' +import { getChainId } from '@zodiac/chains' import { useCallback, useEffect } from 'react' import { type ChainId, diff --git a/deployables/extension/src/types.ts b/deployables/extension/src/types.ts index abc2448a8..922f4881b 100644 --- a/deployables/extension/src/types.ts +++ b/deployables/extension/src/types.ts @@ -1,10 +1,13 @@ -import type { ChainId, Route as CompleteRoute, PrefixedAddress } from 'ser-kit' +import type { + ExecutionRoute as BaseExecutionRoute, + HexAddress, + ProviderType, +} from '@zodiac/schema' +import type { ChainId } from 'ser-kit' import type { SupportedModuleType } from './panel/integrations/zodiac/types' -export enum ProviderType { - WalletConnect, - InjectedWallet, -} +export { ProviderType } from '@zodiac/schema' +export type { HexAddress } from '@zodiac/schema' export interface LegacyConnection { id: string @@ -27,16 +30,7 @@ export interface LegacyConnection { lastUsed?: number } -interface PartialExecutionRoute { - id: string - initiator: PrefixedAddress | undefined - avatar: PrefixedAddress - waypoints: CompleteRoute['waypoints'] | undefined -} - -export type ExecutionRoute = PartialExecutionRoute & { - providerType: ProviderType - label: string +export type ExecutionRoute = BaseExecutionRoute & { lastUsed?: number } @@ -66,5 +60,3 @@ export interface TransactionData { data?: HexAddress from?: HexAddress } - -export type HexAddress = `0x${string}` diff --git a/deployables/extension/test-utils/creators/createTransaction.ts b/deployables/extension/test-utils/creators/createTransaction.ts index 773f4ee3b..f12a3ae60 100644 --- a/deployables/extension/test-utils/creators/createTransaction.ts +++ b/deployables/extension/test-utils/creators/createTransaction.ts @@ -1,5 +1,5 @@ -import { ZERO_ADDRESS } from '@/chains' import { ExecutionStatus, type TransactionState } from '@/state' +import { ZERO_ADDRESS } from '@zodiac/chains' import { nanoid } from 'nanoid' export const createTransaction = ( diff --git a/deployables/extension/test-utils/creators/index.ts b/deployables/extension/test-utils/creators/index.ts index b149ffe96..27cd03387 100644 --- a/deployables/extension/test-utils/creators/index.ts +++ b/deployables/extension/test-utils/creators/index.ts @@ -1,9 +1,12 @@ +export { + createMockExecutionRoute as createMockRoute, + createRoleWaypoint, + createStartingWaypoint, + randomAddress, + randomPrefixedAddress, +} from '@zodiac/test-utils' export { createMockPort } from './createMockPort' -export { createMockRoute } from './createMockRoute' export { createMockTab } from './createMockTab' export type { MockTab } from './createMockTab' export { createMockWebRequest } from './createMockWebRequest' -export { createRoleWaypoint } from './createRoleWaypoint' -export { createStartingWaypoint } from './createStartingWaypoint' export { createTransaction } from './createTransaction' -export { randomAddress, randomPrefixedAddress } from './randomHex' diff --git a/deployables/extension/tsconfig.json b/deployables/extension/tsconfig.json index c359f2c8b..d1ccd22e9 100644 --- a/deployables/extension/tsconfig.json +++ b/deployables/extension/tsconfig.json @@ -22,7 +22,6 @@ "@/utils": ["./src/utils/index.ts"], "@/types": ["./src/types.ts"], "@/const": ["./src/const.ts"], - "@/chains": ["./src/chains/index.ts"], "@/execution-routes": ["./src/panel/execution-routes/index.ts"], "@/state": ["./src/panel/state/index.tsx"], "@/providers": ["./src/panel/providers/index.ts"], diff --git a/packages/chains/package.json b/packages/chains/package.json new file mode 100644 index 000000000..6705e915e --- /dev/null +++ b/packages/chains/package.json @@ -0,0 +1,24 @@ +{ + "name": "@zodiac/chains", + "private": true, + "type": "module", + "license": "UNLICENSED", + "sideEffects": false, + "packageManager": "pnpm@9.15.3", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts" + } + }, + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "@zodiac/schema": "workspace:*" + }, + "peerDependencies": { + "ser-kit": "1.0.7" + }, + "devDependencies": { + "ser-kit": "1.0.7" + } +} \ No newline at end of file diff --git a/packages/chains/src/const.ts b/packages/chains/src/const.ts new file mode 100644 index 000000000..6e0c5d463 --- /dev/null +++ b/packages/chains/src/const.ts @@ -0,0 +1,98 @@ +/* spell-checker: disable */ +import type { HexAddress } from '@zodiac/schema' +import type { ChainId, PrefixedAddress } from 'ser-kit' + +export const ZERO_ADDRESS: HexAddress = + '0x0000000000000000000000000000000000000000' +export const ETH_ZERO_ADDRESS: PrefixedAddress = + 'eth:0x0000000000000000000000000000000000000000' +export const EOA_ZERO_ADDRESS: PrefixedAddress = + 'eoa:0x0000000000000000000000000000000000000000' + +enum Chain { + ETH = 1, + OETH = 10, + GNO = 100, + SEP = 11155111, + MATIC = 137, + ARB1 = 42161, + AVAX = 43114, + BASE = 8453, +} + +export const RPC: Record = { + [Chain.ETH]: 'https://airlock.gnosisguild.org/api/v1/1/rpc', + [Chain.OETH]: 'https://airlock.gnosisguild.org/api/v1/10/rpc', + [Chain.GNO]: 'https://airlock.gnosisguild.org/api/v1/100/rpc', + [Chain.MATIC]: 'https://airlock.gnosisguild.org/api/v1/137/rpc', + [Chain.BASE]: 'https://airlock.gnosisguild.org/api/v1/8453/rpc', + [Chain.ARB1]: 'https://airlock.gnosisguild.org/api/v1/42161/rpc', + [Chain.AVAX]: 'https://airlock.gnosisguild.org/api/v1/43114/rpc', + [Chain.SEP]: 'https://airlock.gnosisguild.org/api/v1/11155111/rpc', +} + +export const EXPLORER_URL: Record = { + [Chain.ETH]: 'https://etherscan.io', + [Chain.OETH]: 'https://optimistic.etherscan.io', + [Chain.GNO]: 'https://gnosisscan.io', + [Chain.MATIC]: 'https://polygonscan.com', + [Chain.BASE]: 'https://basescan.org', + [Chain.ARB1]: 'https://arbiscan.io', + [Chain.AVAX]: 'https://snowtrace.io', + [Chain.SEP]: 'https://sepolia.etherscan.io', +} + +export const EXPLORER_API_URL: Record = { + [Chain.ETH]: 'https://api.etherscan.io/api', + [Chain.OETH]: 'https://api-optimistic.etherscan.io/api', + [Chain.GNO]: 'https://api.gnosisscan.io/api', + [Chain.MATIC]: 'https://api.polygonscan.com/api', + [Chain.BASE]: 'https://api.basescan.org/api', + [Chain.ARB1]: 'https://api.arbiscan.io/api', + [Chain.AVAX]: 'https://api.snowtrace.io/api', + [Chain.SEP]: 'https://api-sepolia.etherscan.io/api', +} + +export const EXPLORER_API_KEY: Record = { + [Chain.ETH]: 'N53BKW6ABNX7CNUK8QIXGRAQS2NME92YAN', + [Chain.OETH]: 'SM2FQ62U49I6H9V9CCEGFS34QGBK4IIJPH', + [Chain.GNO]: 'W575K6DTMSTVB7UFUSNW7GWQ4UWUARTJ7Z', + [Chain.MATIC]: 'NM937M1IZXVQ6QVDXS73XMF8JSAB677JWQ', + [Chain.BASE]: 'KCC7EQHE17IAQZA9TICUS6BQTJGZUDRNIY', + [Chain.ARB1]: 'SJ5BEYBBC3DNSKTH5BAEPFJXUZDAJ133UI', + [Chain.AVAX]: 'notrequired', + [Chain.SEP]: 'N53BKW6ABNX7CNUK8QIXGRAQS2NME92YAN', +} + +export const CHAIN_PREFIX: Record = { + [Chain.ETH]: 'eth', + [Chain.OETH]: 'oeth', + [Chain.GNO]: 'gno', + [Chain.MATIC]: 'matic', + [Chain.BASE]: 'base', + [Chain.ARB1]: 'arb1', + [Chain.AVAX]: 'avax', + [Chain.SEP]: 'sep', +} + +export const CHAIN_CURRENCY: Record = { + [Chain.ETH]: 'ETH', + [Chain.OETH]: 'ETH', + [Chain.GNO]: 'xDAI', + [Chain.MATIC]: 'MATIC', + [Chain.BASE]: 'ETH', + [Chain.ARB1]: 'ETH', + [Chain.AVAX]: 'AVAX', + [Chain.SEP]: 'ETH', +} + +export const CHAIN_NAME: Record = { + [Chain.ETH]: 'Ethereum', + [Chain.OETH]: 'Optimism', + [Chain.GNO]: 'Gnosis', + [Chain.MATIC]: 'Polygon', + [Chain.BASE]: 'Base', + [Chain.ARB1]: 'Arbitrum One', + [Chain.AVAX]: 'Avalanche C-Chain', + [Chain.SEP]: 'Sepolia', +} diff --git a/deployables/extension/src/chains/getChainId.ts b/packages/chains/src/getChainId.ts similarity index 100% rename from deployables/extension/src/chains/getChainId.ts rename to packages/chains/src/getChainId.ts diff --git a/deployables/extension/src/chains/index.ts b/packages/chains/src/index.ts similarity index 100% rename from deployables/extension/src/chains/index.ts rename to packages/chains/src/index.ts diff --git a/packages/chains/tsconfig.json b/packages/chains/tsconfig.json new file mode 100644 index 000000000..438144816 --- /dev/null +++ b/packages/chains/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "jsx": "react-jsx", + "downlevelIteration": true + }, + "include": ["./src/**/*"] +} diff --git a/packages/schema/package.json b/packages/schema/package.json new file mode 100644 index 000000000..8c84149af --- /dev/null +++ b/packages/schema/package.json @@ -0,0 +1,18 @@ +{ + "name": "@zodiac/schema", + "private": true, + "type": "module", + "license": "UNLICENSED", + "sideEffects": false, + "packageManager": "pnpm@9.15.3", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts" + } + }, + "dependencies": { + "ser-kit": "1.0.7", + "zod": "^3.23.8" + } +} \ No newline at end of file diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts new file mode 100644 index 000000000..208dfaf60 --- /dev/null +++ b/packages/schema/src/index.ts @@ -0,0 +1,2 @@ +export { ProviderType, executionRouteSchema } from './routeSchema' +export type { ExecutionRoute, HexAddress } from './routeSchema' diff --git a/packages/schema/src/routeSchema.ts b/packages/schema/src/routeSchema.ts new file mode 100644 index 000000000..24b077e0c --- /dev/null +++ b/packages/schema/src/routeSchema.ts @@ -0,0 +1,128 @@ +import { chains, type PrefixedAddress } from 'ser-kit' +import { z } from 'zod' + +const chainIdSchema = z.union([ + z.literal(chains[0].chainId), + z.literal(chains[1].chainId), + z.literal(chains[2].chainId), + z.literal(chains[3].chainId), + z.literal(chains[4].chainId), + z.literal(chains[5].chainId), + z.literal(chains[6].chainId), + z.literal(chains[7].chainId), +]) + +export type HexAddress = `0x${string}` + +const isHexAddress = (value: string): value is HexAddress => + value.startsWith('0x') && value.length > 2 + +const addressSchema = z.custom( + (value) => typeof value === 'string' && isHexAddress(value), +) + +const prefixedAddressSchema = z.custom((value) => { + if (typeof value !== 'string') { + return false + } + + const [prefix, address] = value.split(':') + + if (!isHexAddress(address)) { + return false + } + + return chains.some(({ shortName }) => prefix === shortName) +}) + +const safeSchema = z.object({ + type: z.literal('SAFE'), + address: addressSchema, + prefixedAddress: prefixedAddressSchema, + chain: chainIdSchema, + threshold: z.number(), +}) + +const rolesSchema = z.object({ + type: z.literal('ROLES'), + address: addressSchema, + prefixedAddress: prefixedAddressSchema, + chain: chainIdSchema, + multisend: addressSchema.array(), + version: z.union([z.literal(1), z.literal(2)]), +}) + +const delaySchema = z.object({ + type: z.literal('DELAY'), + address: addressSchema, + prefixedAddress: prefixedAddressSchema, + chain: chainIdSchema, +}) + +const ownConnectionSchema = z.object({ + type: z.literal('OWNS'), + from: prefixedAddressSchema, +}) + +const isEnabledConnectionSchema = z.object({ + type: z.literal('IS_ENABLED'), + from: prefixedAddressSchema, +}) + +const isMemberConnectionSchema = z.object({ + type: z.literal('IS_MEMBER'), + roles: z.string().array(), + defaultRole: z.string().optional(), + from: prefixedAddressSchema, +}) + +const contractSchema = z.discriminatedUnion('type', [ + safeSchema, + rolesSchema, + delaySchema, +]) + +const waypointSchema = z.object({ + account: contractSchema, + connection: z.discriminatedUnion('type', [ + ownConnectionSchema, + isEnabledConnectionSchema, + isMemberConnectionSchema, + ]), +}) + +const eoaSchema = z.object({ + type: z.literal('EOA'), + address: addressSchema, + prefixedAddress: prefixedAddressSchema, +}) + +const startingPointSchema = z.object({ + account: z.discriminatedUnion('type', [ + eoaSchema, + safeSchema, + rolesSchema, + delaySchema, + ]), +}) + +export enum ProviderType { + WalletConnect, + InjectedWallet, +} + +const walletConnectType = z.literal(ProviderType.WalletConnect) +const injectedProviderType = z.literal(ProviderType.InjectedWallet) + +const providerTypeSchema = z.union([walletConnectType, injectedProviderType]) + +export const executionRouteSchema = z.object({ + id: z.string(), + label: z.string(), + providerType: providerTypeSchema, + avatar: prefixedAddressSchema, + initiator: prefixedAddressSchema.optional(), + waypoints: z.tuple([startingPointSchema]).rest(waypointSchema).optional(), +}) + +export type ExecutionRoute = z.infer diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index ae514e52c..39564938c 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -5,6 +5,7 @@ "license": "UNLICENSED", "sideEffects": false, "packageManager": "pnpm@9.15.3", + "scripts": {"test": "vitest"}, "exports": { ".": { "types": "./src/index.ts", @@ -14,14 +15,19 @@ "dependencies": { "@epic-web/invariant": "^1.0.0", "@testing-library/react": "^16.0.1", + "@zodiac/schema": "workspace:*", + "@zodiac/chains": "workspace:*", "vitest": "2.1.8" }, "peerDependencies": { "react": "19.0.0", - "react-router": "7.1.1" + "react-router": "7.1.1", + "ser-kit": "1.0.7" }, "devDependencies": { + "@types/node": "^22.10.5", "@types/react": "^19.0.3", - "@types/react-dom": "^19.0.2" + "@types/react-dom": "^19.0.2", + "ser-kit": "1.0.7" } } \ No newline at end of file diff --git a/packages/test-utils/src/InspectRoute.tsx b/packages/test-utils/src/InspectRoute.tsx new file mode 100644 index 000000000..88ee2e2e9 --- /dev/null +++ b/packages/test-utils/src/InspectRoute.tsx @@ -0,0 +1,12 @@ +import { useLocation } from 'react-router' + +export const InspectRoute = () => { + const location = useLocation() + + return ( +
+ ) +} diff --git a/deployables/extension/test-utils/creators/createMockRoute.ts b/packages/test-utils/src/creators/createMockExecutionRoute.ts similarity index 75% rename from deployables/extension/test-utils/creators/createMockRoute.ts rename to packages/test-utils/src/creators/createMockExecutionRoute.ts index d4c0a5407..98e4675a4 100644 --- a/deployables/extension/test-utils/creators/createMockRoute.ts +++ b/packages/test-utils/src/creators/createMockExecutionRoute.ts @@ -1,10 +1,10 @@ -import { ETH_ZERO_ADDRESS, ZERO_ADDRESS } from '@/chains' -import { type ExecutionRoute, ProviderType } from '@/types' +import { ETH_ZERO_ADDRESS, ZERO_ADDRESS } from '@zodiac/chains' +import { ProviderType, type ExecutionRoute } from '@zodiac/schema' import { randomUUID } from 'crypto' import { AccountType, formatPrefixedAddress } from 'ser-kit' import { randomHex } from './randomHex' -export const createMockRoute = ( +export const createMockExecutionRoute = ( route: Partial = {}, ): ExecutionRoute => ({ id: randomUUID(), diff --git a/deployables/extension/test-utils/creators/createRoleWaypoint.ts b/packages/test-utils/src/creators/createRoleWaypoint.ts similarity index 93% rename from deployables/extension/test-utils/creators/createRoleWaypoint.ts rename to packages/test-utils/src/creators/createRoleWaypoint.ts index bd288802b..45c2f1aa7 100644 --- a/deployables/extension/test-utils/creators/createRoleWaypoint.ts +++ b/packages/test-utils/src/creators/createRoleWaypoint.ts @@ -1,4 +1,4 @@ -import type { HexAddress } from '@/types' +import type { HexAddress } from '@zodiac/schema' import { AccountType, ConnectionType, diff --git a/deployables/extension/test-utils/creators/createStartingWaypoint.ts b/packages/test-utils/src/creators/createStartingWaypoint.ts similarity index 100% rename from deployables/extension/test-utils/creators/createStartingWaypoint.ts rename to packages/test-utils/src/creators/createStartingWaypoint.ts diff --git a/packages/test-utils/src/creators/index.ts b/packages/test-utils/src/creators/index.ts new file mode 100644 index 000000000..4003fcad3 --- /dev/null +++ b/packages/test-utils/src/creators/index.ts @@ -0,0 +1,4 @@ +export { createMockExecutionRoute } from './createMockExecutionRoute' +export { createRoleWaypoint } from './createRoleWaypoint' +export { createStartingWaypoint } from './createStartingWaypoint' +export { randomAddress, randomHex, randomPrefixedAddress } from './randomHex' diff --git a/deployables/extension/test-utils/creators/randomHex.ts b/packages/test-utils/src/creators/randomHex.ts similarity index 92% rename from deployables/extension/test-utils/creators/randomHex.ts rename to packages/test-utils/src/creators/randomHex.ts index 7f1adaf96..8467b97c8 100644 --- a/deployables/extension/test-utils/creators/randomHex.ts +++ b/packages/test-utils/src/creators/randomHex.ts @@ -1,4 +1,4 @@ -import type { HexAddress } from '@/types' +import type { HexAddress } from '@zodiac/schema' import { formatPrefixedAddress, type ChainId } from 'ser-kit' export const randomHex = (size: number): HexAddress => { diff --git a/packages/test-utils/src/getCurrentPath.spec.ts b/packages/test-utils/src/getCurrentPath.spec.ts new file mode 100644 index 000000000..1076979d7 --- /dev/null +++ b/packages/test-utils/src/getCurrentPath.spec.ts @@ -0,0 +1,8 @@ +import { describe, expect, it } from 'vitest' +import { getCurrentPath } from './getCurrentPath' + +describe('getCurrentPath', () => { + it('is possible to append string values', () => { + expect(getCurrentPath('/path', { key: 'value' })).toEqual('/path?key=value') + }) +}) diff --git a/packages/test-utils/src/getCurrentPath.ts b/packages/test-utils/src/getCurrentPath.ts new file mode 100644 index 000000000..6c0b759ae --- /dev/null +++ b/packages/test-utils/src/getCurrentPath.ts @@ -0,0 +1,17 @@ +export type SearchParams = Record + +export const getCurrentPath = (path: string, searchParams: SearchParams) => { + const url = new URL(path, 'http://localhost') + + Object.entries(searchParams).forEach(([key, value]) => { + if (value == null) { + url.searchParams.delete(key) + } else if (typeof value === 'number') { + url.searchParams.set(key, value.toString()) + } else { + url.searchParams.set(key, value) + } + }) + + return `${url.pathname}${url.search}` +} diff --git a/packages/test-utils/src/index.ts b/packages/test-utils/src/index.ts index 3ef191fb2..d8d93ad45 100644 --- a/packages/test-utils/src/index.ts +++ b/packages/test-utils/src/index.ts @@ -1,5 +1,8 @@ +export * from './creators' export { expectRouteToBe, render } from './render' export type { RenderOptions, Route } from './render' +export { renderFramework } from './renderFramework' +export type { FrameworkRoute, RouteModule } from './renderFramework' export { renderHook } from './renderHook' export type { RenderHookOptions } from './renderHook' export { sleepTillIdle } from './sleepTillIdle' diff --git a/packages/test-utils/src/render.tsx b/packages/test-utils/src/render.tsx index 117d0d299..bc177a79a 100644 --- a/packages/test-utils/src/render.tsx +++ b/packages/test-utils/src/render.tsx @@ -4,10 +4,10 @@ import type { ComponentType } from 'react' import { createMemoryRouter, RouterProvider, - useLocation, type ActionFunction, type LoaderFunction, } from 'react-router' +import { InspectRoute } from './InspectRoute' import { TestElement, waitForTestElement } from './TestElement' import { sleepTillIdle } from './sleepTillIdle' @@ -25,13 +25,31 @@ export type RenderOptions = Parameters[1] & { * `expectRouteToBe` helper from the render result. */ inspectRoutes?: string[] + /** + * Search params that should be added to the current route. + * You can also include them in the route but this way is usually + * more readable. + */ + searchParams?: Record } export const render = async ( currentPath: string, routes: Route[], - { inspectRoutes = [], ...options }: RenderOptions = {}, + { inspectRoutes = [], searchParams = {}, ...options }: RenderOptions = {}, ) => { + const url = new URL(currentPath, 'http://localhost') + + Object.entries(searchParams).forEach(([key, value]) => { + if (value == null) { + url.searchParams.delete(key) + } else if (typeof value === 'number') { + url.searchParams.set(key, value.toString()) + } else { + url.searchParams.set(key, value) + } + }) + const router = createMemoryRouter( [ { @@ -61,17 +79,6 @@ export const render = async ( return result } -const InspectRoute = () => { - const location = useLocation() - - return ( -
- ) -} - export const expectRouteToBe = (expectedPathName: string) => waitFor(() => { const testElement = screen.getByTestId('test-route-element-id') diff --git a/packages/test-utils/src/renderFramework.tsx b/packages/test-utils/src/renderFramework.tsx new file mode 100644 index 000000000..07e3c0097 --- /dev/null +++ b/packages/test-utils/src/renderFramework.tsx @@ -0,0 +1,107 @@ +import { render, type RenderResult } from '@testing-library/react' +import type { ComponentType } from 'react' +import { createRoutesStub, useLoaderData, useParams } from 'react-router' +import type { + CreateActionData, + CreateComponentProps, + CreateLoaderData, + CreateServerLoaderArgs, +} from 'react-router/route-module' +import { getCurrentPath } from './getCurrentPath' +import { InspectRoute } from './InspectRoute' +import type { RenderOptions } from './render' +import { sleepTillIdle } from './sleepTillIdle' +import { TestElement, waitForTestElement } from './TestElement' + +type Func = (...args: any[]) => unknown + +export type RouteModule = { + meta?: Func + links?: Func + headers?: Func + loader?: Func + clientLoader?: Func + action?: Func + clientAction?: Func + HydrateFallback?: unknown + default?: unknown + ErrorBoundary?: unknown + [key: string]: unknown +} + +type Info = { + parents: [ + { + parents: [] + id: 'root' + file: 'root.tsx' + path: '' + params: {} & { [key: string]: string | undefined } + module: Module + loaderData: CreateLoaderData<{}> + actionData: CreateActionData<{}> + }, + ] + id: any + file: any + path: any + params: {} & { [key: string]: string | undefined } + module: Module + loaderData: CreateLoaderData + actionData: CreateActionData +} + +export type FrameworkRoute< + Module extends RouteModule, + Loader extends Func | undefined = Module['loader'], +> = { + path: string + Component: ComponentType>> + loader?: Loader extends Func + ? (args: CreateServerLoaderArgs>) => ReturnType + : never +} + +export async function renderFramework( + currentPath: string, + route: FrameworkRoute, + { inspectRoutes = [], searchParams = {}, ...options }: RenderOptions, +): Promise { + const Stub = createRoutesStub([ + { + path: '/', + Component: TestElement, + children: [ + { + path: route.path, + // @ts-expect-error + loader: route.loader, + Component() { + const loaderData = useLoaderData() + const params = useParams() + + return ( + + ) + }, + }, + + ...inspectRoutes.map((path) => ({ path, Component: InspectRoute })), + ], + }, + ]) + + const result = render( + , + options, + ) + + await waitForTestElement() + await sleepTillIdle() + + return result +} diff --git a/packages/test-utils/tsconfig.json b/packages/test-utils/tsconfig.json index 7e86d78c2..438144816 100644 --- a/packages/test-utils/tsconfig.json +++ b/packages/test-utils/tsconfig.json @@ -10,7 +10,7 @@ "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "ESNext", - "moduleResolution": "Node", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "verbatimModuleSyntax": true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c05fbf65e..b0c16488f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,12 +34,21 @@ importers: deployables/app: dependencies: + '@epic-web/invariant': + specifier: ^1.0.0 + version: 1.0.0 '@react-router/node': specifier: ^7.1.1 version: 7.1.1(react-router@7.1.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.7.3) '@react-router/serve': specifier: ^7.1.1 version: 7.1.1(react-router@7.1.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.7.3) + '@zodiac/schema': + specifier: workspace:* + version: link:../../packages/schema + '@zodiac/ui': + specifier: workspace:* + version: link:../../packages/ui isbot: specifier: ^5.1.17 version: 5.1.21 @@ -52,6 +61,9 @@ importers: react-router: specifier: ^7.1.1 version: 7.1.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + zod: + specifier: ^3.23.8 + version: 3.24.1 devDependencies: '@cloudflare/workers-types': specifier: 4.20241230.0 @@ -62,6 +74,15 @@ importers: '@react-router/dev': specifier: ^7.1.1 version: 7.1.1(@react-router/serve@7.1.1(react-router@7.1.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.7.3))(@types/node@22.10.5)(babel-plugin-macros@3.1.0)(jiti@2.4.1)(less@4.2.1)(lightningcss@1.28.2)(react-router@7.1.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(sass@1.82.0)(stylus@0.62.0)(typescript@5.7.3)(vite@5.4.11(@types/node@22.10.5)(less@4.2.1)(lightningcss@1.28.2)(sass@1.82.0)(stylus@0.62.0))(wrangler@3.100.0(@cloudflare/workers-types@4.20241230.0)(bufferutil@4.0.8)(utf-8-validate@5.0.10))(yaml@2.6.1) + '@testing-library/jest-dom': + specifier: ^6.4.6 + version: 6.6.3 + '@testing-library/react': + specifier: ^16.0.1 + version: 16.1.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.2(@types/react@19.0.4))(@types/react@19.0.4)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@testing-library/user-event': + specifier: 14.5.2 + version: 14.5.2(@testing-library/dom@10.4.0) '@types/node': specifier: ^22.0.0 version: 22.10.5 @@ -71,9 +92,18 @@ importers: '@types/react-dom': specifier: ^19.0.1 version: 19.0.2(@types/react@19.0.4) + '@vitest/coverage-v8': + specifier: 2.1.8 + version: 2.1.8(vitest@2.1.8(@types/node@22.10.5)(happy-dom@16.5.3)(less@4.2.1)(lightningcss@1.28.2)(sass@1.82.0)(stylus@0.62.0)) + '@zodiac/test-utils': + specifier: workspace:* + version: link:../../packages/test-utils autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.4.49) + eslint: + specifier: ^9.7.0 + version: 9.17.0(jiti@2.4.1) postcss: specifier: ^8.4.49 version: 8.4.49 @@ -89,6 +119,9 @@ importers: vite-tsconfig-paths: specifier: ^5.1.4 version: 5.1.4(typescript@5.7.3)(vite@5.4.11(@types/node@22.10.5)(less@4.2.1)(lightningcss@1.28.2)(sass@1.82.0)(stylus@0.62.0)) + vitest: + specifier: 2.1.8 + version: 2.1.8(@types/node@22.10.5)(happy-dom@16.5.3)(less@4.2.1)(lightningcss@1.28.2)(sass@1.82.0)(stylus@0.62.0) wrangler: specifier: ^3.87.0 version: 3.100.0(@cloudflare/workers-types@4.20241230.0)(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -177,6 +210,12 @@ importers: '@headlessui/react': specifier: ^2.2.0 version: 2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@zodiac/chains': + specifier: workspace:* + version: link:../../packages/chains + '@zodiac/schema': + specifier: workspace:* + version: link:../../packages/schema '@zodiac/ui': specifier: workspace:* version: link:../../packages/ui @@ -480,6 +519,28 @@ importers: specifier: 3.100.0 version: 3.100.0(@cloudflare/workers-types@4.20241230.0)(bufferutil@4.0.8)(utf-8-validate@5.0.10) + packages/chains: + dependencies: + '@epic-web/invariant': + specifier: ^1.0.0 + version: 1.0.0 + '@zodiac/schema': + specifier: workspace:* + version: link:../schema + devDependencies: + ser-kit: + specifier: 1.0.7 + version: 1.0.7(bufferutil@4.0.8)(typescript@5.7.3)(utf-8-validate@5.0.10)(zod@3.24.1) + + packages/schema: + dependencies: + ser-kit: + specifier: 1.0.7 + version: 1.0.7(bufferutil@4.0.8)(typescript@5.7.3)(utf-8-validate@5.0.10)(zod@3.24.1) + zod: + specifier: ^3.23.8 + version: 3.24.1 + packages/test-utils: dependencies: '@epic-web/invariant': @@ -488,6 +549,12 @@ importers: '@testing-library/react': specifier: ^16.0.1 version: 16.1.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.2(@types/react@19.0.4))(@types/react@19.0.4)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@zodiac/chains': + specifier: workspace:* + version: link:../chains + '@zodiac/schema': + specifier: workspace:* + version: link:../schema react: specifier: 19.0.0 version: 19.0.0 @@ -498,12 +565,18 @@ importers: specifier: 2.1.8 version: 2.1.8(@types/node@22.10.5)(happy-dom@16.5.3)(less@4.2.1)(lightningcss@1.28.2)(sass@1.82.0)(stylus@0.62.0) devDependencies: + '@types/node': + specifier: ^22.10.5 + version: 22.10.5 '@types/react': specifier: ^19.0.3 version: 19.0.4 '@types/react-dom': specifier: ^19.0.2 version: 19.0.2(@types/react@19.0.4) + ser-kit: + specifier: 1.0.7 + version: 1.0.7(bufferutil@4.0.8)(typescript@5.7.3)(utf-8-validate@5.0.10)(zod@3.24.1) packages/ui: dependencies: @@ -5972,9 +6045,6 @@ packages: resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==} engines: {node: '>=10'} - pkg-types@1.2.1: - resolution: {integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==} - pkg-types@1.3.0: resolution: {integrity: sha512-kS7yWjVFCkIw9hqdJBoMxDdzEngmkr5FXeWZZfQ6GoYacjVnsW6l2CcYW/0ThD0vF4LPJgVYnrg4d0uuhwYQbg==} @@ -10381,7 +10451,7 @@ snapshots: '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) '@vanilla-extract/babel-plugin-debug-ids': 1.2.0 '@vanilla-extract/css': 1.17.0(babel-plugin-macros@3.1.0) - esbuild: 0.17.6 + esbuild: 0.17.19 eval: 0.1.8 find-up: 5.0.0 javascript-stringify: 2.1.0 @@ -13433,7 +13503,7 @@ snapshots: isomorphic-timers-promises@1.0.1: {} - isows@1.0.6(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)): + isows@1.0.6(ws@8.18.0): dependencies: ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -13665,7 +13735,7 @@ snapshots: local-pkg@0.5.1: dependencies: mlly: 1.7.3 - pkg-types: 1.2.1 + pkg-types: 1.3.0 locate-path@5.0.0: dependencies: @@ -14505,11 +14575,11 @@ snapshots: ox@0.6.0(typescript@5.7.3)(zod@3.24.1): dependencies: '@adraffy/ens-normalize': 1.11.0 - '@noble/curves': 1.7.0 - '@noble/hashes': 1.6.1 - '@scure/bip32': 1.6.0 - '@scure/bip39': 1.5.0 - abitype: 1.0.7(typescript@5.7.3)(zod@3.24.1) + '@noble/curves': 1.8.0 + '@noble/hashes': 1.7.0 + '@scure/bip32': 1.6.1 + '@scure/bip39': 1.5.1 + abitype: 1.0.8(typescript@5.7.3)(zod@3.24.1) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.7.3 @@ -14680,12 +14750,6 @@ snapshots: dependencies: find-up: 5.0.0 - pkg-types@1.2.1: - dependencies: - confbox: 0.1.8 - mlly: 1.7.3 - pathe: 1.1.2 - pkg-types@1.3.0: dependencies: confbox: 0.1.8 @@ -16200,7 +16264,7 @@ snapshots: '@scure/bip32': 1.6.0 '@scure/bip39': 1.5.0 abitype: 1.0.7(typescript@5.7.3)(zod@3.24.1) - isows: 1.0.6(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + isows: 1.0.6(ws@8.18.0) ox: 0.6.0(typescript@5.7.3)(zod@3.24.1) webauthn-p256: 0.0.10 ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)